Skip to content
Open
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
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"`.
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.
4 changes: 2 additions & 2 deletions .changeset/exec-tool-single-backend.md
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`.
2 changes: 1 addition & 1 deletion .changeset/modules.md
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/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.
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
55 changes: 53 additions & 2 deletions docs/17_isolate_javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,11 @@ Caller source can import three kinds of module, and all of them are fixed when t
| --- | --- | --- | --- |
| Built in | Always installed | The isolate, backed by the Workspace | `node:fs`, `node:fs/promises` |
| Source | `modules: { name: "source" }` | The isolate | a bundled library |
| Host | `modules: { "ws:name": { fn } }`, or a factory | The Durable Object | `ws:git`, `ws:artifacts`, your own |
| Host | `modules: { "ws:name": { fn } }`, or a factory | The Durable Object | `ws:git`, `ws:container`, your own |

```ts
import { createArtifactsModule } from "@cloudflare/computer/modules/artifacts";
import { createContainerModule } from "@cloudflare/computer/modules/container";
import { createGitModule } from "@cloudflare/computer/modules/git";

new WorkerJavaScriptBackend({
Expand All @@ -139,6 +140,7 @@ new WorkerJavaScriptBackend({
"tar-stream": TAR_STREAM_BUNDLE,
"ws:git": createGitModule(),
"ws:artifacts": createArtifactsModule(),
"ws:container": createContainerModule(),
"ws:weather": {
forecast: ([city]) => lookUpForecast(String(city)),
},
Expand All @@ -148,7 +150,7 @@ new WorkerJavaScriptBackend({

An import that is not built in, configured, or a relative Workspace path fails before the Worker is created. Caller source and durable files cannot shadow a configured or built-in module.

The backend describes its modules for a model in `backend.description`, which `workspace.runtime.describe(id)` returns and the `exec` tool shows. It is built from the same `modules` option the backend runs with, so it always matches what is installed:
The backend describes its modules for a model in `backend.description`, which `workspace.runtime.backends()` returns and the `exec` tool shows. It is built from the same `modules` option the backend runs with, so it always matches what is installed:

```text
`command` is ECMAScript module source, run in an isolated JavaScript runtime. Relative imports resolve from `cwd` in the workspace.
Expand All @@ -158,6 +160,7 @@ Modules code can import:
- `node:fs/promises` (also `node:fs`): the workspace's files. ...
- `tar-stream`: a bundled library.
- `ws:git`: The workspace's Git repository tools: `status({ dir })`, ...
- `ws:container`: Runs shell commands in a full Linux container that shares this workspace's files. ...
- `ws:weather`: exports `forecast`.
```

Expand Down Expand Up @@ -244,6 +247,54 @@ import { create, get, list, importArtifact, deleteArtifact } from "ws:artifacts"

`createArtifactsModule()` from `@cloudflare/computer/modules/artifacts` wraps the Workspace's Artifacts client. Calls that change Artifacts need a read-write backend. `importArtifact()` fetches from a caller-chosen URL on the host, so it is denied unless you pass `createArtifactsModule({ allowNetwork: true })`. Every call fails clearly when no Artifacts binding is configured.

### `ws:container`

`createContainerModule()` from `@cloudflare/computer/modules/container` lets JavaScript run shell commands in the Workspace's container backend. With it, JavaScript is the only backend the model sees, and the container is something that JavaScript can call:

```ts
import { ContainerBackend, withWorkspaceContainer } from "@cloudflare/computer/backends/container";

class Agent extends withWorkspaceContainer(class extends DurableObject<Env> {}) {
workspace = new Workspace({
storage: this.ctx.storage,
backends: [
new WorkerJavaScriptBackend({
loader: this.env.LOADER,
access: "read-write",
modules: { "ws:container": createContainerModule() },
}),
new ContainerBackend({
container: () => this,
workspace: { binding: "Agent", id: this.ctx.id.toString() },
egress: { mode: "direct" },
}),
],
});
}

// Offer only the JavaScript backend; the container is reached through ws:container.
const tools = createAITools({ workspace: this.workspace, exec: { "worker-javascript": {} } });
```

```js
import { exec } from "ws:container";

export default async function () {
const { exitCode, stdout, stderr } = await exec("npm test", { cwd: "/workspace/app" });
return { passed: exitCode === 0, stdout, stderr };
}
```

`exec(command, { cwd, env, stdin, timeoutMs })` runs through `workspace.runtime.exec` on the container backend: `ContainerBackend`, registered as `"container-shell"` unless you pass `backend`. If that backend is missing, or runs module source rather than shell commands, the JavaScript backend fails to connect. The container shares the Workspace's files: writes the module made before the call are pushed to the container, and the container's changes are pulled back before `exec` returns. A non-zero exit code comes back as a value, not as an error.

A few limits follow from `exec` being a host call:

- Output comes back when the command finishes, not while it runs. Each stream keeps its last 2000 lines or `maxOutputBytes` (64 KiB by default), which must stay well under the backend's `maxCapabilityBytes`. When a stream is cut, the result's `truncated.stdout.path` (or `stderr`) names a Workspace file holding all of it (see [Long output](./05_runtime_interface.md#long-output)), and the host never holds more than the end in memory. The default directory, `/.computer/output`, is outside the backend's default `root` (`/workspace`), so isolate code cannot open it with `node:fs`; the agent's `read` and `grep` tools can. Set the Workspace's `output.dir` under `root` if isolate code needs to read it.
- The command's timeout is capped at the time left before the host call deadline (`maxHostCallMs`, which defaults to `maxTimeoutMs`). Raise `defaultTimeoutMs`, `maxTimeoutMs`, and `maxHostCallMs` for slow installs and builds, and remember the container's first start.
- Cancelling the execution kills the running command.

A container command can write to the Workspace, so `exec` refuses to run on a read-only backend. Whether it can reach the network follows `ContainerBackend`'s own `egress` setting, not the JavaScript backend's.

## Isolation and lifecycle

Each execution receives a fresh Dynamic Worker with:
Expand Down
Loading
Loading