Skip to content

Commit f1cf132

Browse files
aron-cfscuffi
authored andcommitted
computer, docs: Add isolate JavaScript backend
Add a module backend that runs ECMAScript sources in Dynamic Workers with structured input, retained execution events, durable filesystem access, and host-owned git and artifact capabilities. Expose the backend on its own package subpath and cover it with unit and workerd integration tests.
1 parent 7af28bc commit f1cf132

28 files changed

Lines changed: 4379 additions & 49 deletions

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Cloudflare Computer is a virtual filesystem that lives inside a
44
Durable Object. The Durable Object holds the authoritative state in
55
SQLite and exposes one pluggable execution surface through
6-
`workspace.runtime`. Two backends ship today:
6+
`workspace.runtime`. Three 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
@@ -12,11 +12,15 @@ SQLite and exposes one pluggable execution surface through
1212
- **Isolate shell** runs [just-bash](https://github.com/vercel-labs/just-bash)
1313
in a Dynamic Worker. It reaches the authoritative Workspace over
1414
Workers RPC, so there is no second store or sync round trip.
15+
- **Isolate JavaScript** runs an ECMAScript module in a fresh Dynamic
16+
Worker with structured input/results, durable relative imports,
17+
configured libraries, Workspace-backed `node:fs/promises`, and trusted `ws:git` and
18+
`ws:artifacts` modules.
19+
1520
A Workspace may register multiple backends under stable IDs.
1621
`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.
22+
entry point; the selected backend defines whether `source` is a shell
23+
command or an ECMAScript module. Backends connect lazily on first use.
2024

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

docs/05_runtime_interface.md

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# 05. Runtime interface
1+
# 05. Runtime Interface
22

33
Workspace exposes one execution router:
44

@@ -12,9 +12,7 @@ const handle = await workspace.runtime.exec(source, {
1212
const result = await handle.result();
1313
```
1414

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.
15+
The backend ID defines how `source` is interpreted. Command runtimes accept shell syntax; module runtimes accept their documented programming language.
1816

1917
## API
2018

@@ -44,12 +42,7 @@ interface WorkspaceRuntimeExecHandle extends ReadableStream<WorkspaceRuntimeEven
4442
}
4543
```
4644

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.
45+
`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.
5346

5447
## Results
5548

@@ -69,11 +62,7 @@ interface WorkspaceRuntimeResult {
6962
}
7063
```
7164

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.
65+
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.
7766

7867
## Backend routing
7968

@@ -85,11 +74,17 @@ await workspace.runtime.exec("grep -R TODO .", {
8574
await workspace.runtime.exec("npm test", {
8675
backend: "container-shell",
8776
});
77+
78+
await workspace.runtime.exec(
79+
`
80+
import fs from "node:fs/promises";
81+
export default async () => fs.readFile("/workspace/package.json", "utf8");
82+
`,
83+
{ backend: "isolate-javascript" },
84+
);
8885
```
8986

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.
87+
Omitting `backend` selects the first configured backend. Backend selection is routing, not authorization; public gateways must validate it against server-side policy.
9388

9489
## Command synchronization
9590

@@ -99,24 +94,16 @@ Command backends continue to use the existing synchronization bracket:
9994
push → spawn → events/result → pull
10095
```
10196

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.
97+
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.
10798

108-
Module backends use host capability calls against the authoritative
109-
Workspace and therefore require no push/pull round trip.
99+
Module backends use host capability calls against the authoritative Workspace and therefore require no push/pull round trip.
110100

111101
## Lifecycle differences
112102

113-
`container-shell` provides computerd's retained process log, replay,
114-
signals, and disposal.
103+
`container-shell` provides computerd's retained process log, replay, signals, and disposal.
104+
105+
`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.
106+
107+
`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.
115108

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.
109+
See [16. Execution runtime architecture](./16_code_execution.md) and [17. Isolate JavaScript](./17_isolate_javascript.md).

docs/10_project_layout.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,13 @@ packages/computer/
5353
├── src/
5454
│ ├── index.ts # Public entrypoint
5555
│ ├── workspace.ts # Workspace facade
56-
│ ├── runtime/ # Public runtime router
56+
│ ├── runtime/ # Public runtime router and capabilities
5757
│ ├── shell.ts # Internal command-backend adapter
5858
│ ├── backend.ts # Command backend interface
5959
│ ├── backends/
6060
│ │ ├── container/ # Cloudflare Container + computerd backend
6161
│ │ ├── worker/ # Dynamic Worker + just-bash backend
62+
│ │ ├── javascript/ # Dynamic Worker ECMAScript backend
6263
│ │ └── test.ts # In-process test backend
6364
│ ├── proxy.ts # WorkspaceProxy
6465
│ ├── proxy-stub.ts # Client-side stub plumbing

docs/12_worker_backend.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ The worker backend trades the real environment for a Workers
3131
isolate that boots instantly, scales out cheaply, and has no
3232
container lifecycle. The shell is the just-bash interpreter; the
3333
supported command set is broad (`cat`, `grep`, `awk`, `sed`, `jq`,
34-
`sort`) but not the full Linux userland. just-bash's Node-only language
35-
commands are disabled on workerd. Filesystem operations forward into the same
34+
`sort`) but not the full Linux userland. JavaScript modules run through the
35+
[`isolate-javascript` backend](./17_isolate_javascript.md), not through just-bash's
36+
Node-only language commands. Filesystem operations forward into the same
3637
SQLite store as the container backend, so the storage shape, mount
3738
rules, and read-only enforcement are unchanged.
3839

docs/16_code_execution.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Workspace execution runtimes
2+
3+
Workspace exposes one execution namespace:
4+
5+
```ts
6+
const handle = await workspace.runtime.exec(source, {
7+
backend: "container-shell",
8+
cwd: "/workspace",
9+
encoding: "utf8",
10+
});
11+
const result = await handle.result();
12+
```
13+
14+
The selected backend defines how it interprets `source`.
15+
16+
| Backend | Source language | Intended use |
17+
| --- | --- | --- |
18+
| `container-shell` | shell command | Full Linux, native binaries, installed packages, processes |
19+
| `isolate-shell` | just-bash command | Fast text tools and Workspace Git without a Container |
20+
| `isolate-javascript` | ECMAScript module | Isolated structured JavaScript with trusted Workspace modules |
21+
22+
Applications may register additional command or module backends under their own IDs. Backend IDs are part of the execution contract: changing the backend may change the source language.
23+
24+
## Lifecycle
25+
26+
```ts
27+
const handle = await workspace.runtime.exec(source, {
28+
id: "build-1",
29+
backend: "isolate-javascript",
30+
});
31+
32+
handle.id;
33+
await handle.kill();
34+
35+
const resumed = await workspace.runtime.getExec("build-1", {
36+
backend: "isolate-javascript",
37+
resume: "full",
38+
});
39+
40+
await workspace.runtime.disposeExec("build-1", {
41+
backend: "isolate-javascript",
42+
});
43+
```
44+
45+
The common result contains process-compatible output and an optional structured value:
46+
47+
```ts
48+
interface WorkspaceRuntimeResult {
49+
status: "completed" | "failed" | "cancelled";
50+
exitCode: number;
51+
stdout: Uint8Array | string;
52+
stderr: Uint8Array | string;
53+
value?: WorkspaceRuntimeValue;
54+
pushed: number;
55+
pulled: number;
56+
skipped: SkippedEntry[];
57+
}
58+
```
59+
60+
Command backends leave `value` unset. Module backends use it for their structured return value.
61+
62+
`container-shell` retains the existing computerd process lifecycle. `isolate-javascript` keeps an execution journal in the Workspace database and retains events/results until `disposeExec`. Active isolate cancellation is host-driven by disposing the child Worker. An execution left running across a Workspace host restart is reconciled to failed because a live Worker capability cannot be serialized into SQLite.
63+
64+
`isolate-shell` intentionally retains its existing behavior in this release: it buffers a just-bash call to completion, does not retain cross-request events, and cannot reattach by ID. Callers that require supervised process behavior should use `container-shell`; callers that require a managed isolate should use `isolate-javascript`.
65+
66+
## Backend authority
67+
68+
There is no general `workspace.scope()` abstraction. Backend construction fixes maximum authority and module availability. A public gateway must validate which backend a signed capability is allowed to select.
69+
70+
For different authority levels, configure distinct backend instances:
71+
72+
```ts
73+
new IsolateJavaScriptBackend({
74+
id: "isolate-javascript-readonly",
75+
loader: env.LOADER,
76+
access: "read",
77+
});
78+
79+
new IsolateJavaScriptBackend({
80+
id: "isolate-javascript",
81+
loader: env.LOADER,
82+
access: "read-write",
83+
});
84+
```
85+
86+
The backend argument is never itself authorization.
87+
88+
See [17. Isolate JavaScript](./17_isolate_javascript.md) for module and trusted-package behavior.

0 commit comments

Comments
 (0)