Skip to content

Commit be031a6

Browse files
committed
examples: adapt Codemode to computer runtime
1 parent bcab35d commit be031a6

18 files changed

Lines changed: 15933 additions & 1 deletion

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ jobs:
114114
- name: container
115115
workspace: "@example/computer-container"
116116
path: examples/container
117+
- name: codemode
118+
workspace: "@example/computer-codemode"
119+
path: examples/codemode
117120
steps:
118121
- uses: actions/checkout@v6
119122
with:

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ To see the pieces working together, start with the examples:
6262
- [`examples/worker`](examples/worker) — same HTTP surface as the
6363
container example, but the shell runs in a Dynamic Worker loaded
6464
through `env.LOADER`. No container.
65+
- [`examples/codemode`](examples/codemode) — external `@cloudflare/codemode`
66+
`Executor` backed by a dedicated Computer JavaScript runtime.
6567
- [`examples/think`](examples/think) — an agent that uses the
6668
workspace as its working directory.
6769

docs/17_isolate_javascript.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,4 +165,4 @@ Console output is bounded but currently buffered in the Dynamic Worker and publi
165165

166166
## Trusted integrations
167167

168-
A host can configure additional reserved capability modules through `IsolateJavaScriptBackend.trustedModules`; these modules are fixed when the backend is constructed and cannot be supplied or replaced by caller source.
168+
A host can configure additional reserved capability modules through `IsolateJavaScriptBackend.trustedModules`; these modules are fixed when the backend is constructed and cannot be supplied or replaced by caller source. [`examples/codemode`](../examples/codemode) uses this seam to implement Codemode's external `Executor` contract.

docs/18_runtime_migration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ This change is a breaking preview-API migration. Public execution now uses one r
1111
| `workspace.shell.kill(id, options)` | `workspace.runtime.killExec(id, options)` |
1212
| `workspace.shell.dispose(id, options)` | `workspace.runtime.disposeExec(id, options)` |
1313
| `workspace.code` / script execution | `workspace.runtime.exec(source, { backend: "isolate-javascript", input })` |
14+
| Workspace-core Codemode backend | External `WorkspaceCodemodeExecutor` from `examples/codemode` |
1415

1516
`WorkspaceShell` still exists internally to implement command backends. It is not a public Workspace property.
1617

examples/codemode/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.wrangler/
2+
build/
3+
node_modules/

examples/codemode/README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Codemode on Computer
2+
3+
This example implements Codemode's `Executor` contract on the Computer JavaScript runtime:
4+
5+
```text
6+
@cloudflare/codemode.runCode
7+
→ WorkspaceCodemodeExecutor
8+
→ workspace.runtime
9+
→ codemode-javascript
10+
→ ws:codemode-adapter
11+
→ execution-scoped providers and connectors
12+
```
13+
14+
Codemode stays outside `@cloudflare/computer`. The example installs one private trusted module on a dedicated `IsolateJavaScriptBackend`; general-purpose JavaScript executions never receive that authority. The adapter is reference code rather than a compatibility-stable package.
15+
16+
## Trust model
17+
18+
Each execution receives an unguessable token that selects only its supplied providers and connectors. The dispatcher revokes that token before disposing the retained runtime execution. Caller code cannot choose a Workspace, backend, trusted-module implementation, or provider set.
19+
20+
Keep the dedicated backend out of untrusted backend allowlists. Trusted provider operations must honor their own deadlines and idempotency requirements: revocation blocks new calls but cannot roll back or forcibly stop a call the host already admitted.
21+
22+
The adapter transports finite, acyclic values, including typed binary values and `undefined`, through tagged envelopes. It rejects malformed envelopes, unsupported values, inherited tool names, namespace collisions, and cyclic provider results.
23+
24+
> [!WARNING]
25+
> The HTTP endpoint is a local harness, not a public API. It intentionally has no authentication. A production service must derive Workspace identity from an authenticated tenant, authorize every operation, and enforce request, execution, and provider quotas.
26+
27+
## Run locally
28+
29+
From the repository root:
30+
31+
```bash
32+
npm install
33+
npm run dev --workspace @example/computer-codemode
34+
```
35+
36+
In another terminal:
37+
38+
```bash
39+
curl -X POST http://127.0.0.1:8787/run
40+
```
41+
42+
Each call runs deterministic Codemode-generated JavaScript, invokes a host provider, and updates `/workspace/codemode.txt` through durable `node:fs/promises`.
43+
44+
## Validate
45+
46+
```bash
47+
npm run typecheck --workspace @example/computer-codemode
48+
npm test --workspace @example/computer-codemode
49+
```
50+
51+
The tests cover provider and connector dispatch, execution-token isolation and cleanup, namespace safety, connector controls, provider preludes, malformed and cyclic values, binary and `undefined` transport, runtime failures, and retained-execution disposal. The integration suite runs through a real Workerd Worker Loader backend.
52+
53+
If this adapter becomes a supported consumer API, extract it into a separately versioned Computer–Codemode integration package rather than moving Codemode into Computer core.

examples/codemode/package.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "@example/computer-codemode",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"description": "External Codemode Executor backed by the Computer JavaScript runtime.",
7+
"scripts": {
8+
"dev": "wrangler dev",
9+
"test": "vitest run --config vitest.config.ts",
10+
"typecheck": "tsc --noEmit",
11+
"cf-typegen": "wrangler types"
12+
},
13+
"dependencies": {
14+
"@cloudflare/codemode": "^0.5.0",
15+
"@cloudflare/computer": "*"
16+
},
17+
"devDependencies": {
18+
"@cloudflare/vitest-pool-workers": "^0.16.10",
19+
"typescript": "^6.0.3",
20+
"vitest": "^4.1.7",
21+
"wrangler": "^4.107.1"
22+
}
23+
}

examples/codemode/src/index.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { DurableObject } from "cloudflare:workers";
2+
import { type ResolvedProvider, runCode } from "@cloudflare/codemode";
3+
import { type DurableObjectStorageLike, Workspace } from "@cloudflare/computer";
4+
import { IsolateJavaScriptBackend } from "@cloudflare/computer/backends/javascript";
5+
6+
import {
7+
type ExecWorkspaceLike,
8+
WorkspaceCodemodeDispatcher,
9+
WorkspaceCodemodeExecutor,
10+
} from "./workspace-executor.js";
11+
12+
const ROOT = "/workspace";
13+
const EXAMPLE_FILE = `${ROOT}/codemode.txt`;
14+
15+
export class CodemodeExample extends DurableObject<Env> {
16+
readonly #workspace: Workspace;
17+
readonly #executor: WorkspaceCodemodeExecutor;
18+
#ready?: Promise<void>;
19+
#runQueue: Promise<void> = Promise.resolve();
20+
21+
constructor(ctx: DurableObjectState, env: Env) {
22+
super(ctx, env);
23+
const dispatcher = new WorkspaceCodemodeDispatcher();
24+
this.#workspace = new Workspace({
25+
storage: ctx.storage as unknown as DurableObjectStorageLike,
26+
waitUntil: ctx.waitUntil.bind(ctx),
27+
backends: [
28+
new IsolateJavaScriptBackend({
29+
id: "codemode-javascript",
30+
loader: env.LOADER,
31+
root: ROOT,
32+
access: "read-write",
33+
trustedModules: { "ws:codemode-adapter": dispatcher },
34+
}),
35+
],
36+
});
37+
this.#executor = new WorkspaceCodemodeExecutor({
38+
workspace: this.#workspace as unknown as ExecWorkspaceLike,
39+
dispatcher,
40+
});
41+
}
42+
43+
run() {
44+
const run = this.#runQueue.then(
45+
() => this.#runOnce(),
46+
() => this.#runOnce(),
47+
);
48+
this.#runQueue = run.then(
49+
() => undefined,
50+
() => undefined,
51+
);
52+
return run;
53+
}
54+
55+
async #runOnce() {
56+
await this.#ensureReady();
57+
const before = await this.#workspace.fs.readFile(EXAMPLE_FILE, "utf8");
58+
const code = `async () => {
59+
const fs = await import("node:fs/promises");
60+
const before = await fs.readFile(${JSON.stringify(EXAMPLE_FILE)}, "utf8");
61+
const after = String(await demo.next(Number(before)));
62+
await fs.writeFile(${JSON.stringify(EXAMPLE_FILE)}, after);
63+
return { before, after };
64+
}`;
65+
const providers: ResolvedProvider[] = [
66+
{
67+
name: "demo",
68+
fns: { next: async (value) => (Number(value) + 1) % 1_000_000 },
69+
},
70+
];
71+
let result: { result?: unknown; logs?: string[]; error?: string };
72+
try {
73+
result = await runCode({ executor: this.#executor, providers, code });
74+
} catch (error) {
75+
result = { error: error instanceof Error ? error.message : String(error) };
76+
}
77+
const after = await this.#workspace.fs.readFile(EXAMPLE_FILE, "utf8");
78+
return { ok: result.error === undefined, before, after, result };
79+
}
80+
81+
#ensureReady() {
82+
if (this.#ready) return this.#ready;
83+
const ready = (async () => {
84+
await this.#workspace.fs.mkdir(ROOT, { recursive: true });
85+
try {
86+
await this.#workspace.fs.stat(EXAMPLE_FILE);
87+
} catch (error) {
88+
if ((error as { code?: string }).code !== "ENOENT") throw error;
89+
await this.#workspace.fs.writeFile(EXAMPLE_FILE, "0");
90+
}
91+
})();
92+
const guarded = ready.catch((error) => {
93+
if (this.#ready === guarded) this.#ready = undefined;
94+
throw error;
95+
});
96+
this.#ready = guarded;
97+
return guarded;
98+
}
99+
}
100+
101+
interface CodemodeExampleStub {
102+
run(): Promise<{ ok: boolean; [key: string]: unknown }>;
103+
}
104+
105+
export default {
106+
async fetch(request: Request, env: Env) {
107+
const url = new URL(request.url);
108+
if (request.method !== "POST" || url.pathname !== "/run") {
109+
return new Response("POST /run to execute the Codemode example.\n", {
110+
status: 405,
111+
headers: { allow: "POST", "content-type": "text/plain; charset=utf-8" },
112+
});
113+
}
114+
try {
115+
const stub = env.CodemodeExample.get(
116+
env.CodemodeExample.idFromName("example"),
117+
) as unknown as CodemodeExampleStub;
118+
const result = await stub.run();
119+
return Response.json(result, { status: result.ok ? 200 : 422 });
120+
} catch (error) {
121+
return Response.json(
122+
{ error: error instanceof Error ? error.message : String(error) },
123+
{ status: 500 },
124+
);
125+
}
126+
},
127+
} satisfies ExportedHandler<Env>;
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
2+
import type { DurableObjectStorageLike, WorkspaceRuntimeLoader } from "@cloudflare/computer";
3+
import { Workspace } from "@cloudflare/computer";
4+
import { IsolateJavaScriptBackend } from "@cloudflare/computer/backends/javascript";
5+
6+
import {
7+
type ExecWorkspaceLike,
8+
WorkspaceCodemodeDispatcher,
9+
WorkspaceCodemodeExecutor,
10+
} from "./workspace-executor.js";
11+
12+
interface Env {
13+
LOADER: WorkerLoader;
14+
TestHost: DurableObjectNamespace<TestHost>;
15+
}
16+
17+
export class TestHost extends DurableObject<Env> {
18+
readonly #dispatcher = new WorkspaceCodemodeDispatcher();
19+
readonly #executor: WorkspaceCodemodeExecutor;
20+
21+
constructor(ctx: DurableObjectState, env: Env) {
22+
super(ctx, env);
23+
const workspace = new Workspace({
24+
storage: ctx.storage as unknown as DurableObjectStorageLike,
25+
waitUntil: ctx.waitUntil.bind(ctx),
26+
backends: [
27+
new IsolateJavaScriptBackend({
28+
id: "codemode-javascript",
29+
loader: env.LOADER as unknown as WorkspaceRuntimeLoader,
30+
trustedModules: { "ws:codemode-adapter": this.#dispatcher },
31+
}),
32+
],
33+
});
34+
this.#executor = new WorkspaceCodemodeExecutor({
35+
workspace: workspace as unknown as ExecWorkspaceLike,
36+
dispatcher: this.#dispatcher,
37+
});
38+
}
39+
40+
run(mode: "success" | "pause" | "error" | "unsupported") {
41+
const code =
42+
mode === "success"
43+
? `async () => {
44+
const bytes = await tools.bytes(new globalThis.Uint8Array([1, 2]));
45+
const binary = await tools.binary([new globalThis.ArrayBuffer(4), new globalThis.Int16Array([1, 2])]);
46+
const remote = await connector.lookup({ id: 7 });
47+
return {
48+
bytes: globalThis.Array.from(bytes),
49+
binary: [binary[0] instanceof globalThis.ArrayBuffer, binary[1] instanceof globalThis.Int16Array, globalThis.Array.from(binary[1])],
50+
remote,
51+
local: tools.local(),
52+
shadowedGlobal: await Uint8Array.echo(7),
53+
isUndefined: (await tools.undefinedValue()) === undefined
54+
};
55+
}`
56+
: mode === "unsupported"
57+
? `async () => tools.echo(new Date())`
58+
: `async () => connector.${mode}({})`;
59+
return this.#executor.execute(
60+
code,
61+
[
62+
{
63+
name: "tools",
64+
fns: {
65+
echo: async (value) => value,
66+
undefinedValue: async () => undefined,
67+
bytes: async (value) => {
68+
if (!(value instanceof Uint8Array)) throw new Error("expected bytes");
69+
return value.map((byte) => byte + 1);
70+
},
71+
binary: async (value) => {
72+
if (
73+
!Array.isArray(value) ||
74+
!(value[0] instanceof ArrayBuffer) ||
75+
!(value[1] instanceof Int16Array)
76+
)
77+
throw new Error("expected binary types");
78+
return value;
79+
},
80+
},
81+
prelude: "tools.local = () => 'prelude';",
82+
},
83+
{
84+
name: "Uint8Array",
85+
fns: { echo: async (value) => value },
86+
},
87+
],
88+
{
89+
connectors: [
90+
{
91+
name: "connector",
92+
binding: {
93+
async callTool(method, args) {
94+
if (method === "pause") return { __codemode_control__: "pause" };
95+
if (method === "error") {
96+
return { __codemode_control__: "error", message: "connector failed" };
97+
}
98+
return { method, args };
99+
},
100+
},
101+
},
102+
],
103+
},
104+
);
105+
}
106+
}
107+
108+
export default class extends WorkerEntrypoint<Env> {
109+
override async fetch(request: Request) {
110+
const mode = new URL(request.url).pathname.slice(1) as
111+
| "success"
112+
| "pause"
113+
| "error"
114+
| "unsupported";
115+
const host = this.env.TestHost.get(this.env.TestHost.idFromName("test"));
116+
return Response.json(await host.run(mode));
117+
}
118+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { SELF } from "cloudflare:test";
2+
import { describe, expect, it } from "vitest";
3+
4+
describe("WorkspaceCodemodeExecutor integration", () => {
5+
it("executes providers, bytes, connectors, and preludes through the JavaScript backend", async () => {
6+
const response = await SELF.fetch("https://example.test/success");
7+
expect(response.status).toBe(200);
8+
expect(await response.json()).toEqual({
9+
result: {
10+
bytes: [2, 3],
11+
binary: [true, true, [1, 2]],
12+
remote: { method: "lookup", args: { id: 7 } },
13+
local: "prelude",
14+
shadowedGlobal: 7,
15+
isUndefined: true,
16+
},
17+
logs: [],
18+
});
19+
});
20+
21+
it("propagates connector pause and error controls", async () => {
22+
const pause = await SELF.fetch("https://example.test/pause");
23+
expect(await pause.json()).toMatchObject({
24+
error: expect.stringContaining("__CODEMODE_PAUSE__"),
25+
});
26+
const error = await SELF.fetch("https://example.test/error");
27+
expect(await error.json()).toMatchObject({
28+
error: expect.stringContaining("connector failed"),
29+
});
30+
31+
const unsupported = await SELF.fetch("https://example.test/unsupported");
32+
expect(await unsupported.json()).toMatchObject({
33+
error: expect.stringContaining("plain JSON objects"),
34+
});
35+
});
36+
});

0 commit comments

Comments
 (0)