Skip to content

Commit 6f9fb50

Browse files
committed
computer, docs: Route execution through runtime
Replace the public shell execution surface with workspace.runtime and route command execution through stable backend IDs. The existing container and worker command backends keep the push/pull bracket while sharing one client, stub, and observation path. Document the runtime surface and update the examples to call workspace.runtime instead of workspace.shell.
1 parent ca5bf52 commit 6f9fb50

66 files changed

Lines changed: 2661 additions & 2273 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,21 @@
22

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

88
- **Container** projects the SQLite state into a sandbox container as
99
a real FUSE mount. A sandbox-side daemon (`computerd`) mounts the state
1010
as a filesystem and syncs changes back over a capnweb RPC channel.
1111
Full Linux userland, real binaries, real network.
12-
- **Worker** runs the shell as [just-bash](https://github.com/vercel-labs/just-bash)
13-
inside a Dynamic Worker loaded through `env.LOADER`. The shell
14-
reaches the host workspace over Workers RPC, so there is no second
15-
store and no sync round trip. Broad textual tooling (`cat`,
16-
`grep`, `awk`, `sed`, `jq`, ...), no container lifecycle.
17-
18-
A single Workspace can host more than one backend at the same
19-
time. Each backend registers under a stable `id`; `shell.exec`
20-
defaults to the first one in the list and the caller routes a
21-
specific call elsewhere with `{ backend: "sandbox" }`. Common
22-
shape: a Worker backend for cheap textual tooling that runs every
23-
command cold, plus a Container backend the agent reaches for when
24-
it needs a real Linux environment for `npm`, `git`, or anything
25-
else the worker isolate can't host. Each backend keeps its own
26-
sync cursors and connects lazily on first use.
12+
- **Isolate shell** runs [just-bash](https://github.com/vercel-labs/just-bash)
13+
in a Dynamic Worker. It reaches the authoritative Workspace over
14+
Workers RPC, so there is no second store or sync round trip.
15+
A Workspace may register multiple backends under stable IDs.
16+
`workspace.runtime.exec(source, { backend })` is the single execution
17+
entry point; the selected backend defines how to interpret `source`.
18+
The shipped backends treat it as a shell command. Backends connect lazily on
19+
first use.
2720

2821
Workspace can also be constructed without a backend at all, giving
2922
callers the filesystem on its own.

docs/01_vfs.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,10 @@ via FUSE) create it like any other directory.
6464

6565
## Conventions
6666

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

@@ -135,7 +135,7 @@ pinned `DISABLE_FUSE=1`, which produced a degraded mode where:
135135
directly; synchronization with the DO-side VFS happens over RPC
136136
through the post-exec pull bracket and explicit
137137
`workspace.push()` / `workspace.pull()` calls.
138-
- Writes performed by `shell.exec` are picked up by the post-exec
138+
- Writes performed by `runtime.exec` are picked up by the post-exec
139139
pull. `workspace.push()` flushes pending DO-side writes to the
140140
container without waiting for the next `exec()`. Use these when
141141
you need to synchronize the two sides outside of a command run.

docs/02_sync_protocol.md

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -294,14 +294,14 @@ edited file) shows up exactly once on the wire. See
294294
- **Concurrent mutators.** `Workspace.push()` and `Workspace.pull()`
295295
go through a per-Workspace tail-promise FIFO. Two concurrent
296296
callers queue — the second can't enter `pushOnce` / `pullOnce`
297-
until the first has resolved or rejected. The shell exec bracket
298-
drives `push()` / `pull()` through the same facade, so
299-
`shell.exec()` calls participate in the FIFO automatically.
300-
Rejections aren't contagious: a failed mutation surfaces its
301-
error to its own caller without poisoning the queue for the next.
302-
Pure reads on `Workspace.fs` bypass the FIFO entirely — they hit
303-
the local SQLite store, which the DO runtime already serialises
304-
internally through its input gates.
297+
until the first has resolved or rejected. A command's pre-exec push
298+
and post-stream pull each use this facade, but the FIFO is not held
299+
for the command's lifetime: overlapping commands and explicit sync
300+
calls are not one transaction. Rejections aren't contagious: a
301+
failed mutation surfaces its error to its own caller without
302+
poisoning the queue for the next. Pure reads on `Workspace.fs`
303+
bypass the FIFO entirely — they hit the local SQLite store, which
304+
the DO runtime already serialises internally through its input gates.
305305

306306
## Conflict semantics
307307

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

328329
### Across two containers sharing one Workspace
329330

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

docs/04_filesystem_interface.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ file could be large.
1515
```ts
1616
interface Workspace {
1717
fs: WorkspaceFilesystem;
18-
shell: WorkspaceShell; // see 05_shell_interface.md
18+
runtime: WorkspaceRuntime; // see 05_runtime_interface.md
1919
}
2020
```
2121

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

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

238238
```ts
@@ -261,7 +261,7 @@ for (const hit of hits) {
261261
}
262262
```
263263

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

267267
## Error handling

docs/05_runtime_interface.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# 05. Runtime interface
2+
3+
Workspace exposes one execution router:
4+
5+
```ts
6+
const handle = await workspace.runtime.exec(source, {
7+
backend: "container-shell",
8+
cwd: "/workspace",
9+
encoding: "utf8",
10+
});
11+
12+
const result = await handle.result();
13+
```
14+
15+
The backend ID defines how `source` is interpreted. The shipped
16+
container and worker backends treat it as shell syntax. Module backends
17+
can use the same surface for structured code execution.
18+
19+
## API
20+
21+
```ts
22+
interface WorkspaceRuntime {
23+
exec(source: string, options?: WorkspaceRuntimeExecOptions): Promise<WorkspaceRuntimeExecHandle>;
24+
getExec(id: string, options?: WorkspaceRuntimeGetOptions): Promise<WorkspaceRuntimeExecHandle>;
25+
killExec(id: string, options?: WorkspaceRuntimeKillOptions): Promise<void>;
26+
disposeExec(id: string, options?: WorkspaceRuntimeDisposeOptions): Promise<void>;
27+
}
28+
29+
interface WorkspaceRuntimeExecOptions {
30+
id?: string;
31+
backend?: string;
32+
cwd?: string;
33+
encoding?: "utf8";
34+
input?: WorkspaceRuntimeValue;
35+
timeoutMs?: number;
36+
}
37+
38+
interface WorkspaceRuntimeExecHandle extends ReadableStream<WorkspaceRuntimeEvent> {
39+
readonly id: string;
40+
readonly backend: string;
41+
result(): Promise<WorkspaceRuntimeResult>;
42+
kill(signal?: KillSignal): Promise<void>;
43+
[Symbol.dispose](): void;
44+
}
45+
```
46+
47+
`input` is accepted by structured module backends and rejected by command
48+
backends. `cwd` is the command working directory or, for module backends,
49+
the base path for backend-specific resolution. A handle is single-consumer:
50+
call `result()` or consume its event stream, not both. Repeated `result()`
51+
calls return the same promise. `backend` records the resolved backend needed
52+
for later reattachment.
53+
54+
## Results
55+
56+
```ts
57+
interface WorkspaceRuntimeResult {
58+
status: "completed" | "failed" | "cancelled";
59+
exitCode: number;
60+
stdout: Uint8Array | string;
61+
stderr: Uint8Array | string;
62+
value?: WorkspaceRuntimeValue;
63+
pushed: number;
64+
pulled: number;
65+
skipped: SkippedEntry[];
66+
sync:
67+
| { status: "complete"; applied: number; skipped: SkippedEntry[] }
68+
| { status: "pending"; applied: number; skipped: SkippedEntry[]; error: string };
69+
}
70+
```
71+
72+
Command backends leave `value` unset. Module backends can use `value` for a
73+
structured return value. A command can complete while its post-command pull
74+
fails; in that case `sync.status` is `"pending"`, and a configured
75+
`SyncRetryScheduler` can durably retry the pull without rerunning the
76+
command.
77+
78+
## Backend routing
79+
80+
```ts
81+
await workspace.runtime.exec("grep -R TODO .", {
82+
backend: "isolate-shell",
83+
});
84+
85+
await workspace.runtime.exec("npm test", {
86+
backend: "container-shell",
87+
});
88+
```
89+
90+
Omitting `backend` selects the first configured backend. Backend selection is
91+
routing, not authorization; public gateways must validate it against
92+
server-side policy.
93+
94+
## Command synchronization
95+
96+
Command backends continue to use the existing synchronization bracket:
97+
98+
```text
99+
push → spawn → events/result → pull
100+
```
101+
102+
A backend with `sync: "none"`, such as `isolate-shell`, shares the host
103+
store and reports zero push/pull counts. A container has its own VFS and
104+
synchronizes changes before and after command execution. Fully draining
105+
either `result()` or the event stream completes the post-command pull before
106+
the stream closes.
107+
108+
Module backends use host capability calls against the authoritative
109+
Workspace and therefore require no push/pull round trip.
110+
111+
## Lifecycle differences
112+
113+
`container-shell` provides computerd's retained process log, replay,
114+
signals, and disposal.
115+
116+
`isolate-shell` intentionally preserves one-call, buffered-result behavior
117+
in this release. It does not retain executions for later reattachment or
118+
disposal. `timeoutMs` and a concurrent `killExec()` for a caller-supplied
119+
execution ID cooperatively abort just-bash at statement boundaries; by the
120+
time an ordinary `exec()` promise returns, the command has already settled.
121+
Use the container backend when detached execution and retained lifecycle are
122+
required.

0 commit comments

Comments
 (0)