Skip to content

computer: Pick exec backends with one exec option - #181

Merged
aron-cf merged 7 commits into
feat/ws-container-modulefrom
feat/exec-tool-options
Oct 1, 2026
Merged

aron-cf merged 7 commits into
feat/ws-container-modulefrom
feat/exec-tool-options

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Stacked on #172.

Turning on the exec tool took a nested shell option, a defaultBackend, and a description for every backend, even one that already describes itself:

import { createAITools } from "@cloudflare/computer/tools";

createAITools({
  workspace,
  shell: {
    defaultBackend: "shell",
    backends: {
      shell: { description: "Fast Worker shell..." },
      container: { description: "Full Linux..." },
    },
  },
});

createAITools now takes exec, which lists the backends the model can use, keyed by backend id. Leave it out to use every backend the Workspace has:

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

createAITools({ workspace });                                     // every backend
createAITools({ workspace, exec: { "worker-javascript": {} } });  // just this one
createAITools({
  workspace,
  exec: { "worker-javascript": { description: "Use for data work." } },
});

The keys are the ids backends are registered under ("worker-javascript", "worker-shell", "container-shell" by default). {} adds nothing beyond the backend's own description, a description you pass comes before it, and exec: {} means no exec tool.

There is no default backend. With one backend the tool has no backend argument. With several, backend is required, so the model names one on every call and the order of exec entries means nothing. defaultBackend goes away. createExecTool takes the same map as backends, and rejects an id the Workspace does not have. The runtime gains backendIds() so the tool can list backends.

The default needs no text from the caller, so WorkerShellBackend and CloudflareContainerBackend now describe themselves, as WorkerJavaScriptBackend does (#171). A custom backend that says nothing gets a one-line default instead of an error.

createAITools also moves to its own entry point, @cloudflare/computer/tools/ai-sdk. @cloudflare/computer/tools keeps the framework-neutral create*Tool functions and WorkspaceFileStore, which leaves room for tool sets for other frameworks next to it.

This breaks three things:

  • Import createAITools from @cloudflare/computer/tools/ai-sdk.
  • exec is no longer opt-in. A Workspace with backends gets the tool unless you pass exec: {} or readonly: true. To keep a backend away from the model, such as the container behind ws:container, list backends explicitly.
  • A tool with several backends rejects calls that leave out backend.

shell still works, deprecated. It maps onto exec, and its defaultBackend is ignored. Output limits stay on createExecTool.

The examples follow along. think drops its whole shell block for createAITools({ workspace: this.workspace }). mcp and celld use the map form with their own descriptions, because they reach the Workspace over RPC, where backends can't describe themselves. mcp's test now names worker-shell in its exec calls. This PR also trims the #171 changeset where it showed the old shell form.

The tests read the generated JSON Schema to check that backend is absent with one backend and required with several. They cover the default over every backend, exec: {}, unknown ids, the fallback description, and the deprecated shell mapping. docs/09_tool_interface.md covers the option.

createAITools only offered exec when the caller passed shell options:
a backends map whose values were { description } objects, plus a
separate defaultBackend. Exposing one JavaScript backend with no extra
text took shell: { backends: { "worker-javascript": {} } }.

createAITools now takes exec, and leaving it out offers every backend
the Workspace has, its default first. Pass a list of backend ids, or a
map from id to text for the model, with the default first; true
exposes a backend with no extra text and exec: false turns the tool
off. createExecTool takes the same backends, rejects an id the
Workspace does not have, and drops defaultBackend. The runtime lists
its backends through backendIds().

WorkerShellBackend and CloudflareContainerBackend now describe
themselves, as the JavaScript backend does, so the default needs no
descriptions. A backend that says nothing gets a one-line default
instead of an error. shell still works, deprecated, and maps onto exec.
The think example drops its backend descriptions entirely, and the
other examples move to exec.
@mattzcarey mattzcarey added the allow-pr Allow a PR to remain open. label Oct 1, 2026
@changeset-bot

changeset-bot Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

馃 Changeset detected

Latest commit: 65b1769

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@cloudflare/computer Minor
@cloudflare/dofs Minor
@cloudflare/computer-rpc Minor
@cloudflare/computerd Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 6 potential issues.

Devin Review

Comment thread packages/computer/src/tools/ai.ts Outdated
Comment on lines +144 to +149
const backends = selected.map(({ id, guidance }) => {
const callable = runtime.isCallable?.(id) === true;
const own = runtime.describe?.(id);
const text =
[guidance, own].filter((part) => part !== undefined && part !== "").join("\n\n") ||
(callable ? "Runs `command` as module source." : "Runs shell commands.");

@devin-ai-integration devin-ai-integration Bot Oct 1, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

馃敶 Callable backend loses structured input

With a WorkspaceClient and explicit callable backend, createExecTool omits input and describes code as shell commands. makeRuntimeClient drops isCallable, so celld agents cannot pass structured input to JavaScript.

Learn more

The exec tool uses the runtime's isCallable(id) method to decide whether to offer structured input and whether a command is module source. WorkspaceClient wraps its underlying runtime but exposes only execution methods, not isCallable. Explicit backend selection still creates a tool, but the tool cannot offer the callable-only fields. The same wrapper also loses describe(id), which otherwise provides backend-specific instructions.

Example: Celld registers a callable celld-javascript backend and passes its local getWorkspace(this) client with exec: { 'celld-javascript': '...' }. The generated input schema contains no input, so the model cannot send a JSON value to the module.

Recommended fix: Expose callable and description metadata on WorkspaceRuntimeClient, forwarding it locally from WorkspaceRuntime and remotely through WorkspaceRuntimeStub. Cover explicit callable backends in both client paths.

Devin Review


Was this helpful? React with 馃憤 or 馃憥 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in #182. The client snapshot answers isCallable and describe too, so a callable backend keeps input and its module list through getWorkspace(), locally and over RPC.

Comment thread packages/computer/src/tools/ai.ts Outdated
Comment on lines +184 to +185
readonly description =
"A shell in a full Linux container: npm, node, python, package managers, test runners, native binaries, and network access. Starts much more slowly than an in-Worker backend because the container must boot.";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

馃煛 Container falsely advertises network access

With default egress: { mode: 'none' }, the description advertises network access unavailable to container commands. connect disables internet unless egress is direct, so agents select unusable network commands.

Learn more

The default exec tool uses a backend's description when the caller supplies no model guidance. CloudflareContainerBackend sets its description independently of its configured egress policy. Its default policy is none; connect starts the container without internet unless the policy is direct. The tool therefore describes a capability that the default configuration does not offer.

Example: Create a container backend without an egress option and expose it through createAITools. The model sees 'network access' and requests curl against a public site, but the container was started without internet.

Recommended fix: Derive network guidance from the actual egress mode; distinguish direct, http-gateway, and none policies rather than advertising unconditional network access.

Devin Review


Was this helpful? React with 馃憤 or 馃憥 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in #182. The description now follows the egress mode: direct, through a gateway, or none.

Comment thread packages/computer/src/tools/exec.ts Outdated
Comment thread packages/computer/README.md Outdated
@pkg-pr-new

pkg-pr-new Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@cloudflare/computer@181

commit: 65b1769

exec took a list of backend ids, a map from id to text with true for no
text, or false for no tool. Three shapes are hard to explain. It is now
one map: each backend the model can use, with a note for the model, ""
for none. Leaving exec out still offers every backend, and an empty map
means no exec tool.

@devin-ai-integration devin-ai-integration Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 2 new potential issues.

Devin Review

options: CreateAIToolsOptions,
runtime: ExecWorkspaceLike["runtime"],
): Omit<ExecToolOptions, "workspace"> & { backends: ExecBackends } {
if (options.shell !== undefined) {

@devin-ai-integration devin-ai-integration Bot Oct 1, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

馃煛 Empty exec selection still exposes tool

When exec: {} accompanies legacy shell settings, execOptions uses shell first. The explicit opt-out still exposes the configured exec tool.

Learn more

createAITools accepts both the new exec setting and the deprecated shell setting. An empty exec map means no exec tool, but createAITools relies on the map returned by execOptions to decide whether to add it. The legacy branch wins even when exec is explicitly empty.

Example: With shell: { backends: { shell: { description: "Commands" } } } and exec: {}, the returned tool set includes exec; the empty map instead calls for no exec tool.

Recommended fix: Check for an explicit empty exec selection before converting legacy shell, while retaining the desired precedence for nonempty settings. Add a test supplying both options.

Devin Review


Was this helpful? React with 馃憤 or 馃憥 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in #182. exec now takes precedence over the deprecated shell, so exec: {} always means no exec tool.

Comment thread packages/computer/src/tools/ai.ts Outdated
Each exec entry is now { description? } instead of a bare string, so a
backend with nothing to add is {} and later per-backend options have a
place to go. shell's backends map now passes through unchanged.
createAITools builds an AI SDK ToolSet, but it shared the
@cloudflare/computer/tools entry point with the framework-neutral
pieces. It now has its own entry point, tools/ai-sdk, which leaves room
for tool sets for other frameworks beside it. tools keeps the
individual create*Tool functions and WorkspaceFileStore. The examples
and docs import from the new path.
With several backends the exec tool had a default: the first one
listed, used when the model left backend out. That made listing order
part of the configuration and let the model run a command without
choosing where.

backend is now required whenever there is a choice, the description no
longer names a default, and the order of exec entries means nothing.
With one backend there is still no backend argument. The deprecated
shell option ignores defaultBackend.
The exec tool now requires a backend when more than one is offered, and
this example offers worker-shell and container-shell.

@devin-ai-integration devin-ai-integration Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 1 new potential issue.

Devin Review

Comment on lines +31 to +33
* @deprecated Use `exec`. `{ backends }` becomes `exec: backends`;
* `defaultBackend` is ignored, because the model names a backend
* whenever there is a choice. Output limits move to `createExecTool`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

馃攳 PR description conflicts with backend selection

The PR description still promises a first-backend default and migration ordering. The API requires a backend for multiple choices and ignores shell.defaultBackend.

Devin Review


Was this helpful? React with 馃憤 or 馃憥 to provide feedback.

@mattzcarey mattzcarey changed the title computer: Offer exec over every backend by default computer: clean up createAITools Oct 1, 2026

@devin-ai-integration devin-ai-integration Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 1 new potential issue.

Devin Review

Comment on lines 174 to +178
shape.backend = z
// SAFETY: createExecTool checked that backendIds has at least one entry.
.enum(backendIds as [string, ...string[]])
.optional()
.describe(
`Which backend to run on. Omit to use the default (${JSON.stringify(defaultBackend)}). If a command fails because the backend lacks that tool, retry on a backend whose description covers it.`,
"Which backend to run on. If a command fails because the backend lacks that tool, retry on a backend whose description covers it.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

馃攳 Consumer guidance still assumes an implicit backend

The MCP guide calls backend optional, and the Think prompt names a default shell. Calls following either instruction now omit a required argument and fail validation.

Devin Review


Was this helpful? React with 馃憤 or 馃憥 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in #182. The mcp README now marks backend as required, and the think prompt tells the model to name a backend on every call.

@mattzcarey mattzcarey changed the title computer: clean up createAITools computer: Pick exec backends with one exec option Oct 1, 2026

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 0 new potential issues.

Devin Review

@aron-cf
aron-cf merged commit 1d0b5e9 into feat/ws-container-module Oct 1, 2026
16 checks passed
@aron-cf
aron-cf deleted the feat/exec-tool-options branch October 1, 2026 10:23
mattzcarey added a commit that referenced this pull request Oct 1, 2026
Split each tool into a framework-neutral core under tools/common
(schema, description, executor; zod only) and an adapter per agent
library. tools/ai-sdk wraps the core with `tool()` from `ai`;
tools/pi-ai and tools/tanstack-ai build their own shapes from the
same core without importing their libraries, so each entry point
pulls in only what it uses.

The exec core is the one #181 and #182 settled on: `exec` takes a
backend map, every Workspace backend is offered by default, `backend`
appears only when there is a choice, and `input` only when a backend
is callable. All three tool sets resolve options through the same
resolveToolOptions, so they offer the same tools.

The pi and TanStack adapters come from #149.

Co-authored-by: aron <263346377+aron-cf@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

allow-pr Allow a PR to remain open.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants