Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 14 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,25 @@

Cloudflare Computer is a virtual filesystem that lives inside a
Durable Object. The Durable Object holds the authoritative state in
SQLite and exposes the filesystem to a shell through a pluggable
backend. Two backends ship today:
SQLite and exposes one pluggable execution surface through
`workspace.runtime`. Three backends ship today:

- **Container** projects the SQLite state into a sandbox container as
a real FUSE mount. A sandbox-side daemon (`computerd`) mounts the state
as a filesystem and syncs changes back over a capnweb RPC channel.
Full Linux userland, real binaries, real network.
- **Worker** runs the shell as [just-bash](https://github.com/vercel-labs/just-bash)
inside a Dynamic Worker loaded through `env.LOADER`. The shell
reaches the host workspace over Workers RPC, so there is no second
store and no sync round trip. Broad textual tooling (`cat`,
`grep`, `awk`, `sed`, `jq`, ...), no container lifecycle.

A single Workspace can host more than one backend at the same
time. Each backend registers under a stable `id`; `shell.exec`
defaults to the first one in the list and the caller routes a
specific call elsewhere with `{ backend: "sandbox" }`. Common
shape: a Worker backend for cheap textual tooling that runs every
command cold, plus a Container backend the agent reaches for when
it needs a real Linux environment for `npm`, `git`, or anything
else the worker isolate can't host. Each backend keeps its own
sync cursors and connects lazily on first use.
- **Isolate shell** runs [just-bash](https://github.com/vercel-labs/just-bash)
in a Dynamic Worker. It reaches the authoritative Workspace over
Workers RPC, so there is no second store or sync round trip.
- **Isolate JavaScript** runs an ECMAScript module in a fresh Dynamic
Worker with structured input/results, durable relative imports,
configured libraries, Workspace-backed `node:fs/promises`, and trusted `ws:git` and
`ws:artifacts` modules.

A Workspace may register multiple backends under stable IDs.
`workspace.runtime.exec(source, { backend })` is the single execution
entry point; the selected backend defines whether `source` is a shell
command or an ECMAScript module. Backends connect lazily on first use.

Workspace can also be constructed without a backend at all, giving
callers the filesystem on its own.
Expand Down
8 changes: 4 additions & 4 deletions docs/01_vfs.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@ via FUSE) create it like any other directory.

## Conventions

- **Absolute paths only.** Every fs and shell call takes an absolute
- **Absolute host paths.** Every `workspace.fs` path and command-backend `cwd` takes an absolute
path starting with `/`. Relative paths are rejected with `EINVAL`.
Resolve paths against `process.cwd()` (or the `cwd` option on
`shell.exec`) at the call site if you need relative semantics.
`runtime.exec`) at the call site if you need relative semantics.
- **Forward slashes.** Paths are POSIX-style. Backslashes are not
separators.
- **No trailing slash.** `/workspace/foo` and `/workspace/foo/` are
Expand All @@ -93,7 +93,7 @@ target shape:
construction.
- Read-only mounts (the default) reject all writes under their root
with `EROFS`. Read-write mounts mirror writes back to the provider.
- Writes that originate from `shell.exec` under a read-only mount
- Writes that originate from `runtime.exec` under a read-only mount
are silently dropped on the post-exec pull (see
[02. Sync Protocol](./02_sync_protocol.md)).

Expand Down Expand Up @@ -135,7 +135,7 @@ pinned `DISABLE_FUSE=1`, which produced a degraded mode where:
directly; synchronization with the DO-side VFS happens over RPC
through the post-exec pull bracket and explicit
`workspace.push()` / `workspace.pull()` calls.
- Writes performed by `shell.exec` are picked up by the post-exec
- Writes performed by `runtime.exec` are picked up by the post-exec
pull. `workspace.push()` flushes pending DO-side writes to the
container without waiting for the next `exec()`. Use these when
you need to synchronize the two sides outside of a command run.
Expand Down
23 changes: 12 additions & 11 deletions docs/02_sync_protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,14 +294,14 @@ edited file) shows up exactly once on the wire. See
- **Concurrent mutators.** `Workspace.push()` and `Workspace.pull()`
go through a per-Workspace tail-promise FIFO. Two concurrent
callers queue — the second can't enter `pushOnce` / `pullOnce`
until the first has resolved or rejected. The shell exec bracket
drives `push()` / `pull()` through the same facade, so
`shell.exec()` calls participate in the FIFO automatically.
Rejections aren't contagious: a failed mutation surfaces its
error to its own caller without poisoning the queue for the next.
Pure reads on `Workspace.fs` bypass the FIFO entirely — they hit
the local SQLite store, which the DO runtime already serialises
internally through its input gates.
until the first has resolved or rejected. A command's pre-exec push
and post-stream pull each use this facade, but the FIFO is not held
for the command's lifetime: overlapping commands and explicit sync
calls are not one transaction. Rejections aren't contagious: a
failed mutation surfaces its error to its own caller without
poisoning the queue for the next. Pure reads on `Workspace.fs`
bypass the FIFO entirely — they hit the local SQLite store, which
the DO runtime already serialises internally through its input gates.

## Conflict semantics

Expand All @@ -322,8 +322,9 @@ the input gate wins and that is the authoritative state.
The per-`Workspace` tail-promise FIFO adds a second layer of
serialisation for `push()` and `pull()`: a concurrent pair of callers
that both trigger sync operations will queue at the FIFO before either
enters `pushOnce` / `pullOnce`. `shell.exec()` participates in the
same FIFO automatically.
enters `pushOnce` / `pullOnce`. The push and pull phases of
`runtime.exec()` use that FIFO independently; the running command does
not hold it.

### Across two containers sharing one Workspace

Expand Down Expand Up @@ -450,7 +451,7 @@ from the DO. Two options worth weighing later:
- **Stub entries with an `ignored` flag** on `stat()`, surfaced via
`readdir`. Easy to retrofit; surprising for tools that walk the tree
and don't check the flag.
- **An explicit shell-only namespace** — e.g. `workspace.shell.readdir`
- **An explicit shell-only namespace** — e.g. `workspace.runtime.readdir`
returns container-only entries, `workspace.fs.readdir` stays clean.
Cleaner separation, larger API surface.

Expand Down
6 changes: 3 additions & 3 deletions docs/04_filesystem_interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ file could be large.
```ts
interface Workspace {
fs: WorkspaceFilesystem;
shell: WorkspaceShell; // see 05_shell_interface.md
runtime: WorkspaceRuntime; // see 05_runtime_interface.md
}
```

Expand Down Expand Up @@ -232,7 +232,7 @@ const paths = await fs.ls("/workspace/.agents/skills");
### `grep`

Available on `Workspace.fs` for parity with the agent tools, and on
`Workspace.shell` when you want it to run inside the container (faster
`Workspace.runtime` when you want it to run inside the container (faster
for large trees because it uses ripgrep).

```ts
Expand Down Expand Up @@ -261,7 +261,7 @@ for (const hit of hits) {
}
```

See [05. Shell Interface](./05_shell_interface.md) for the container-side
See [05. Shell Interface](./05_runtime_interface.md) for the container-side
variant.

## Error handling
Expand Down
109 changes: 109 additions & 0 deletions docs/05_runtime_interface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# 05. Runtime Interface

Workspace exposes one execution router:

```ts
const handle = await workspace.runtime.exec(source, {
backend: "container-shell",
cwd: "/workspace",
encoding: "utf8",
});

const result = await handle.result();
```

The backend ID defines how `source` is interpreted. Command runtimes accept shell syntax; module runtimes accept their documented programming language.

## API

```ts
interface WorkspaceRuntime {
exec(source: string, options?: WorkspaceRuntimeExecOptions): Promise<WorkspaceRuntimeExecHandle>;
getExec(id: string, options?: WorkspaceRuntimeGetOptions): Promise<WorkspaceRuntimeExecHandle>;
killExec(id: string, options?: WorkspaceRuntimeKillOptions): Promise<void>;
disposeExec(id: string, options?: WorkspaceRuntimeDisposeOptions): Promise<void>;
}

interface WorkspaceRuntimeExecOptions {
id?: string;
backend?: string;
cwd?: string;
encoding?: "utf8";
input?: WorkspaceRuntimeValue;
timeoutMs?: number;
}

interface WorkspaceRuntimeExecHandle extends ReadableStream<WorkspaceRuntimeEvent> {
readonly id: string;
readonly backend: string;
result(): Promise<WorkspaceRuntimeResult>;
kill(signal?: KillSignal): Promise<void>;
[Symbol.dispose](): void;
}
```

`input` is accepted by structured module backends and rejected by command backends. `cwd` is the command working directory or the base for durable relative module imports. A handle is single-consumer: call `result()` or consume its event stream, not both. Repeated `result()` calls return the same promise. `backend` records the resolved backend needed for later reattachment.

## Results

```ts
interface WorkspaceRuntimeResult {
status: "completed" | "failed" | "cancelled";
exitCode: number;
stdout: Uint8Array | string;
stderr: Uint8Array | string;
value?: WorkspaceRuntimeValue;
pushed: number;
pulled: number;
skipped: SkippedEntry[];
sync:
| { status: "complete"; applied: number; skipped: SkippedEntry[] }
| { status: "pending"; applied: number; skipped: SkippedEntry[]; error: string };
}
```

Command backends leave `value` unset. `isolate-javascript` uses `value` for the module's structured return value and reports a zero-entry completed sync. A command can complete while its post-command pull fails; in that case `sync.status` is `"pending"`, and a configured `SyncRetryScheduler` can durably retry the pull without rerunning the command.

## Backend routing

```ts
await workspace.runtime.exec("grep -R TODO .", {
backend: "isolate-shell",
});

await workspace.runtime.exec("npm test", {
backend: "container-shell",
});

await workspace.runtime.exec(
`
import fs from "node:fs/promises";
export default async () => fs.readFile("/workspace/package.json", "utf8");
`,
{ backend: "isolate-javascript" },
);
```

Omitting `backend` selects the first configured backend. Backend selection is routing, not authorization; public gateways must validate it against server-side policy.

## Command synchronization

Command backends continue to use the existing synchronization bracket:

```text
push → spawn → events/result → pull
```

A backend with `sync: "none"`, such as `isolate-shell`, shares the host store and reports zero push/pull counts. A Container has its own VFS and synchronizes changes before and after command execution. Fully draining either `result()` or the event stream completes the post-command pull before the stream closes.

Module backends use host capability calls against the authoritative Workspace and therefore require no push/pull round trip.

## Lifecycle differences

`container-shell` provides computerd's retained process log, replay, signals, and disposal.

`isolate-javascript` provides a Workspace-owned execution journal, retained result/events, host cancellation, and explicit disposal. Active Workers cannot be serialized across host restart; orphaned running records are reconciled to failed.

`isolate-shell` intentionally preserves one-call, buffered-result behavior in this release. It does not retain executions for later reattachment or disposal. `timeoutMs` and a concurrent `killExec()` for a caller-supplied execution ID cooperatively abort just-bash at statement boundaries; by the time an ordinary `exec()` promise returns, the command has already settled. Use the Container or JavaScript isolate when detached execution and retained lifecycle are required.

See [16. Execution runtime architecture](./16_code_execution.md) and [17. Isolate JavaScript](./17_isolate_javascript.md).
Loading
Loading