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
44 changes: 17 additions & 27 deletions docs/09_tool_interface.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# 09. Tool interface (agents)

`@cloudflare/computer/tools` ships ready-made [AI SDK](https://github.com/vercel/ai) tools for agents that use a `Workspace`.
`@cloudflare/computer/tools/ai-sdk` ships `createAITools()`, a ready-made [AI SDK](https://github.com/vercel/ai) tool set for agents that use a `Workspace`. The individual `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 @@ -24,13 +24,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. |

`createAITools()` always 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`.
`createAITools()` always 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 @@ -55,29 +55,18 @@ 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.

## `createAITools`

Expand All @@ -89,7 +78,7 @@ createAITools({
read?,
write?,
edit?,
shell?,
exec?,
});
```

Expand All @@ -101,7 +90,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. |

## `read`

Expand Down Expand Up @@ -253,21 +243,21 @@ 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.

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
7 changes: 4 additions & 3 deletions docs/10_project_layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,10 @@ produces the Node SEA single-file binary at

## Tools

AI SDK tools (`read`, `write`, `edit`, `ls`, optional `exec`, and
optional `publish`) ship from the `@cloudflare/computer/tools` subpath
rather than a separate package, under
AI SDK tools (`read`, `write`, `edit`, `ls`, `exec`, and optional
`publish`) ship from the package rather than a separate one:
`createAITools()` from `@cloudflare/computer/tools/ai-sdk`, and the
individual `create*Tool` functions from `@cloudflare/computer/tools`. They live under
[`packages/computer/src/tools/`](../packages/computer/src/tools/). See
[09. Tool Interface (Agents)](./09_tool_interface.md).

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 is cut at `maxOutputBytes` (64 KiB by default), which must stay well under the backend's `maxCapabilityBytes`.
- 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