Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5350184
computer: Add ws:container for isolate JavaScript
mattzcarey Oct 1, 2026
35f0c79
computer: Pick exec backends with one exec option (#181)
mattzcarey Oct 1, 2026
2954593
computer: Carry backend information through Workspace clients (#182)
mattzcarey Oct 1, 2026
f9a96ac
computer: Describe the new ContainerBackend instead of the legacy one
mattzcarey Oct 1, 2026
45b8745
computer: Check ws:container's backend when it connects
mattzcarey Oct 1, 2026
589f2d6
docs: Use ContainerBackend for ws:container
mattzcarey Oct 1, 2026
6169225
examples/think, examples/mcp: Move to ContainerBackend
mattzcarey Oct 1, 2026
0117110
computer: Check only that ws:container's backend exists
mattzcarey Oct 1, 2026
3846920
computer: Tell ws:container's backend kind by protocol
mattzcarey Oct 1, 2026
bb75496
computer: Run a single-backend exec tool on its backend for direct calls
mattzcarey Oct 6, 2026
ba4b679
computer: Format the exec tool after the rebase onto main
mattzcarey Oct 6, 2026
d2129fc
computer: Let the runtime cut ws:container output and say where the r…
mattzcarey Oct 6, 2026
f153332
computer: Say where ws:container's saved output can be read
mattzcarey Oct 6, 2026
6ee9b6a
computer: Pass isolate capability calls as native RPC values
mattzcarey Oct 1, 2026
752e63d
computer: Keep host bridge responses within their limits
mattzcarey Oct 6, 2026
83b6988
computer: Keep fitting error paths and own __proto__ fields in host v…
mattzcarey Oct 6, 2026
26d8a6a
computer: Leave concurrent JavaScript executions to the platform
mattzcarey Oct 1, 2026
38e678e
computer: Mention -C in ws:git's description
mattzcarey Oct 2, 2026
bcdf317
computer: Leave publish out of tools built from a remote client witho…
mattzcarey Oct 2, 2026
610b643
computer: Drop undefined result fields and reject cyclic arguments cl…
mattzcarey Oct 2, 2026
11266d0
computer: Report ws:container's file sync to the caller
mattzcarey Oct 2, 2026
c2c6408
computer: Cap ws:container's skipped-path list
mattzcarey Oct 2, 2026
e5abd08
computer: Expect skippedCount in the ws:container truncation test
mattzcarey Oct 6, 2026
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/container-sync-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/computer": patch
---

`ws:container`'s `exec` also returns `sync: { status, skipped, skippedCount, error? }`. `status` is `pending` when the container's file changes have not reached the Workspace, and `skipped` lists up to 100 paths the Workspace refused, such as files in a read-only mount, with `skippedCount` giving the full count. The docs now state that the sync is last-writer-wins: the container's changes replace files written in the Workspace while the command runs.
11 changes: 11 additions & 0 deletions .changeset/exec-tool-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@cloudflare/computer": minor
---

`createAITools` takes an `exec` option that lists the backends the model can use, keyed by backend id: `exec: { "worker-javascript": { description: "Use for data work." } }`. Leave it out to use every backend the Workspace has. `{}` exposes a backend with nothing beyond its own description, and `exec: {}` means no exec tool. `createExecTool` takes the same map as `backends`, and `defaultBackend` goes away: with more than one backend the model must name one on every call.

`WorkerShellBackend` and `ContainerBackend` now describe themselves to the model, as `WorkerJavaScriptBackend` does, so the default needs no descriptions. A backend that says nothing gets a one-line default instead of an error.

`shell` still works and is deprecated. `shell: { backends }` becomes `exec: backends`, and its `defaultBackend` is ignored. Output limits stay on `createExecTool`.

`createAITools` moves to its own entry point, `@cloudflare/computer/tools/ai-sdk`. `@cloudflare/computer/tools` keeps the individual `create*Tool` functions and `WorkspaceFileStore`. Change `import { createAITools } from "@cloudflare/computer/tools"` to `from "@cloudflare/computer/tools/ai-sdk"`.
Comment on lines +5 to +11

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consolidate to one line or break into several one-liners.

5 changes: 5 additions & 0 deletions .changeset/exec-tool-review-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/computer": patch
---

A `WorkspaceClient` from `getWorkspace()` now answers `runtime.backends()`, locally and over RPC, from a snapshot taken when the client is created. `createAITools({ workspace: await getWorkspace(this) })` therefore offers `exec` over every backend, and a callable backend keeps its `input` argument and module list. `ContainerBackend` describes network access that matches its `egress` setting, and `exec` takes precedence over the deprecated `shell` option.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add runtime.backends() to the workspace to return configured backends.

4 changes: 2 additions & 2 deletions .changeset/exec-tool-single-backend.md

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Condense to one line and link to docs

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"@cloudflare/computer": minor
---

The `exec` tool offers only the arguments that can work. With one backend there is no `backend` argument, the tool always runs there, `defaultBackend` becomes optional, and the description talks about what that backend does rather than how to choose one. `input` appears only when a configured backend accepts it.
The `exec` tool offers only the arguments that can work. With one backend there is no `backend` argument, the tool always runs there, and the description talks about what that backend does rather than how to choose one. `input` appears only when a configured backend accepts it.

Each backend's entry now adds what the backend says about itself, read through `workspace.runtime.describe(id)`. For `WorkerJavaScriptBackend` that is its source language and every module code can import, so `shell: { backends: { "worker-javascript": {} } }` is enough and the module list the model reads cannot drift from `modules`. A backend `description` is required only for a backend that does not describe itself.
Each backend's entry now adds what the backend says about itself, read through `workspace.runtime.backends()`. For `WorkerJavaScriptBackend` that is its source language and every module code can import, so the module list the model reads cannot drift from `modules`.
5 changes: 5 additions & 0 deletions .changeset/isolate-values.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/computer": patch
---

A `WorkerJavaScriptBackend` run that returns an object with `undefined` fields now completes with those fields dropped, as `JSON.stringify` does, instead of failing with "must be JSON-compatible values". A cyclic argument to a `node:fs` or host module call now fails with a clear "must be acyclic" error instead of a stack overflow.
5 changes: 5 additions & 0 deletions .changeset/js-no-execution-cap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/computer": minor
---

`WorkerJavaScriptBackend` no longer caps concurrent executions, and the `maxConcurrentExecutions` option is removed. Its default of 24 sat above the platform's own limit of 10 concurrent Dynamic Workers per request, so runs 11 through 24 were admitted and then failed with a platform error anyway. Executions now start until the platform says no, and that error is the execution's error. Remove `maxConcurrentExecutions` from backend options.
2 changes: 1 addition & 1 deletion .changeset/modules.md

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Condense to one line and link to docs

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@

`ws:git` and `ws:artifacts` are no longer installed automatically. Add `createGitModule()` from `@cloudflare/computer/modules/git` and `createArtifactsModule()` from `@cloudflare/computer/modules/artifacts`. `node:fs` and `node:fs/promises` stay built in.

The backend describes its source language and every importable module for a model in `backend.description`, which `workspace.runtime.describe(id)` returns.
The backend describes its source language and every importable module for a model in `backend.description`, which `workspace.runtime.backends()` returns along with each backend's id and whether it is callable.

To migrate, move `trustedModules` entries into `modules`, replacing any `call(method, args)` handler with one function per method. Replace `allowGitNetwork: true` with `createGitModule({ allowNetwork: true })` and `allowArtifactNetwork: true` with `createArtifactsModule({ allowNetwork: true })`.
5 changes: 5 additions & 0 deletions .changeset/native-rpc-modules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/computer": patch
---

`WorkerJavaScriptBackend` passes `node:fs` and host module calls between the isolate and the Durable Object as real Workers RPC values instead of JSON text with a custom byte encoding. Byte arrays now count at their real size against `maxCapabilityBytes`, so a 900-byte write fits under a 1024-byte limit where it used to be rejected. Every call still goes through one host bridge that enforces call counts, concurrency, deadlines, and byte budgets, and it now rejects functions, RPC stubs, and cycles in a request before the host acts on it.
5 changes: 5 additions & 0 deletions .changeset/remote-client-assets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/computer": patch
---

A remote `WorkspaceClient` from `getWorkspace(stub)` reports `assets` as `undefined` when the Workspace has no assets publisher, as a local client does. `createAITools` built from a remote client no longer offers a `publish` tool that fails when called.
5 changes: 5 additions & 0 deletions .changeset/ws-container-module.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/computer": minor
---

Add `createContainerModule()` in `@cloudflare/computer/modules/container`. Install it as `modules: { "ws:container": createContainerModule() }` on a `WorkerJavaScriptBackend`, and JavaScript can run shell commands in the Workspace's `ContainerBackend` with `import { exec } from "ws:container"`. The JavaScript backend fails to connect if that backend is missing or runs module source rather than shell commands. The container shares the Workspace's files, a canceled execution kills the command, and `exec` refuses to run on a read-only backend. The module describes itself, so the `exec` tool tells the model about it without extra configuration.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Add `createContainerModule()` in `@cloudflare/computer/modules/container`. Install it as `modules: { "ws:container": createContainerModule() }` on a `WorkerJavaScriptBackend`, and JavaScript can run shell commands in the Workspace's `ContainerBackend` with `import { exec } from "ws:container"`. The JavaScript backend fails to connect if that backend is missing or runs module source rather than shell commands. The container shares the Workspace's files, a canceled execution kills the command, and `exec` refuses to run on a read-only backend. The module describes itself, so the `exec` tool tells the model about it without extra configuration.
Add "ws:container" module to WorkerJavaScriptBackend.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ SQLite and exposes one pluggable execution surface through
- **Isolate JavaScript** runs an ECMAScript module in a fresh Dynamic
Worker with structured input/results, durable relative imports,
configured libraries, Workspace-backed `node:fs/promises`, and host modules such as
`ws:git` and `ws:artifacts`.
`ws:git`, `ws:artifacts`, and `ws:container`.

A Workspace may register multiple backends under stable IDs.
`workspace.runtime.exec(source, { backend })` is the single execution
Expand Down
48 changes: 19 additions & 29 deletions docs/09_tool_interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ Computer ships a ready-made tool set for agents that use a `Workspace`, once for

| Library | Entry point | Factory |
| --- | --- | --- |
| [AI SDK](https://github.com/vercel/ai) (`ai`) | `@cloudflare/computer/tools` | `createAITools` |
| [AI SDK](https://github.com/vercel/ai) (`ai`) | `@cloudflare/computer/tools/ai-sdk` | `createAITools` |
| [pi](https://github.com/earendil-works/pi) (`@earendil-works/pi-ai`) | `@cloudflare/computer/tools/pi-ai` | `createPiTools` |
| [TanStack AI](https://tanstack.com/ai) (`@tanstack/ai`) | `@cloudflare/computer/tools/tanstack-ai` | `createTanStackTools` |

All three take the same options and build the same tools, with the same names, descriptions, schemas, and limits. Only the shape they return differs. Each entry point imports only `zod` and its own library's types, so a pi agent never loads `ai` and an AI SDK agent never loads pi. The individual AI SDK `create*Tool` functions and `WorkspaceFileStore` also come from `@cloudflare/computer/tools`.
All three take the same options and build the same tools, with the same names, descriptions, schemas, and limits. Only the shape they return differs. Each entry point imports only `zod` and its own library's types, so a pi agent never loads `ai` and an AI SDK agent never loads pi. The individual AI SDK `create*Tool` functions and `WorkspaceFileStore` come from `@cloudflare/computer/tools`.

The tools wrap three Workspace surfaces:

- `workspace.fs` for file reads, writes, edits, searches, listings, and deletion;
- `workspace.runtime.exec` for command execution when the caller opts in;
- `workspace.runtime.exec` for running commands and code on the Workspace's backends;
- `workspace.assets` for publishing generated files when an assets publisher is configured.

## What ships
Expand All @@ -34,13 +34,13 @@ The tools wrap three Workspace surfaces:
| `createPublishTool` | Publish a workspace file through `workspace.assets`. |
| `WorkspaceFileStore` | Adapt `workspace.fs` to the store used by file tools. |

Every tool set names its tools `read`, `ls`, `find`, `grep`, `write`, `edit`, and `delete`. `exec` appears when the caller supplies `shell` options. `publish` appears when assets are configured. In read-only mode the set is `read`, `ls`, `find`, and `grep`.
Every tool set names its tools `read`, `ls`, `find`, `grep`, `write`, `edit`, and `delete`. `exec` appears when the Workspace has a backend, unless you pass `exec: {}`. `publish` appears when assets are configured. In read-only mode the set is `read`, `ls`, `find`, and `grep`.

## Wiring up

```ts
import { Workspace } from "@cloudflare/computer";
import { createAITools } from "@cloudflare/computer/tools";
import { createAITools } from "@cloudflare/computer/tools/ai-sdk";

export class Agent {
workspace: Workspace;
Expand All @@ -65,31 +65,20 @@ export class Agent {

Pass the returned AI SDK `ToolSet` to `generateText`, `streamText`, or an agent framework hook such as `getTools()`.

Pass `shell` only when the Workspace has matching backend ids. With one backend, `exec` has no `backend` argument and always runs there:
`exec` lists the backends the model can use, keyed by backend id. Leave it out to use every backend.

```ts
const tools = createAITools({
createAITools({ workspace }); // every backend
createAITools({ workspace, exec: { "worker-javascript": {} } }); // just this one
createAITools({
workspace,
shell: { backends: { "worker-javascript": {} } },
exec: { "worker-javascript": { description: "Use for data work." } }, // with your own text
});
```

With more than one, pass `defaultBackend` and the model picks a backend per call:

```ts
const tools = createAITools({
workspace,
shell: {
defaultBackend: "shell",
backends: {
shell: { description: "Fast Worker shell with built-in text commands." },
container: { description: "Full Linux userland in a Cloudflare Container." },
},
},
});
```
Each backend describes itself, and a `description` you pass comes first. `exec: {}` means no exec tool. With one backend, `exec` has no `backend` argument and always runs there. With several, the model must name a backend on every call; there is no default.

`createPiTools` and `createTanStackTools` take `shell` the same way.
`createPiTools` and `createTanStackTools` take `exec` the same way.

## pi

Expand Down Expand Up @@ -166,7 +155,7 @@ createAITools({
read?,
write?,
edit?,
shell?,
exec?,
});
```

Expand All @@ -178,7 +167,8 @@ createAITools({
| `read` | default caps | Options passed to `createReadTool`. |
| `write` | default caps | Options passed to `createWriteTool`. |
| `edit` | default caps | Options passed to `createEditTool`. |
| `shell` | omitted | Options passed to `createExecTool`. |
| `exec` | every backend | Backend id to `{ description? }`. `{}` omits `exec`. |
| `shell` | omitted | Deprecated. `{ backends }` becomes `exec: backends`; `defaultBackend` is ignored. |

`createPiTools` and `createTanStackTools` take the same options, plus their own listed above.

Expand Down Expand Up @@ -332,17 +322,17 @@ The tool uses forced removal, so deleting a missing path succeeds. Set `recursiv

## `exec`

`exec` is opt-in. It calls `workspace.runtime.exec` with the configured backend and streams bounded output.
`exec` calls `workspace.runtime.exec` on the chosen backend and streams bounded output. `createExecTool({ workspace, backends?, maxBytes?, streamMaxBytes? })` takes the same `backends` as the `exec` option, plus output limits.

Each backend's entry in the tool description joins two parts: the `description` you pass, and what the backend says about itself (`backend.description`, read through `workspace.runtime.describe(id)`). `WorkerJavaScriptBackend` describes its source language and every module code can import, so `{ "worker-javascript": {} }` is enough and the list stays in step with `modules`. A backend that does not describe itself needs a `description`. Describe capabilities and startup cost in plain language.
Each backend's entry in the tool description joins two parts: your text, if any, and what the backend says about itself (`backend.description`, read through `workspace.runtime.backends()`). `WorkerJavaScriptBackend` describes its source language and every module code can import, so the list stays in step with `modules`. `WorkerShellBackend` and `ContainerBackend` describe their command sets, network access, and startup cost. A backend that says nothing gets a one-line default, so add text for a custom backend.

The tool offers only the arguments that can work:

| Backends | Arguments |
| --- | --- |
| One shell backend | `command`, `cwd`, `env` |
| One callable backend | `command`, `cwd`, `env`, `input` |
| More than one | `command`, `cwd`, `backend`, `env`, plus `input` when any is callable. `defaultBackend` is required. |
| More than one | `command`, `cwd`, `backend` (required), `env`, plus `input` when any is callable |

A `backend` value the model sends anyway is dropped when only one backend is configured. The output still names the backend that ran.

Expand All @@ -357,7 +347,7 @@ line 3000

The model can open that file with `read` or search it with `grep`. `streamMaxBytes` is ignored; memory per stream stays within a few times `maxBytes`.

Wire this tool carefully: it executes arbitrary shell commands inside the configured backend. Treat its output as untrusted text when including it in later model input. Omit `shell` or use `readonly: true` when command execution is not part of the agent's job.
Wire this tool carefully: it executes arbitrary shell commands inside the configured backend. Treat its output as untrusted text when including it in later model input. Pass `exec: {}` or `readonly: true` when command execution is not part of the agent's job, and list backends explicitly when the Workspace has one the model should not use directly.

## `publish`

Expand Down
6 changes: 3 additions & 3 deletions docs/10_project_layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,13 +175,13 @@ produces the Node SEA single-file binary at

## Tools

Agent tools (`read`, `write`, `edit`, `ls`, optional `exec`, and optional
Agent tools (`read`, `write`, `edit`, `ls`, `exec`, and optional
`publish`) ship from the package rather than a separate one, with one
entry point per agent library: `createAITools()` from
`@cloudflare/computer/tools`, `createPiTools()` from
`@cloudflare/computer/tools/ai-sdk`, `createPiTools()` from
`@cloudflare/computer/tools/pi-ai`, and `createTanStackTools()` from
`@cloudflare/computer/tools/tanstack-ai`. The individual AI SDK
`create*Tool` functions come from `@cloudflare/computer/tools` too. They live under
`create*Tool` functions come from `@cloudflare/computer/tools`. They live under
[`packages/computer/src/tools/`](../packages/computer/src/tools/):
`common/` holds each tool's schema, description, and executor with no
agent library in it, and `ai-sdk/`, `pi-ai/`, and `tanstack-ai/` wrap
Expand Down
Loading
Loading