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
5 changes: 5 additions & 0 deletions .changeset/codemode-cli-binary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/computerd": minor
---

Ships a second binary, codemode, next to computerd in the release artifacts and the computer-computerd-linux-x64 image. Run inside the container, it sends a script to the workspace's host and prints the result, with subcommands to print the host's TypeScript declarations, search and describe one method at a time, and list what a paused run is waiting on. It dials ws://computer.internal/codemode by default and needs no credentials.
5 changes: 5 additions & 0 deletions .changeset/codemode-container-option.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/computer": minor
---

CloudflareContainerBackend takes a codemode option. When set, a command inside the container can run codemode < script.js: the script reaches the Durable Object through the existing egress interception at /codemode and runs in a dynamic worker through @cloudflare/codemode, with the connectors you list as typed globals. WorkspaceProxy forwards /codemode to the Durable Object the way it forwards /api. @cloudflare/codemode is a new optional peer dependency, needed only when the option is used.
5 changes: 5 additions & 0 deletions .changeset/codemode-rpc-interface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/computer-rpc": minor
---

Adds the CodemodeRPC interface, the code surface a process inside the container reaches by opening a WebSocket to the host's egress endpoint at /codemode. It carries types, search, and describe for discovering the globals a script may call, execute, which runs a script body and reports completed, paused, or error rather than rejecting, and pending, which lists what a paused run is waiting on. Approval is deliberately not on the surface.
10 changes: 5 additions & 5 deletions .github/changeset-publish.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ function imageExists(tag) {

function stageComputerdBinary() {
mkdirSync("packages/computer-computerd-linux-x64/bin", { recursive: true });
copyFileSync(
"artifacts/computerd/computerd-linux-x64",
"packages/computer-computerd-linux-x64/bin/computerd",
);
chmodSync("packages/computer-computerd-linux-x64/bin/computerd", 0o755);
for (const entry of ["computerd", "codemode"]) {
const staged = `packages/computer-computerd-linux-x64/bin/${entry}`;
copyFileSync(`artifacts/computerd/${entry}-linux-x64`, staged);
chmodSync(staged, 0o755);
}
}

const { version } = readJson("packages/computerd/package.json");
Expand Down
9 changes: 6 additions & 3 deletions .github/workflows/next-computerd-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,18 @@ jobs:
- name: Build all workspaces
run: npm run build --workspaces --if-present

- name: Build the computerd binary
- name: Build the computerd and codemode binaries
run: npm run build:bin --workspace @cloudflare/computerd

- name: Stage the computerd binary
- name: Stage the computerd and codemode binaries
run: |
mkdir -p packages/computer-computerd-linux-x64/bin
cp artifacts/computerd/computerd-linux-x64 \
packages/computer-computerd-linux-x64/bin/computerd
chmod 755 packages/computer-computerd-linux-x64/bin/computerd
cp artifacts/computerd/codemode-linux-x64 \
packages/computer-computerd-linux-x64/bin/codemode
chmod 755 packages/computer-computerd-linux-x64/bin/computerd \
packages/computer-computerd-linux-x64/bin/codemode

- name: Set up buildx
uses: docker/setup-buildx-action@v3
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ public surface. Each is a Worker workspace with its own README.
- [`examples/egress`](examples/egress) — sends one URL through the container,
Worker shell, and Worker JavaScript backends with matching `none`, `all`, or
custom egress policies.
- [`examples/codemode`](examples/codemode) — a container whose commands
can run `codemode < script.js`: the script runs on the Durable Object
in a dynamic worker, with the app's connectors as typed globals.
- [`examples/mcp`](examples/mcp) — a Computer MCP example:
one Code Mode `code` tool backed by a durable workspace, a Worker shell,
and a full Linux container.
Expand Down
26 changes: 23 additions & 3 deletions docs/07_injected_service.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@ The "injected service" is the workspace daemon that runs *inside* the
sandbox container. It owns the FUSE mount, the in-container VFS, the
exec runner, and the capnweb RPC endpoint the DO talks to.

The package ships it as a single self-contained Node SEA binary —
The package ships it as a self-contained Node SEA binary —
**`computerd`** — produced by `packages/computerd/` (npm package
`@cloudflare/computerd`, bin name `computerd`). The binary embeds Node,
the `fuse-native` prebuilds, and `libfuse` as SEA assets, so the host
image does **not** need a Node runtime. Build it with:
image does **not** need a Node runtime. The same build produces a second
binary, **`codemode`**, the client for the host's code surface described
under "HTTP / WS surface". Build both with:

```bash
npm run build:bin --workspace @cloudflare/computerd
# → artifacts/computerd/computerd-linux-x64
# → artifacts/computerd/computerd-macos-x64
# → artifacts/computerd/codemode-linux-x64
# → artifacts/computerd/codemode-macos-x64
```

`examples/container/Dockerfile` is the canonical recipe for
Expand Down Expand Up @@ -56,6 +60,21 @@ backend pins it to `8080`) and serves:
| `/connect` | `POST` | Tells `computerd` to dial *out* to a caller-supplied endpoint and serve a `WorkspaceRPC` session over that outbound WebSocket. Used by the Cloudflare backend (see below). |
| `/` | `GET` | Banner/info page. |

One more route exists for processes inside the container, served by the
host rather than by `computerd`. A request from the container to
`http://computer.internal/codemode` with a WebSocket upgrade reaches the
Durable Object through the same egress interception the daemon dials
back through, and the container backend answers it with a capnweb
session whose bootstrap stub is `CodemodeRPC` (doc 08). The `codemode`
binary that ships next to `computerd` is the client for it: a command
the workspace runs can execute a script on the host with
`codemode < script.js`, print the host's TypeScript declarations with
`codemode types`, and find one method with `codemode search` or
`codemode describe`. Approving a paused run is deliberately not on
this surface. The route answers `404` unless the backend was
constructed with the `codemode` option, and it needs no credential:
reaching it at all means running inside this workspace's container.

`/api` is the workspace surface: the session itself, plus anything that
reads through it. `/__computerd` is daemon introspection, which is why
runtime info sits there and revisions do not.
Expand All @@ -76,7 +95,8 @@ RUN apt-get update \
&& rm -rf /var/lib/apt/lists/*

COPY build/computerd-linux-x64 /usr/local/bin/computerd
RUN chmod +x /usr/local/bin/computerd
COPY build/codemode-linux-x64 /usr/local/bin/codemode
RUN chmod +x /usr/local/bin/computerd /usr/local/bin/codemode

ENV PORT=8080
ENV MOUNT_POINT=/workspace
Expand Down
38 changes: 38 additions & 0 deletions docs/08_capnweb_interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,44 @@ converts to `string` when the caller passes `encoding: "utf8"`. Every
event carries a monotonic `seq` (per exec id) so callers can resume
from a known point after a disconnect.

## `CodemodeRPC`

The interfaces above run over the session the daemon serves. One more
interface runs in the other direction: the host serves it, and a
short-lived command inside the container is the client. A process
that opens a WebSocket to `/codemode` on the egress host gets a
session whose bootstrap stub is:

```ts
interface CodemodeRPC {
types(): Promise<{ types: string; connectors: string[] }>;
search(query: string): Promise<CodemodeSearch>;
describe(target: string): Promise<CodemodeDescription>;
execute(input: { code: string }): Promise<CodemodeResult>;
pending(executionId?: string): Promise<CodemodePendingAction[]>;
}

type CodemodeResult =
| { status: "completed"; executionId: string; result?: unknown; logs?: string[] }
| { status: "paused"; executionId: string; pending: CodemodePendingAction[] }
| { status: "error"; executionId: string; error: string; logs?: string[] };
```

`code` is the body of an async function. The host runs it through a
codemode runtime in a dynamic worker, with the connectors the
container backend was configured with as typed globals; `types` is
the TypeScript declaration of all of them, and `search` and
`describe` are the runtime's own discovery helpers for one method at
a time. `execute` never rejects: a script that throws comes back as
an `error` result, and a run that stops for approval on the host
comes back as `paused` with the actions it waits on, which `pending`
also lists. Approving or rejecting is deliberately absent: a run
pauses because a connector asked for a human's decision, and handing
that decision to the process that wrote the script would make the
gate meaningless. Each connection is its own session and
is disposed when the client closes the socket. The `codemode` binary
in the container image is the reference client.

## Push and fetch semantics

Push and fetch are symmetric. The same `ChangeEntry` shape moves in
Expand Down
2 changes: 2 additions & 0 deletions examples/codemode/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.wrangler/
node_modules/
24 changes: 24 additions & 0 deletions examples/codemode/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Container image for the codemode example.
#
# Both binaries come out of the public GHCR image, a single layer over
# `scratch` holding computerd and codemode at /usr/local/bin. The
# :VERSION tag is rewritten by the changesets Version Packages PR
# through .github/changeset-version.mjs.

FROM ghcr.io/cloudflare/computer-computerd-linux-x64:0.2.1 AS computerd

FROM debian:stable-slim

RUN apt-get update \
&& apt-get install -y --no-install-recommends fuse3 libfuse2t64 ca-certificates \
&& rm -rf /var/lib/apt/lists/*

COPY --from=computerd /usr/local/bin/computerd /usr/local/bin/computerd
COPY --from=computerd /usr/local/bin/codemode /usr/local/bin/codemode

ENV PORT=8080
ENV MOUNT_POINT=/workspace
ENV FUSE_MOUNT=auto
EXPOSE 8080

ENTRYPOINT ["/usr/local/bin/computerd"]
73 changes: 73 additions & 0 deletions examples/codemode/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# codemode example

> [!IMPORTANT]
> **PREVIEW ONLY** This package is provided as a preview for feedback only.
> APIs are unstable and the design is subject to change.

A Worker + Durable Object that boots a container in which any command
can run `codemode < script.js`. The script does not run in the
container: it travels back to the Durable Object and runs there, in a
dynamic worker, with the connectors the Durable Object configured in
scope as typed globals. Here that is one small `notes` connector over
the Durable Object's storage.

```
client ─► Worker POST /c/<name>/exec ─► DO ─► container: codemode < script.js
ws://computer.internal/codemode
DO: codemode runtime ─► dynamic worker
notes.add(), notes.list()
```

## What is where

`src/index.ts` is the Durable Object and a one-route Worker. The only
codemode-specific lines are the `codemode` option on
`CloudflareContainerBackend`, which names the loader, the Durable
Object state, and the connectors, and the two exports the runtime
needs: `WorkspaceProxy`, which carries the container's requests to the
Durable Object, and `CodemodeRuntime`, the facet the runtime keeps its
executions in. `wrangler.jsonc` lists `CodemodeRuntime` as a Durable
Object binding for the same reason.

`src/notes-connector.ts` is the whole connector. Swap it for connectors
over whatever the container should reach: KV, R2, an MCP server through
`McpConnector`, an OpenAPI service through `OpenApiConnector`.

`Dockerfile` copies both `computerd` and `codemode` out of the public
image.

## Run it

```sh
npm run dev --workspace @example/computer-codemode
./script/run
```

The script runs four commands inside the container through
`POST /c/demo/exec`:

```sh
codemode types # declarations of every global
codemode search "append a note" # find a method
codemode describe notes.add # declarations for one method
echo 'await notes.add({ text: "hello" }); return await notes.list({})' | codemode
```

A script is the body of an async function; `return` sends a value
back and `console.log` lines come back on stderr. Exit code 1 means
the script threw, 2 means `codemode` could not connect or was used
wrongly, and 3 means the run paused for approval on the host. There
is no command to approve it from the container, on purpose: a run
pauses because a connector asked for a human's decision.

## Tests

```sh
npm test --workspace @example/computer-codemode
```

The workers test opens `/codemode` on the Durable Object the way the
container would and runs scripts through the real runtime and dynamic
worker, without a container.
27 changes: 27 additions & 0 deletions examples/codemode/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@example/computer-codemode",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Example Worker + Durable Object whose container can run scripts on the host with the codemode CLI.",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"test": "vitest run --config vitest.config.ts",
"typecheck": "tsc --noEmit",
"cf-typegen": "wrangler types"
},
"dependencies": {
"@cloudflare/codemode": "^0.5.0",
"@cloudflare/computer": "*"
},
"devDependencies": {
"@cloudflare/computer-rpc": "*",
"@cloudflare/vitest-pool-workers": "^0.22.0",
"@cloudflare/workers-types": "^5.20260722.1",
"capnweb": "^0.10.0",
"typescript": "^6.0.3",
"vitest": "^4.1.11",
"wrangler": "^4.107.1"
}
}
35 changes: 35 additions & 0 deletions examples/codemode/script/run
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# Smoke test against a running `wrangler dev` of this example. Each
# step runs a command inside the container; the codemode ones travel
# back to the Durable Object and run there.
#
# npm run dev (in another terminal), then:
# ./script/run # against http://127.0.0.1:8787
# NAME=foo ./script/run # use Durable Object instance 'foo'

set -euo pipefail

BASE_URL="${1:-http://127.0.0.1:8787}"
BASE_URL="${BASE_URL%/}"
NAME="${NAME:-demo}"

step() { printf '\n=== %s ===\n' "$*"; }
exec_in_container() {
curl -fsS -X POST "${BASE_URL}/c/${NAME}/exec" \
-H 'content-type: application/json' \
-d "$(printf '{"command":%s}' "$(printf '%s' "$1" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(JSON.stringify(s)))')")"
}

step "1. what can a script call?"
exec_in_container 'codemode types'

step "2. find one method"
exec_in_container 'codemode search "append a note" && codemode describe notes.add'

step "3. run a script"
exec_in_container "echo 'await notes.add({ text: \"hello\" }); return await notes.list({})' | codemode"

step "4. a script that throws exits 1"
exec_in_container "codemode -e 'throw new Error(\"nope\")'; echo exit=\$?"

printf '\nOK\n'
Loading
Loading