Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ jobs:
- name: container
workspace: "@example/computer-container"
path: examples/container
- name: codemode
workspace: "@example/computer-codemode"
path: examples/codemode
steps:
- uses: actions/checkout@v6
with:
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ To see the pieces working together, start with the examples:
- [`examples/worker`](examples/worker) — same HTTP surface as the
container example, but the shell runs in a Dynamic Worker loaded
through `env.LOADER`. No container.
- [`examples/codemode`](examples/codemode) — external `@cloudflare/codemode`
`Executor` backed by a dedicated Computer JavaScript runtime.
- [`examples/think`](examples/think) — an agent that uses the
workspace as its working directory.

Expand Down
2 changes: 1 addition & 1 deletion docs/17_isolate_javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,4 @@ Console output is bounded but currently buffered in the Dynamic Worker and publi

## Trusted integrations

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.
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.
1 change: 1 addition & 0 deletions docs/18_runtime_migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This change is a breaking preview-API migration. Public execution now uses one r
| `workspace.shell.kill(id, options)` | `workspace.runtime.killExec(id, options)` |
| `workspace.shell.dispose(id, options)` | `workspace.runtime.disposeExec(id, options)` |
| `workspace.code` / script execution | `workspace.runtime.exec(source, { backend: "isolate-javascript", input })` |
| Workspace-core Codemode backend | External `WorkspaceCodemodeExecutor` from `examples/codemode` |

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

Expand Down
3 changes: 3 additions & 0 deletions examples/codemode/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.wrangler/
build/
node_modules/
53 changes: 53 additions & 0 deletions examples/codemode/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Codemode on Computer

This example implements Codemode's `Executor` contract on the Computer JavaScript runtime:

```text
@cloudflare/codemode.runCode
→ WorkspaceCodemodeExecutor
→ workspace.runtime
→ codemode-javascript
→ ws:codemode-adapter
→ execution-scoped providers and connectors
```

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.

## Trust model

Each execution receives an unguessable token that selects only its supplied providers and connectors. The executor serializes calls, the dispatcher rejects overlapping tokens, and authority is revoked before retained execution cleanup. Caller code cannot choose a Workspace, backend, trusted-module implementation, or provider set.

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.

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.

> [!WARNING]
> 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.

## Run locally

From the repository root:

```bash
npm install
npm run dev --workspace @example/computer-codemode
```

In another terminal:

```bash
curl -X POST http://127.0.0.1:8787/run
```

Each call runs deterministic Codemode-generated JavaScript, invokes a host provider, and updates `/workspace/codemode.txt` through durable `node:fs/promises`.

## Validate

```bash
npm run typecheck --workspace @example/computer-codemode
npm test --workspace @example/computer-codemode
```

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.

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.
23 changes: 23 additions & 0 deletions examples/codemode/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@example/computer-codemode",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "External Codemode Executor backed by the Computer JavaScript runtime.",
"scripts": {
"dev": "wrangler dev",
"test": "vitest run --config vitest.config.ts",
"typecheck": "tsc --noEmit",
"cf-typegen": "wrangler types"
},
"dependencies": {
"@cloudflare/codemode": "^0.5.0",
"@cloudflare/computer": "*"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.16.10",
"typescript": "^6.0.3",
"vitest": "^4.1.7",
"wrangler": "^4.107.1"
}
}
113 changes: 113 additions & 0 deletions examples/codemode/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { DurableObject } from "cloudflare:workers";
import { type ResolvedProvider, runCode } from "@cloudflare/codemode";
import { type DurableObjectStorageLike, Workspace } from "@cloudflare/computer";
import { IsolateJavaScriptBackend } from "@cloudflare/computer/backends/javascript";

import {
type ExecWorkspaceLike,
WorkspaceCodemodeDispatcher,
WorkspaceCodemodeExecutor,
} from "./workspace-executor.js";

const ROOT = "/workspace";
const EXAMPLE_FILE = `${ROOT}/codemode.txt`;

export class CodemodeExample extends DurableObject<Env> {
readonly #workspace: Workspace;
readonly #executor: WorkspaceCodemodeExecutor;
#ready?: Promise<void>;

constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
const dispatcher = new WorkspaceCodemodeDispatcher();
this.#workspace = new Workspace({
storage: ctx.storage as unknown as DurableObjectStorageLike,
waitUntil: ctx.waitUntil.bind(ctx),
backends: [
new IsolateJavaScriptBackend({
id: "codemode-javascript",
loader: env.LOADER,
root: ROOT,
access: "read-write",
trustedModules: { "ws:codemode-adapter": dispatcher },
}),
],
});
this.#executor = new WorkspaceCodemodeExecutor({
workspace: this.#workspace as unknown as ExecWorkspaceLike,
dispatcher,
});
}

async run() {
await this.#ensureReady();
const code = `async () => {
const fs = await import("node:fs/promises");
const before = await fs.readFile(${JSON.stringify(EXAMPLE_FILE)}, "utf8");
const after = String(await demo.next(Number(before)));
await fs.writeFile(${JSON.stringify(EXAMPLE_FILE)}, after);
return { before, after };
}`;
const providers: ResolvedProvider[] = [
{
name: "demo",
fns: { next: async (value) => (Number(value) + 1) % 1_000_000 },
},
];
let result: { result?: unknown; logs?: string[]; error?: string };
try {
result = await runCode({ executor: this.#executor, providers, code });
} catch (error) {
result = { error: error instanceof Error ? error.message : String(error) };
}
const file = await this.#workspace.fs.readFile(EXAMPLE_FILE, "utf8");
return { ok: result.error === undefined, file, result };
}

#ensureReady() {
if (this.#ready) return this.#ready;
const ready = (async () => {
await this.#workspace.fs.mkdir(ROOT, { recursive: true });
try {
await this.#workspace.fs.stat(EXAMPLE_FILE);
} catch (error) {
if ((error as { code?: string }).code !== "ENOENT") throw error;
await this.#workspace.fs.writeFile(EXAMPLE_FILE, "0");
}
})();
const guarded = ready.catch((error) => {
if (this.#ready === guarded) this.#ready = undefined;
throw error;
});
this.#ready = guarded;
return guarded;
}
}

interface CodemodeExampleStub {
run(): Promise<{ ok: boolean; [key: string]: unknown }>;
}

export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
if (request.method !== "POST" || url.pathname !== "/run") {
return new Response("POST /run to execute the Codemode example.\n", {
status: 405,
headers: { allow: "POST", "content-type": "text/plain; charset=utf-8" },
});
}
try {
const stub = env.CodemodeExample.get(
env.CodemodeExample.idFromName("example"),
) as unknown as CodemodeExampleStub;
const result = await stub.run();
return Response.json(result, { status: result.ok ? 200 : 422 });
} catch (error) {
return Response.json(
{ error: error instanceof Error ? error.message : String(error) },
{ status: 500 },
);
}
},
} satisfies ExportedHandler<Env>;
120 changes: 120 additions & 0 deletions examples/codemode/src/test-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
import type { DurableObjectStorageLike, WorkspaceRuntimeLoader } from "@cloudflare/computer";
import { Workspace } from "@cloudflare/computer";
import { IsolateJavaScriptBackend } from "@cloudflare/computer/backends/javascript";

import exampleHandler, { CodemodeExample } from "./index.js";
import {
type ExecWorkspaceLike,
WorkspaceCodemodeDispatcher,
WorkspaceCodemodeExecutor,
} from "./workspace-executor.js";

export { CodemodeExample };

interface Env {
LOADER: WorkerLoader;
TestHost: DurableObjectNamespace<TestHost>;
CodemodeExample: DurableObjectNamespace<CodemodeExample>;
}

export class TestHost extends DurableObject<Env> {
readonly #dispatcher = new WorkspaceCodemodeDispatcher();
readonly #executor: WorkspaceCodemodeExecutor;

constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
const workspace = new Workspace({
storage: ctx.storage as unknown as DurableObjectStorageLike,
waitUntil: ctx.waitUntil.bind(ctx),
backends: [
new IsolateJavaScriptBackend({
id: "codemode-javascript",
loader: env.LOADER as unknown as WorkspaceRuntimeLoader,
trustedModules: { "ws:codemode-adapter": this.#dispatcher },
}),
],
});
this.#executor = new WorkspaceCodemodeExecutor({
workspace: workspace as unknown as ExecWorkspaceLike,
dispatcher: this.#dispatcher,
});
}

run(mode: "success" | "pause" | "error" | "unsupported") {
const code =
mode === "success"
? `async () => {
const bytes = await tools.bytes(new globalThis.Uint8Array([1, 2]));
const binary = await tools.binary([new globalThis.ArrayBuffer(4), new globalThis.Int16Array([1, 2])]);
const remote = await connector.lookup({ id: 7 });
return {
bytes: globalThis.Array.from(bytes),
binary: [binary[0] instanceof globalThis.ArrayBuffer, binary[1] instanceof globalThis.Int16Array, globalThis.Array.from(binary[1])],
remote,
local: tools.local(),
shadowedGlobal: await Uint8Array.echo(7),
isUndefined: (await tools.undefinedValue()) === undefined
};
}`
: mode === "unsupported"
? `async () => tools.echo(new Date())`
: `async () => connector.${mode}({})`;
return this.#executor.execute(
code,
[
{
name: "tools",
fns: {
echo: async (value) => value,
undefinedValue: async () => undefined,
bytes: async (value) => {
if (!(value instanceof Uint8Array)) throw new Error("expected bytes");
return value.map((byte) => byte + 1);
},
binary: async (value) => {
if (
!Array.isArray(value) ||
!(value[0] instanceof ArrayBuffer) ||
!(value[1] instanceof Int16Array)
)
throw new Error("expected binary types");
return value;
},
},
prelude: "tools.local = () => 'prelude';",
},
{
name: "Uint8Array",
fns: { echo: async (value) => value },
},
],
{
connectors: [
{
name: "connector",
binding: {
async callTool(method, args) {
if (method === "pause") return { __codemode_control__: "pause" };
if (method === "error") {
return { __codemode_control__: "error", message: "connector failed" };
}
return { method, args };
},
},
},
],
},
);
}
}

export default class extends WorkerEntrypoint<Env> {
override async fetch(request: Request) {
const pathname = new URL(request.url).pathname;
if (pathname === "/run") return exampleHandler.fetch(request, this.env);
const mode = pathname.slice(1) as "success" | "pause" | "error" | "unsupported";
const host = this.env.TestHost.get(this.env.TestHost.idFromName("test"));
return Response.json(await host.run(mode));
}
}
Loading
Loading