From f5555db3d9177dec07eff46ee9da01372826b606 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Thu, 1 Oct 2026 10:40:59 +0100 Subject: [PATCH 1/7] computer: Offer exec over every backend by default 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. --- .changeset/exec-tool-options.md | 9 ++ .changeset/exec-tool-single-backend.md | 4 +- docs/09_tool_interface.md | 42 +++---- docs/17_isolate_javascript.md | 6 +- examples/celld/.wrangler/cache/cf.json | 70 +++++++++++- examples/celld/src/index.ts | 33 +++--- examples/mcp/src/server.ts | 39 +++---- examples/rlm/worker/executor-tool.ts | 7 +- examples/think/src/agent.ts | 33 +----- packages/computer/README.md | 14 +-- .../container/cloudflare-container.ts | 3 + .../src/backends/worker-shell/worker-shell.ts | 3 + packages/computer/src/runtime/runtime.ts | 7 ++ packages/computer/src/tools/ai.test.ts | 103 ++++++++++++------ packages/computer/src/tools/ai.ts | 52 +++++++-- packages/computer/src/tools/exec.ts | 95 ++++++++++------ packages/computer/src/tools/index.ts | 2 +- 17 files changed, 323 insertions(+), 199 deletions(-) create mode 100644 .changeset/exec-tool-options.md diff --git a/.changeset/exec-tool-options.md b/.changeset/exec-tool-options.md new file mode 100644 index 00000000..189a1917 --- /dev/null +++ b/.changeset/exec-tool-options.md @@ -0,0 +1,9 @@ +--- +"@cloudflare/computer": minor +--- + +`createAITools` takes an `exec` option, and offers the `exec` tool over every backend the Workspace has when you leave it out. Pass a list of backend ids, or a map from id to text for the model, with the default first: `exec: { "worker-javascript": "Use for data work." }`. `true` exposes a backend with no extra text, and `exec: false` turns the tool off. `createExecTool` takes the same `backends`, and `defaultBackend` goes away. + +`WorkerShellBackend` and `CloudflareContainerBackend` 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: { id: { description } }, defaultBackend }` becomes `exec: { id: description }` with the default first. Output limits stay on `createExecTool`. diff --git a/.changeset/exec-tool-single-backend.md b/.changeset/exec-tool-single-backend.md index 701465bc..a524ec8d 100644 --- a/.changeset/exec-tool-single-backend.md +++ b/.changeset/exec-tool-single-backend.md @@ -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.describe(id)`. For `WorkerJavaScriptBackend` that is its source language and every module code can import, so the module list the model reads cannot drift from `modules`. diff --git a/docs/09_tool_interface.md b/docs/09_tool_interface.md index 181a2ba7..be98be8d 100644 --- a/docs/09_tool_interface.md +++ b/docs/09_tool_interface.md @@ -5,7 +5,7 @@ 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 @@ -24,7 +24,7 @@ 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: false`. `publish` appears when assets are configured. In read-only mode the set is `read`, `ls`, `find`, and `grep`. ## Wiring up @@ -55,29 +55,16 @@ 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: +By default `exec` offers every backend the Workspace has, with the Workspace's default first. Each backend describes itself to the model. Pick backends with `exec`, either as a list or as a map to text for the model (`true` adds none). The first entry is the default: ```ts -const tools = createAITools({ - workspace, - shell: { backends: { "worker-javascript": {} } }, -}); +createAITools({ workspace }); // every backend +createAITools({ workspace, exec: ["worker-javascript"] }); // just one +createAITools({ workspace, exec: { "worker-javascript": "Use for data work." } }); // with your own text +createAITools({ workspace, exec: false }); // no exec tool ``` -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." }, - }, - }, -}); -``` +With one backend, `exec` has no `backend` argument and always runs there. ## `createAITools` @@ -89,7 +76,7 @@ createAITools({ read?, write?, edit?, - shell?, + exec?, }); ``` @@ -101,7 +88,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 | A list of backend ids, or a map from id to text for the model. The first is the default. `false` omits `exec`. | +| `shell` | omitted | Deprecated. `{ backends: { id: { description } }, defaultBackend }` becomes `exec: { id: description }` with the default first. | ## `read` @@ -253,9 +241,9 @@ 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.describe(id)`). `WorkerJavaScriptBackend` describes its source language and every module code can import, so the list stays in step with `modules`. `WorkerShellBackend` and `CloudflareContainerBackend` describe their command sets 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: @@ -263,11 +251,11 @@ The tool offers only the arguments that can work: | --- | --- | | 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`, `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: false` 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` diff --git a/docs/17_isolate_javascript.md b/docs/17_isolate_javascript.md index 89a68ce4..e99d01d8 100644 --- a/docs/17_isolate_javascript.md +++ b/docs/17_isolate_javascript.md @@ -264,10 +264,8 @@ this.workspace = new Workspace({ ], }); -const tools = createAITools({ - workspace: this.workspace, - shell: { backends: { "worker-javascript": {} } }, -}); +// Offer only the JavaScript backend; the container is reached through ws:container. +const tools = createAITools({ workspace: this.workspace, exec: ["worker-javascript"] }); ``` ```js diff --git a/examples/celld/.wrangler/cache/cf.json b/examples/celld/.wrangler/cache/cf.json index 774bed0f..b7282d4e 100644 --- a/examples/celld/.wrangler/cache/cf.json +++ b/examples/celld/.wrangler/cache/cf.json @@ -1 +1,69 @@ -{"httpProtocol":"HTTP/1.1","clientAcceptEncoding":"gzip, deflate, br","requestPriority":"","edgeRequestKeepAliveStatus":1,"requestHeaderNames":{},"clientTcpRtt":18,"clientQuicRtt":0,"colo":"DFW","asn":8075,"asOrganization":"Microsoft Limited","country":"US","isEUCountry":false,"city":"Cheyenne","continent":"NA","region":"Wyoming","regionCode":"WY","timezone":"America/Denver","longitude":"-104.82025","latitude":"41.13998","postalCode":"82001","metroCode":"759","tlsVersion":"TLSv1.3","tlsCipher":"AEAD-AES256-GCM-SHA384","tlsClientRandom":"fk2JVeEAfBlrP/C+VZOSiSLmj9xN+c5P7Deo2tukmqM=","tlsClientCiphersSha1":"kXrN3VEKDdzz2cPKTQaKzpxVTxQ=","tlsClientExtensionsSha1":"1eY97BUYYO8vDaTfHQywB1pcNdM=","tlsClientExtensionsSha1Le":"u4wtEMFQBY18l3BzHAvORm+KGRw=","tlsExportedAuthenticator":{"clientHandshake":"2f42c1b65266f736de9ab540b71507dcf00dd653d1b6612774008de57f0492b5e295b41bd3468c7634b880e61deaa0a3","serverHandshake":"343706c57562dfbaa23089d3966f4d64ba8e1ae3d7db043474ed644a0487b8545dbeae9a83f980f4b5cc54c4d9a04d49","clientFinished":"8c5bace08e1fa6903f2bdc37ea035442e4d944cd1402ea604a0b45454ec14b28fc7fb947ba3a8322df65b6abe23c5c40","serverFinished":"22f787324da16e80d1ae8311cf917d76f6000ecece618d6d489ef12802b1771e220fe49617f1b5074d984247142a7ff7"},"tlsClientHelloLength":"1605","tlsClientAuth":{"certPresented":"0","certVerified":"NONE","certRevoked":"0","certIssuerDN":"","certSubjectDN":"","certIssuerDNRFC2253":"","certSubjectDNRFC2253":"","certIssuerDNLegacy":"","certSubjectDNLegacy":"","certSerial":"","certIssuerSerial":"","certSKI":"","certIssuerSKI":"","certFingerprintSHA1":"","certFingerprintSHA256":"","certNotBefore":"","certNotAfter":"","certRFC9440":"","certRFC9440TooLarge":false,"certChainRFC9440":"","certChainRFC9440TooLarge":false},"verifiedBotCategory":"","edgeL4":{"deliveryRate":238490},"botManagement":{"corporateProxy":false,"verifiedBot":false,"jsDetection":{"passed":false},"staticResource":false,"detectionIds":{},"score":99}} \ No newline at end of file +{ + "httpProtocol": "HTTP/1.1", + "clientAcceptEncoding": "gzip, deflate, br", + "requestPriority": "", + "edgeRequestKeepAliveStatus": 1, + "requestHeaderNames": {}, + "clientTcpRtt": 18, + "clientQuicRtt": 0, + "colo": "DFW", + "asn": 8075, + "asOrganization": "Microsoft Limited", + "country": "US", + "isEUCountry": false, + "city": "Cheyenne", + "continent": "NA", + "region": "Wyoming", + "regionCode": "WY", + "timezone": "America/Denver", + "longitude": "-104.82025", + "latitude": "41.13998", + "postalCode": "82001", + "metroCode": "759", + "tlsVersion": "TLSv1.3", + "tlsCipher": "AEAD-AES256-GCM-SHA384", + "tlsClientRandom": "fk2JVeEAfBlrP/C+VZOSiSLmj9xN+c5P7Deo2tukmqM=", + "tlsClientCiphersSha1": "kXrN3VEKDdzz2cPKTQaKzpxVTxQ=", + "tlsClientExtensionsSha1": "1eY97BUYYO8vDaTfHQywB1pcNdM=", + "tlsClientExtensionsSha1Le": "u4wtEMFQBY18l3BzHAvORm+KGRw=", + "tlsExportedAuthenticator": { + "clientHandshake": "2f42c1b65266f736de9ab540b71507dcf00dd653d1b6612774008de57f0492b5e295b41bd3468c7634b880e61deaa0a3", + "serverHandshake": "343706c57562dfbaa23089d3966f4d64ba8e1ae3d7db043474ed644a0487b8545dbeae9a83f980f4b5cc54c4d9a04d49", + "clientFinished": "8c5bace08e1fa6903f2bdc37ea035442e4d944cd1402ea604a0b45454ec14b28fc7fb947ba3a8322df65b6abe23c5c40", + "serverFinished": "22f787324da16e80d1ae8311cf917d76f6000ecece618d6d489ef12802b1771e220fe49617f1b5074d984247142a7ff7" + }, + "tlsClientHelloLength": "1605", + "tlsClientAuth": { + "certPresented": "0", + "certVerified": "NONE", + "certRevoked": "0", + "certIssuerDN": "", + "certSubjectDN": "", + "certIssuerDNRFC2253": "", + "certSubjectDNRFC2253": "", + "certIssuerDNLegacy": "", + "certSubjectDNLegacy": "", + "certSerial": "", + "certIssuerSerial": "", + "certSKI": "", + "certIssuerSKI": "", + "certFingerprintSHA1": "", + "certFingerprintSHA256": "", + "certNotBefore": "", + "certNotAfter": "", + "certRFC9440": "", + "certRFC9440TooLarge": false, + "certChainRFC9440": "", + "certChainRFC9440TooLarge": false + }, + "verifiedBotCategory": "", + "edgeL4": { "deliveryRate": 238490 }, + "botManagement": { + "corporateProxy": false, + "verifiedBot": false, + "jsDetection": { "passed": false }, + "staticResource": false, + "detectionIds": {}, + "score": 99 + } +} diff --git a/examples/celld/src/index.ts b/examples/celld/src/index.ts index fbcf24bd..b5185c68 100644 --- a/examples/celld/src/index.ts +++ b/examples/celld/src/index.ts @@ -49,25 +49,20 @@ export class CelldAgent extends withWorkspace(CelldAgentBase, (self) => { assets: false, ...(this.bindings.LOADER ? { - shell: { - defaultBackend: CELLD_JAVASCRIPT_BACKEND_ID, - backends: { - [CELLD_JAVASCRIPT_BACKEND_ID]: { - description: [ - "Runs a complete JavaScript module in a celld Dynamic Worker with structured input and output.", - "Pass module source, not a filename or bare script. The module must have a default export. Export a function to receive `(input, ctx)` and return structured output.", - "", - "```js", - "export default async function main(input, ctx) {", - ' console.log("cwd:", ctx.cwd);', - " return { received: input };", - "}", - "```", - "", - "The loaded worker cannot access the Workspace filesystem. Use read, write, edit, ls, find, grep, and delete outside exec.", - ].join("\n"), - }, - }, + exec: { + [CELLD_JAVASCRIPT_BACKEND_ID]: [ + "Runs a complete JavaScript module in a celld Dynamic Worker with structured input and output.", + "Pass module source, not a filename or bare script. The module must have a default export. Export a function to receive `(input, ctx)` and return structured output.", + "", + "```js", + "export default async function main(input, ctx) {", + ' console.log("cwd:", ctx.cwd);', + " return { received: input };", + "}", + "```", + "", + "The loaded worker cannot access the Workspace filesystem. Use read, write, edit, ls, find, grep, and delete outside exec.", + ].join("\n"), }, } : {}), diff --git a/examples/mcp/src/server.ts b/examples/mcp/src/server.ts index 090275db..78ee9c81 100644 --- a/examples/mcp/src/server.ts +++ b/examples/mcp/src/server.ts @@ -11,29 +11,22 @@ export async function createComputerMCPServer(workspace: WorkspaceClient, loader const tools = createAITools({ workspace, assets: false, - shell: { - backends: { - "worker-shell": { - description: - "just-bash in an isolated Dynamic Worker. Starts quickly, " + - "does not boot a container, and has no ambient outbound network. " + - "Use it for common shell commands, quick file inspection, and " + - "text transformations. Its built-in git command supports clone, " + - "status, diff, and log; clone accepts HTTPS URLs through the " + - "durable workspace. Prefer the dedicated read, write, and edit " + - "tools for file operations. Cannot run npm, Node.js, Python, " + - "package managers, or arbitrary native binaries.", - }, - "container-shell": { - description: - "Full Debian Linux in a Cloudflare Container with Node.js, npm, " + - "git, package management, native binaries, and outbound network. " + - "Use it for dependency installation, builds, tests, or commands " + - "that worker-shell cannot run. Cold starts more slowly because " + - "the container must boot; prefer worker-shell for simple tasks.", - }, - }, - defaultBackend: "worker-shell", + exec: { + "worker-shell": + "just-bash in an isolated Dynamic Worker. Starts quickly, " + + "does not boot a container, and has no ambient outbound network. " + + "Use it for common shell commands, quick file inspection, and " + + "text transformations. Its built-in git command supports clone, " + + "status, diff, and log; clone accepts HTTPS URLs through the " + + "durable workspace. Prefer the dedicated read, write, and edit " + + "tools for file operations. Cannot run npm, Node.js, Python, " + + "package managers, or arbitrary native binaries.", + "container-shell": + "Full Debian Linux in a Cloudflare Container with Node.js, npm, " + + "git, package management, native binaries, and outbound network. " + + "Use it for dependency installation, builds, tests, or commands " + + "that worker-shell cannot run. Cold starts more slowly because " + + "the container must boot; prefer worker-shell for simple tasks.", }, }); diff --git a/examples/rlm/worker/executor-tool.ts b/examples/rlm/worker/executor-tool.ts index 07921ea9..921616f1 100644 --- a/examples/rlm/worker/executor-tool.ts +++ b/examples/rlm/worker/executor-tool.ts @@ -14,12 +14,9 @@ export function createExecutorTool( const executor = createExecTool({ workspace, backends: { - [backend]: { - description: - "Callable isolated JavaScript. The command must be a complete ES module with a default async function.", - }, + [backend]: + "Callable isolated JavaScript. The command must be a complete ES module with a default async function.", }, - defaultBackend: backend, maxBytes: 16 * 1024, streamMaxBytes: 16 * 1024, }); diff --git a/examples/think/src/agent.ts b/examples/think/src/agent.ts index 624af906..edf4108a 100644 --- a/examples/think/src/agent.ts +++ b/examples/think/src/agent.ts @@ -154,35 +154,8 @@ export class Assistant extends withWorkspaceContainer(AssistantBase) { } override getTools(): ToolSet { - return createAITools({ - workspace: this.workspace, - shell: { - defaultBackend: "shell", - backends: { - shell: { - description: - "just-bash in a Dynamic Worker. Cold-start fast, no " + - "container, no public network. Good for cat / grep / sed / " + - "awk / jq / head / tail / sort / find, quick file " + - "inspection, text transformations, and `git` (clone / " + - "status / diff / log) — the shell registers a built-in " + - "`git` command that forwards to the host workspace, so " + - "network-bound subcommands like `git clone` work even " + - "though the isolate itself has no public network. Only " + - "https:// URLs are supported. Cannot run npm, node, python, " + - "or any binary outside just-bash's built-in command set.", - }, - container: { - description: - "Cloudflare Container running computerd over capnweb. Full Linux " + - "userland: npm, node, python, package managers, test " + - "runners, real binaries on $PATH, and public network. Cold " + - "start is much slower because the container must boot; " + - "reach for it when the shell backend can't run the command. " + - "For git itself, prefer the shell backend.", - }, - }, - }, - }); + // Every backend the Workspace has, "shell" first. Both describe + // themselves to the model. + return createAITools({ workspace: this.workspace }); } } diff --git a/packages/computer/README.md b/packages/computer/README.md index 3f0115b8..c666c8b6 100644 --- a/packages/computer/README.md +++ b/packages/computer/README.md @@ -278,18 +278,14 @@ import { createAITools } from "@cloudflare/computer/tools"; const tools = createAITools({ workspace, read: { maxBytes: 32 * 1024, maxLines: 800 }, - shell: { - defaultBackend: "shell", - backends: { - shell: { description: "Fast Worker shell with built-in text commands." }, - container: { description: "Full Linux userland in a Cloudflare Container." }, - }, - }, + // Omit `exec` to offer every backend, the Workspace default first. + exec: { shell: "Try this first.", container: true }, }); ``` -The model reads each backend's `description` when deciding where a -command should run, so write them in plain language. Truncated text +Each backend describes itself to the model, and the text you give in +`exec` comes first. The model reads both when deciding where a command +should run, so write yours in plain language. Truncated text model output keeps both line and byte continuations; pass both to the next call to avoid transferring the same bytes again. Eligible image and PDF bytes are captured once during the bounded tool execution and returned diff --git a/packages/computer/src/backends/container/cloudflare-container.ts b/packages/computer/src/backends/container/cloudflare-container.ts index fc7445b6..29862b23 100644 --- a/packages/computer/src/backends/container/cloudflare-container.ts +++ b/packages/computer/src/backends/container/cloudflare-container.ts @@ -180,6 +180,9 @@ function bearerMatches(header: string | null, expected: string | undefined): boo export class CloudflareContainerBackend implements WorkspaceBackend { readonly type = "cloudflare-container"; + /** What this backend tells a model: a full Linux shell that is slower to start. */ + 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."; readonly id: string; readonly #options: Required< diff --git a/packages/computer/src/backends/worker-shell/worker-shell.ts b/packages/computer/src/backends/worker-shell/worker-shell.ts index 981059d1..1fe4a1b2 100644 --- a/packages/computer/src/backends/worker-shell/worker-shell.ts +++ b/packages/computer/src/backends/worker-shell/worker-shell.ts @@ -139,6 +139,9 @@ const DEFAULT_COMPAT_FLAGS = ["nodejs_compat"]; export class WorkerShellBackend implements WorkspaceBackend { readonly type = "worker-shell"; + /** What this backend tells a model: a fast shell with a fixed command set. */ + readonly description = + "A just-bash shell in a Dynamic Worker. Starts fast, with no container and no direct network. Good for cat, grep, sed, awk, jq, head, tail, sort, find, text transformations, and a built-in `git` (clone, status, diff, log) that works through the workspace. Cannot run npm, node, python, or binaries outside its built-in command set."; readonly id: string; readonly #options: WorkerShellBackendOptions; readonly #egress: WorkspaceEgressPolicy; diff --git a/packages/computer/src/runtime/runtime.ts b/packages/computer/src/runtime/runtime.ts index f74965f8..64842597 100644 --- a/packages/computer/src/runtime/runtime.ts +++ b/packages/computer/src/runtime/runtime.ts @@ -43,6 +43,13 @@ export class WorkspaceRuntime { return this.#options.backends.get(id)?.callable === true; } + // Every registered backend id, in registration order. The first is + // the default. The exec tool uses this when the caller does not pick + // backends itself. + backendIds(): string[] { + return [...this.#options.backends.keys()]; + } + // What the named backend says about itself for a model: its source // language and, for the JavaScript backend, the modules code can // import. The exec tool adds it to the backend's entry so a caller diff --git a/packages/computer/src/tools/ai.test.ts b/packages/computer/src/tools/ai.test.ts index 146c7c83..57237c49 100644 --- a/packages/computer/src/tools/ai.test.ts +++ b/packages/computer/src/tools/ai.test.ts @@ -1330,19 +1330,55 @@ describe("createAITools filesystem tools", () => { }); describe("createAITools exec tool", () => { - it("adds exec only when shell options are provided", () => { - const workspace = makeWorkspace(); + it("offers exec by default only when the workspace has a backend", () => { + const withBackend = new Workspace({ + storage: new SQLiteTestStorage(), + backends: [streamingCommandBackend([]) as never], + }); - expect(createAITools({ workspace }).exec).toBeUndefined(); - expect( - createAITools({ - workspace, - shell: { - defaultBackend: "shell", - backends: { shell: { description: "test shell" } }, - }, - }).exec, - ).toBeDefined(); + expect(createAITools({ workspace: makeWorkspace() }).exec).toBeUndefined(); + expect(createAITools({ workspace: withBackend }).exec).toBeDefined(); + expect(createAITools({ workspace: withBackend, exec: false }).exec).toBeUndefined(); + expect(createAITools({ workspace: withBackend, readonly: true }).exec).toBeUndefined(); + }); + + it("offers every workspace backend by default, the workspace default first", () => { + const workspace = new Workspace({ + storage: new SQLiteTestStorage(), + backends: [ + streamingCommandBackend([]) as never, + new WorkerJavaScriptBackend({ loader: { load: () => ({ getEntrypoint: () => ({}) }) } }), + ], + }); + const tools = createAITools({ workspace }); + const schema = z.toJSONSchema(inputSchema(tools.exec)) as { + properties: { backend?: { enum?: string[] } }; + }; + + expect(schema.properties.backend?.enum).toEqual(["shell", "worker-javascript"]); + expect(toolDescription(tools.exec)).toContain('Default backend: "shell"'); + expect(toolDescription(tools.exec)).toContain('- "shell": Runs shell commands.'); + expect(toolDescription(tools.exec)).toContain("ECMAScript module source"); + }); + + it("takes backends as a list or as a map to the model's guidance", () => { + const workspace = new Workspace({ + storage: new SQLiteTestStorage(), + backends: [ + streamingCommandBackend([]) as never, + new WorkerJavaScriptBackend({ loader: { load: () => ({ getEntrypoint: () => ({}) }) } }), + ], + }); + const listed = createAITools({ workspace, exec: ["worker-javascript"] }); + const mapped = createAITools({ + workspace, + exec: { "worker-javascript": "Use for data work.", shell: true }, + }); + + expect(inputProperties(listed.exec)).not.toContain("backend"); + expect(toolDescription(listed.exec)).not.toContain('"shell"'); + expect(toolDescription(mapped.exec)).toContain('Default backend: "worker-javascript"'); + expect(toolDescription(mapped.exec)).toContain("Use for data work.\n\n`command` is ECMAScript"); }); it("runs shell commands on the selected backend and truncates output", async () => { @@ -1528,15 +1564,10 @@ describe("createAITools exec tool", () => { }); }); - it("rejects invalid shell backend configuration", () => { - const workspace = makeWorkspace(); - - expect(() => - createAITools({ - workspace, - shell: { defaultBackend: "missing", backends: { shell: { description: "test" } } }, - }), - ).toThrow(/defaultBackend/); + it("rejects a backend the workspace does not have", () => { + expect(() => createAITools({ workspace: makeWorkspace(), exec: ["missing"] })).toThrow( + /unknown backend "missing"/, + ); }); }); @@ -1739,7 +1770,7 @@ describe("createAITools callable exec", () => { expect(toolDescription(withBackendOnly.exec)).toContain("`ws:weather` exports `forecast`"); }); - it("requires a description for a backend that does not describe itself", () => { + it("falls back to a short description for a backend that does not describe itself", () => { const workspace = { runtime: { async exec() { @@ -1747,10 +1778,9 @@ describe("createAITools callable exec", () => { }, }, }; + const tools = createAITools({ workspace, exec: ["shell"] }); - expect(() => createAITools({ workspace, shell: { backends: { shell: {} } } })).toThrow( - /does not describe itself/, - ); + expect(toolDescription(tools.exec)).toContain("Runs shell commands."); }); }); @@ -1859,17 +1889,20 @@ describe("createAITools exec with one backend", () => { expect(inputProperties(tools.exec)).toEqual(["backend", "command", "cwd", "env", "input"]); }); - it("requires defaultBackend when more than one backend is configured", () => { - const { workspace } = recordingWorkspace(false); + it("uses the first listed backend as the default, and the deprecated shell default first", async () => { + const { calls, workspace } = recordingWorkspace(false); + const listed = createAITools({ workspace, exec: { container: "Linux.", shell: "Fast." } }); + const legacy = createAITools({ + workspace, + shell: { + defaultBackend: "shell", + backends: { container: { description: "Linux." }, shell: { description: "Fast." } }, + }, + }); - expect(() => - createAITools({ - workspace, - shell: { - backends: { shell: { description: "Fast shell." }, container: { description: "Linux." } }, - }, - }), - ).toThrow(/defaultBackend/); + await executeTool(listed.exec, { command: "ls" }); + await executeTool(legacy.exec, { command: "ls" }); + expect(calls.map((call) => call.backend)).toEqual(["container", "shell"]); }); }); diff --git a/packages/computer/src/tools/ai.ts b/packages/computer/src/tools/ai.ts index 78cc8358..3407b8c2 100644 --- a/packages/computer/src/tools/ai.ts +++ b/packages/computer/src/tools/ai.ts @@ -1,5 +1,10 @@ import type { ToolSet } from "ai"; -import { createExecTool, type ExecToolOptions, type ExecWorkspaceLike } from "./exec.js"; +import { + createExecTool, + type ExecBackends, + type ExecToolOptions, + type ExecWorkspaceLike, +} from "./exec.js"; import { createDeleteTool } from "./fs/delete.js"; import { createEditTool, type EditToolOptions } from "./fs/edit.js"; import { createFindTool } from "./fs/find.js"; @@ -17,7 +22,22 @@ export interface CreateAIToolsOptions { read?: Omit; write?: Omit; edit?: Omit; - shell?: Omit; + // Which backends `exec` may run on: a list of backend ids, or a map + // from id to text for the model (`true` for none). The first is the + // default. Omit to offer every backend the Workspace has; pass + // `false` for no exec tool. + exec?: ExecBackends | false; + /** + * @deprecated Use `exec`. `{ backends: { id: { description } }, + * defaultBackend }` becomes `exec: { id: description }` with the + * default listed first. Output limits move to `createExecTool`. + */ + shell?: LegacyShellOptions; +} + +interface LegacyShellOptions extends Omit { + backends: Record; + defaultBackend?: string; } export function createAITools(options: CreateAIToolsOptions): ToolSet { @@ -35,11 +55,12 @@ export function createAITools(options: CreateAIToolsOptions): ToolSet { tools.edit = createEditTool({ store, ...options.edit }); tools.delete = createDeleteTool({ store }); - if (options.shell !== undefined) { - tools.exec = createExecTool({ - workspace: options.workspace as ExecWorkspaceLike, - ...options.shell, - }); + const runtime = options.workspace.runtime; + if (runtime !== undefined && options.exec !== false) { + const exec = execOptions(options); + if (exec.backends !== undefined || (runtime.backendIds?.().length ?? 0) > 0) { + tools.exec = createExecTool({ workspace: { runtime }, ...exec }); + } } if (options.assets !== false && options.workspace.assets !== undefined) { @@ -48,3 +69,20 @@ export function createAITools(options: CreateAIToolsOptions): ToolSet { return tools; } + +// Turn `exec`, or the deprecated `shell`, into createExecTool options. +function execOptions(options: CreateAIToolsOptions): Omit { + if (options.shell === undefined) { + return options.exec === undefined || options.exec === false ? {} : { backends: options.exec }; + } + const { backends, defaultBackend, ...limits } = options.shell; + const ids = Object.keys(backends); + const ordered = + defaultBackend === undefined + ? ids + : [defaultBackend, ...ids.filter((id) => id !== defaultBackend)]; + return { + ...limits, + backends: Object.fromEntries(ordered.map((id) => [id, backends[id]?.description ?? true])), + }; +} diff --git a/packages/computer/src/tools/exec.ts b/packages/computer/src/tools/exec.ts index 620d8e56..af2df108 100644 --- a/packages/computer/src/tools/exec.ts +++ b/packages/computer/src/tools/exec.ts @@ -67,27 +67,26 @@ export interface ExecWorkspaceLike { isCallable?(id: string): boolean; // What a backend says about itself for a model, such as the // language it runs and the modules that code can import. The tool - // shows it after the caller's own description. + // shows it after the caller's own text. describe?(id: string): string | undefined; + // Every registered backend id, default first. Used when the caller + // does not pick backends, and to reject an unknown id up front. + backendIds?(): string[]; }; } -export interface ExecBackendDescription { - // Guidance for the model about this backend, shown before whatever - // the backend says about itself. Required only when the backend does - // not describe itself. - description?: string; -} +/** + * Which backends the exec tool may run on: a list of backend ids, or a + * map from id to text shown to the model before the backend's own + * description (`true` for none). The first entry is the default. + */ +export type ExecBackends = readonly string[] | Readonly>; export interface ExecToolOptions { workspace: ExecWorkspaceLike; - // Backends the model may run on. With exactly one, the tool has no - // `backend` argument and always runs there, so the model never has - // to reason about backends. - backends: Record; - // Backend used when the model omits `backend`. Required when more - // than one backend is configured; with one it defaults to that one. - defaultBackend?: string; + // Omit to offer every backend the Workspace has, its default first. + // With exactly one backend the tool has no `backend` argument. + backends?: ExecBackends; // Per-snapshot display cap for each of stdout and stderr, in bytes. // Output past it is shown as a truncation marker. Defaults to 64 KiB. maxBytes?: number; @@ -135,29 +134,20 @@ export function createExecTool(options: ExecToolOptions): Tool JSON.stringify(id)).join(", ")}`, - ); - } - const runtime = options.workspace.runtime; - const backends = backendIds.map((id) => { - const text = [options.backends[id]?.description, runtime.describe?.(id)] - .filter((part) => part !== undefined && part !== "") - .join("\n\n"); - if (text === "") { - throw new Error( - `createExecTool: backend ${JSON.stringify(id)} does not describe itself; pass a description`, - ); - } - return { id, text, callable: runtime.isCallable?.(id) === true }; + const selected = selectBackends(options.backends, runtime); + const [first] = selected; + if (first === undefined) throw new Error("createExecTool: no backends to run on"); + const defaultBackend = first.id; + const backendIds = selected.map((backend) => backend.id); + const single = backendIds.length === 1; + 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."); + return { id, text, callable }; }); const callableBackendIds = new Set(backends.filter((b) => b.callable).map((b) => b.id)); const description = describeTool(backends, defaultBackend); @@ -349,6 +339,39 @@ function describeTool(backends: readonly DescribedBackend[], defaultBackend: str ].join("\n"); } +// Resolve the caller's choice to an ordered list, default first. +function selectBackends( + backends: ExecBackends | undefined, + runtime: ExecWorkspaceLike["runtime"], +): Array<{ id: string; guidance: string | undefined }> { + const known = runtime.backendIds?.(); + let selected: Array<{ id: string; guidance: string | undefined }>; + if (backends === undefined) { + if (known === undefined) { + throw new Error("createExecTool: pass `backends`; this workspace cannot list its backends"); + } + selected = known.map((id) => ({ id, guidance: undefined })); + } else if (isBackendList(backends)) { + selected = backends.map((id) => ({ id, guidance: undefined })); + } else { + selected = Object.entries(backends).map(([id, text]) => ({ + id, + guidance: text === true ? undefined : text, + })); + } + const unknown = known === undefined ? [] : selected.filter((b) => !known.includes(b.id)); + if (unknown.length > 0) { + throw new Error( + `createExecTool: unknown backend ${unknown.map((b) => JSON.stringify(b.id)).join(", ")}; the workspace has ${known?.map((id) => JSON.stringify(id)).join(", ") || "none"}`, + ); + } + return selected; +} + +function isBackendList(backends: ExecBackends): backends is readonly string[] { + return Array.isArray(backends); +} + function commandHint(backends: readonly DescribedBackend[]): string { if (backends.every((backend) => backend.callable)) return "Module source to run."; if (backends.every((backend) => !backend.callable)) { diff --git a/packages/computer/src/tools/index.ts b/packages/computer/src/tools/index.ts index 9bad4745..a814da5d 100644 --- a/packages/computer/src/tools/index.ts +++ b/packages/computer/src/tools/index.ts @@ -1,7 +1,7 @@ export { type CreateAIToolsOptions, createAITools } from "./ai.js"; export { createExecTool, - type ExecBackendDescription, + type ExecBackends, type ExecRuntimeHandle, type ExecStreamEvent, type ExecToolOptions, From a78f2bdaa7e1d4dd5a6e38347de2f772c4a92d03 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Thu, 1 Oct 2026 10:48:03 +0100 Subject: [PATCH 2/7] computer: Take exec backends as one map of notes 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. --- .changeset/exec-tool-options.md | 2 +- docs/09_tool_interface.md | 16 ++++----- docs/17_isolate_javascript.md | 2 +- packages/computer/README.md | 4 +-- packages/computer/src/tools/ai.test.ts | 12 +++---- packages/computer/src/tools/ai.ts | 45 ++++++++++++++------------ packages/computer/src/tools/exec.ts | 20 ++++-------- 7 files changed, 47 insertions(+), 54 deletions(-) diff --git a/.changeset/exec-tool-options.md b/.changeset/exec-tool-options.md index 189a1917..83479857 100644 --- a/.changeset/exec-tool-options.md +++ b/.changeset/exec-tool-options.md @@ -2,7 +2,7 @@ "@cloudflare/computer": minor --- -`createAITools` takes an `exec` option, and offers the `exec` tool over every backend the Workspace has when you leave it out. Pass a list of backend ids, or a map from id to text for the model, with the default first: `exec: { "worker-javascript": "Use for data work." }`. `true` exposes a backend with no extra text, and `exec: false` turns the tool off. `createExecTool` takes the same `backends`, and `defaultBackend` goes away. +`createAITools` takes an `exec` option that lists the backends the model can use, each with a note for the model: `exec: { "worker-javascript": "Use for data work." }`. Leave it out to use every backend the Workspace has. The first backend is the default, a note can be `""`, and `exec: {}` means no exec tool. `createExecTool` takes the same map as `backends`, and `defaultBackend` goes away. `WorkerShellBackend` and `CloudflareContainerBackend` 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. diff --git a/docs/09_tool_interface.md b/docs/09_tool_interface.md index be98be8d..cf73ae4a 100644 --- a/docs/09_tool_interface.md +++ b/docs/09_tool_interface.md @@ -24,7 +24,7 @@ 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 Workspace has a backend, unless you pass `exec: false`. `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 @@ -55,16 +55,14 @@ export class Agent { Pass the returned AI SDK `ToolSet` to `generateText`, `streamText`, or an agent framework hook such as `getTools()`. -By default `exec` offers every backend the Workspace has, with the Workspace's default first. Each backend describes itself to the model. Pick backends with `exec`, either as a list or as a map to text for the model (`true` adds none). The first entry is the default: +`exec` lists the backends the model can use, each with a note for the model. Leave it out to use every backend. ```ts -createAITools({ workspace }); // every backend -createAITools({ workspace, exec: ["worker-javascript"] }); // just one -createAITools({ workspace, exec: { "worker-javascript": "Use for data work." } }); // with your own text -createAITools({ workspace, exec: false }); // no exec tool +createAITools({ workspace }); // every backend +createAITools({ workspace, exec: { "worker-javascript": "Use for data work." } }); // just this one ``` -With one backend, `exec` has no `backend` argument and always runs there. +Each backend also describes itself, so a note can be `""`. The first backend is the default, and `exec: {}` means no exec tool. With one backend, `exec` has no `backend` argument and always runs there. ## `createAITools` @@ -88,7 +86,7 @@ createAITools({ | `read` | default caps | Options passed to `createReadTool`. | | `write` | default caps | Options passed to `createWriteTool`. | | `edit` | default caps | Options passed to `createEditTool`. | -| `exec` | every backend | A list of backend ids, or a map from id to text for the model. The first is the default. `false` omits `exec`. | +| `exec` | every backend | Backend id to a note for the model (`""` for none). The first is the default. `{}` omits `exec`. | | `shell` | omitted | Deprecated. `{ backends: { id: { description } }, defaultBackend }` becomes `exec: { id: description }` with the default first. | ## `read` @@ -255,7 +253,7 @@ The tool offers only the arguments that can work: 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. Pass `exec: false` 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. +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` diff --git a/docs/17_isolate_javascript.md b/docs/17_isolate_javascript.md index e99d01d8..3b832c81 100644 --- a/docs/17_isolate_javascript.md +++ b/docs/17_isolate_javascript.md @@ -265,7 +265,7 @@ this.workspace = new Workspace({ }); // Offer only the JavaScript backend; the container is reached through ws:container. -const tools = createAITools({ workspace: this.workspace, exec: ["worker-javascript"] }); +const tools = createAITools({ workspace: this.workspace, exec: { "worker-javascript": "" } }); ``` ```js diff --git a/packages/computer/README.md b/packages/computer/README.md index c666c8b6..be7ac0ad 100644 --- a/packages/computer/README.md +++ b/packages/computer/README.md @@ -278,8 +278,8 @@ import { createAITools } from "@cloudflare/computer/tools"; const tools = createAITools({ workspace, read: { maxBytes: 32 * 1024, maxLines: 800 }, - // Omit `exec` to offer every backend, the Workspace default first. - exec: { shell: "Try this first.", container: true }, + // The backends the model can use, each with a note. Omit for every backend. + exec: { shell: "Try this first.", container: "" }, }); ``` diff --git a/packages/computer/src/tools/ai.test.ts b/packages/computer/src/tools/ai.test.ts index 57237c49..9f6a3ab7 100644 --- a/packages/computer/src/tools/ai.test.ts +++ b/packages/computer/src/tools/ai.test.ts @@ -1338,7 +1338,7 @@ describe("createAITools exec tool", () => { expect(createAITools({ workspace: makeWorkspace() }).exec).toBeUndefined(); expect(createAITools({ workspace: withBackend }).exec).toBeDefined(); - expect(createAITools({ workspace: withBackend, exec: false }).exec).toBeUndefined(); + expect(createAITools({ workspace: withBackend, exec: {} }).exec).toBeUndefined(); expect(createAITools({ workspace: withBackend, readonly: true }).exec).toBeUndefined(); }); @@ -1361,7 +1361,7 @@ describe("createAITools exec tool", () => { expect(toolDescription(tools.exec)).toContain("ECMAScript module source"); }); - it("takes backends as a list or as a map to the model's guidance", () => { + it("takes the backends to offer, each with a note for the model", () => { const workspace = new Workspace({ storage: new SQLiteTestStorage(), backends: [ @@ -1369,10 +1369,10 @@ describe("createAITools exec tool", () => { new WorkerJavaScriptBackend({ loader: { load: () => ({ getEntrypoint: () => ({}) }) } }), ], }); - const listed = createAITools({ workspace, exec: ["worker-javascript"] }); + const listed = createAITools({ workspace, exec: { "worker-javascript": "" } }); const mapped = createAITools({ workspace, - exec: { "worker-javascript": "Use for data work.", shell: true }, + exec: { "worker-javascript": "Use for data work.", shell: "" }, }); expect(inputProperties(listed.exec)).not.toContain("backend"); @@ -1565,7 +1565,7 @@ describe("createAITools exec tool", () => { }); it("rejects a backend the workspace does not have", () => { - expect(() => createAITools({ workspace: makeWorkspace(), exec: ["missing"] })).toThrow( + expect(() => createAITools({ workspace: makeWorkspace(), exec: { missing: "" } })).toThrow( /unknown backend "missing"/, ); }); @@ -1778,7 +1778,7 @@ describe("createAITools callable exec", () => { }, }, }; - const tools = createAITools({ workspace, exec: ["shell"] }); + const tools = createAITools({ workspace, exec: { shell: "" } }); expect(toolDescription(tools.exec)).toContain("Runs shell commands."); }); diff --git a/packages/computer/src/tools/ai.ts b/packages/computer/src/tools/ai.ts index 3407b8c2..dd8d71b8 100644 --- a/packages/computer/src/tools/ai.ts +++ b/packages/computer/src/tools/ai.ts @@ -22,11 +22,10 @@ export interface CreateAIToolsOptions { read?: Omit; write?: Omit; edit?: Omit; - // Which backends `exec` may run on: a list of backend ids, or a map - // from id to text for the model (`true` for none). The first is the - // default. Omit to offer every backend the Workspace has; pass - // `false` for no exec tool. - exec?: ExecBackends | false; + // The backends `exec` may run on, each with a note for the model + // ("" for none). The first is the default. Omit to offer every + // backend the Workspace has. `{}` means no exec tool. + exec?: ExecBackends; /** * @deprecated Use `exec`. `{ backends: { id: { description } }, * defaultBackend }` becomes `exec: { id: description }` with the @@ -56,9 +55,9 @@ export function createAITools(options: CreateAIToolsOptions): ToolSet { tools.delete = createDeleteTool({ store }); const runtime = options.workspace.runtime; - if (runtime !== undefined && options.exec !== false) { - const exec = execOptions(options); - if (exec.backends !== undefined || (runtime.backendIds?.().length ?? 0) > 0) { + if (runtime !== undefined) { + const exec = execOptions(options, runtime); + if (Object.keys(exec.backends).length > 0) { tools.exec = createExecTool({ workspace: { runtime }, ...exec }); } } @@ -71,18 +70,22 @@ export function createAITools(options: CreateAIToolsOptions): ToolSet { } // Turn `exec`, or the deprecated `shell`, into createExecTool options. -function execOptions(options: CreateAIToolsOptions): Omit { - if (options.shell === undefined) { - return options.exec === undefined || options.exec === false ? {} : { backends: options.exec }; +function execOptions( + options: CreateAIToolsOptions, + runtime: ExecWorkspaceLike["runtime"], +): Omit & { backends: ExecBackends } { + if (options.shell !== undefined) { + const { backends, defaultBackend, ...limits } = options.shell; + const ids = Object.keys(backends); + const ordered = + defaultBackend === undefined + ? ids + : [defaultBackend, ...ids.filter((id) => id !== defaultBackend)]; + return { + ...limits, + backends: Object.fromEntries(ordered.map((id) => [id, backends[id]?.description ?? ""])), + }; } - const { backends, defaultBackend, ...limits } = options.shell; - const ids = Object.keys(backends); - const ordered = - defaultBackend === undefined - ? ids - : [defaultBackend, ...ids.filter((id) => id !== defaultBackend)]; - return { - ...limits, - backends: Object.fromEntries(ordered.map((id) => [id, backends[id]?.description ?? true])), - }; + if (options.exec !== undefined) return { backends: options.exec }; + return { backends: Object.fromEntries((runtime.backendIds?.() ?? []).map((id) => [id, ""])) }; } diff --git a/packages/computer/src/tools/exec.ts b/packages/computer/src/tools/exec.ts index af2df108..60679ef1 100644 --- a/packages/computer/src/tools/exec.ts +++ b/packages/computer/src/tools/exec.ts @@ -76,11 +76,12 @@ export interface ExecWorkspaceLike { } /** - * Which backends the exec tool may run on: a list of backend ids, or a - * map from id to text shown to the model before the backend's own - * description (`true` for none). The first entry is the default. + * The backends the exec tool may run on, each with a note for the + * model: `{ "worker-javascript": "Use for data work." }`. The note comes + * before the backend's own description; use `""` for none. The first + * backend is the default. */ -export type ExecBackends = readonly string[] | Readonly>; +export type ExecBackends = Readonly>; export interface ExecToolOptions { workspace: ExecWorkspaceLike; @@ -351,13 +352,8 @@ function selectBackends( throw new Error("createExecTool: pass `backends`; this workspace cannot list its backends"); } selected = known.map((id) => ({ id, guidance: undefined })); - } else if (isBackendList(backends)) { - selected = backends.map((id) => ({ id, guidance: undefined })); } else { - selected = Object.entries(backends).map(([id, text]) => ({ - id, - guidance: text === true ? undefined : text, - })); + selected = Object.entries(backends).map(([id, note]) => ({ id, guidance: note })); } const unknown = known === undefined ? [] : selected.filter((b) => !known.includes(b.id)); if (unknown.length > 0) { @@ -368,10 +364,6 @@ function selectBackends( return selected; } -function isBackendList(backends: ExecBackends): backends is readonly string[] { - return Array.isArray(backends); -} - function commandHint(backends: readonly DescribedBackend[]): string { if (backends.every((backend) => backend.callable)) return "Module source to run."; if (backends.every((backend) => !backend.callable)) { From ddcedde9131b13c6672acf8be1a1c50d1450a161 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Thu, 1 Oct 2026 10:57:31 +0100 Subject: [PATCH 3/7] computer: Take exec backend options as objects 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. --- .changeset/exec-tool-options.md | 4 +-- docs/09_tool_interface.md | 14 +++++++---- docs/17_isolate_javascript.md | 2 +- examples/celld/src/index.ts | 28 +++++++++++---------- examples/mcp/src/server.ts | 34 ++++++++++++++------------ examples/rlm/worker/executor-tool.ts | 6 +++-- packages/computer/README.md | 4 +-- packages/computer/src/tools/ai.test.ts | 13 ++++++---- packages/computer/src/tools/ai.ts | 18 +++++++------- packages/computer/src/tools/exec.ts | 21 +++++++++++----- packages/computer/src/tools/index.ts | 1 + 11 files changed, 85 insertions(+), 60 deletions(-) diff --git a/.changeset/exec-tool-options.md b/.changeset/exec-tool-options.md index 83479857..14e87f26 100644 --- a/.changeset/exec-tool-options.md +++ b/.changeset/exec-tool-options.md @@ -2,8 +2,8 @@ "@cloudflare/computer": minor --- -`createAITools` takes an `exec` option that lists the backends the model can use, each with a note for the model: `exec: { "worker-javascript": "Use for data work." }`. Leave it out to use every backend the Workspace has. The first backend is the default, a note can be `""`, and `exec: {}` means no exec tool. `createExecTool` takes the same map as `backends`, and `defaultBackend` goes away. +`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. The first backend is the default, `{}` 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. `WorkerShellBackend` and `CloudflareContainerBackend` 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: { id: { description } }, defaultBackend }` becomes `exec: { id: description }` with the default first. Output limits stay on `createExecTool`. +`shell` still works and is deprecated. `shell: { backends, defaultBackend }` becomes `exec: backends` with the default first. Output limits stay on `createExecTool`. diff --git a/docs/09_tool_interface.md b/docs/09_tool_interface.md index cf73ae4a..a6d99a90 100644 --- a/docs/09_tool_interface.md +++ b/docs/09_tool_interface.md @@ -55,14 +55,18 @@ export class Agent { Pass the returned AI SDK `ToolSet` to `generateText`, `streamText`, or an agent framework hook such as `getTools()`. -`exec` lists the backends the model can use, each with a note for the model. Leave it out to use every backend. +`exec` lists the backends the model can use, keyed by backend id. Leave it out to use every backend. ```ts createAITools({ workspace }); // every backend -createAITools({ workspace, exec: { "worker-javascript": "Use for data work." } }); // just this one +createAITools({ workspace, exec: { "worker-javascript": {} } }); // just this one +createAITools({ + workspace, + exec: { "worker-javascript": { description: "Use for data work." } }, // with your own text +}); ``` -Each backend also describes itself, so a note can be `""`. The first backend is the default, and `exec: {}` means no exec tool. With one backend, `exec` has no `backend` argument and always runs there. +Each backend describes itself, and a `description` you pass comes first. The first backend is the default, and `exec: {}` means no exec tool. With one backend, `exec` has no `backend` argument and always runs there. ## `createAITools` @@ -86,8 +90,8 @@ createAITools({ | `read` | default caps | Options passed to `createReadTool`. | | `write` | default caps | Options passed to `createWriteTool`. | | `edit` | default caps | Options passed to `createEditTool`. | -| `exec` | every backend | Backend id to a note for the model (`""` for none). The first is the default. `{}` omits `exec`. | -| `shell` | omitted | Deprecated. `{ backends: { id: { description } }, defaultBackend }` becomes `exec: { id: description }` with the default first. | +| `exec` | every backend | Backend id to `{ description? }`. The first is the default. `{}` omits `exec`. | +| `shell` | omitted | Deprecated. `{ backends, defaultBackend }` becomes `exec: backends` with the default first. | ## `read` diff --git a/docs/17_isolate_javascript.md b/docs/17_isolate_javascript.md index 3b832c81..6e3f351d 100644 --- a/docs/17_isolate_javascript.md +++ b/docs/17_isolate_javascript.md @@ -265,7 +265,7 @@ this.workspace = new Workspace({ }); // Offer only the JavaScript backend; the container is reached through ws:container. -const tools = createAITools({ workspace: this.workspace, exec: { "worker-javascript": "" } }); +const tools = createAITools({ workspace: this.workspace, exec: { "worker-javascript": {} } }); ``` ```js diff --git a/examples/celld/src/index.ts b/examples/celld/src/index.ts index b5185c68..badbd6ef 100644 --- a/examples/celld/src/index.ts +++ b/examples/celld/src/index.ts @@ -50,19 +50,21 @@ export class CelldAgent extends withWorkspace(CelldAgentBase, (self) => { ...(this.bindings.LOADER ? { exec: { - [CELLD_JAVASCRIPT_BACKEND_ID]: [ - "Runs a complete JavaScript module in a celld Dynamic Worker with structured input and output.", - "Pass module source, not a filename or bare script. The module must have a default export. Export a function to receive `(input, ctx)` and return structured output.", - "", - "```js", - "export default async function main(input, ctx) {", - ' console.log("cwd:", ctx.cwd);', - " return { received: input };", - "}", - "```", - "", - "The loaded worker cannot access the Workspace filesystem. Use read, write, edit, ls, find, grep, and delete outside exec.", - ].join("\n"), + [CELLD_JAVASCRIPT_BACKEND_ID]: { + description: [ + "Runs a complete JavaScript module in a celld Dynamic Worker with structured input and output.", + "Pass module source, not a filename or bare script. The module must have a default export. Export a function to receive `(input, ctx)` and return structured output.", + "", + "```js", + "export default async function main(input, ctx) {", + ' console.log("cwd:", ctx.cwd);', + " return { received: input };", + "}", + "```", + "", + "The loaded worker cannot access the Workspace filesystem. Use read, write, edit, ls, find, grep, and delete outside exec.", + ].join("\n"), + }, }, } : {}), diff --git a/examples/mcp/src/server.ts b/examples/mcp/src/server.ts index 78ee9c81..9df92ad3 100644 --- a/examples/mcp/src/server.ts +++ b/examples/mcp/src/server.ts @@ -12,21 +12,25 @@ export async function createComputerMCPServer(workspace: WorkspaceClient, loader workspace, assets: false, exec: { - "worker-shell": - "just-bash in an isolated Dynamic Worker. Starts quickly, " + - "does not boot a container, and has no ambient outbound network. " + - "Use it for common shell commands, quick file inspection, and " + - "text transformations. Its built-in git command supports clone, " + - "status, diff, and log; clone accepts HTTPS URLs through the " + - "durable workspace. Prefer the dedicated read, write, and edit " + - "tools for file operations. Cannot run npm, Node.js, Python, " + - "package managers, or arbitrary native binaries.", - "container-shell": - "Full Debian Linux in a Cloudflare Container with Node.js, npm, " + - "git, package management, native binaries, and outbound network. " + - "Use it for dependency installation, builds, tests, or commands " + - "that worker-shell cannot run. Cold starts more slowly because " + - "the container must boot; prefer worker-shell for simple tasks.", + "worker-shell": { + description: + "just-bash in an isolated Dynamic Worker. Starts quickly, " + + "does not boot a container, and has no ambient outbound network. " + + "Use it for common shell commands, quick file inspection, and " + + "text transformations. Its built-in git command supports clone, " + + "status, diff, and log; clone accepts HTTPS URLs through the " + + "durable workspace. Prefer the dedicated read, write, and edit " + + "tools for file operations. Cannot run npm, Node.js, Python, " + + "package managers, or arbitrary native binaries.", + }, + "container-shell": { + description: + "Full Debian Linux in a Cloudflare Container with Node.js, npm, " + + "git, package management, native binaries, and outbound network. " + + "Use it for dependency installation, builds, tests, or commands " + + "that worker-shell cannot run. Cold starts more slowly because " + + "the container must boot; prefer worker-shell for simple tasks.", + }, }, }); diff --git a/examples/rlm/worker/executor-tool.ts b/examples/rlm/worker/executor-tool.ts index 921616f1..ec5e53dd 100644 --- a/examples/rlm/worker/executor-tool.ts +++ b/examples/rlm/worker/executor-tool.ts @@ -14,8 +14,10 @@ export function createExecutorTool( const executor = createExecTool({ workspace, backends: { - [backend]: - "Callable isolated JavaScript. The command must be a complete ES module with a default async function.", + [backend]: { + description: + "Callable isolated JavaScript. The command must be a complete ES module with a default async function.", + }, }, maxBytes: 16 * 1024, streamMaxBytes: 16 * 1024, diff --git a/packages/computer/README.md b/packages/computer/README.md index be7ac0ad..29bc211c 100644 --- a/packages/computer/README.md +++ b/packages/computer/README.md @@ -278,8 +278,8 @@ import { createAITools } from "@cloudflare/computer/tools"; const tools = createAITools({ workspace, read: { maxBytes: 32 * 1024, maxLines: 800 }, - // The backends the model can use, each with a note. Omit for every backend. - exec: { shell: "Try this first.", container: "" }, + // The backends the model can use. Omit for every backend. + exec: { shell: { description: "Try this first." }, container: {} }, }); ``` diff --git a/packages/computer/src/tools/ai.test.ts b/packages/computer/src/tools/ai.test.ts index 9f6a3ab7..da1817c5 100644 --- a/packages/computer/src/tools/ai.test.ts +++ b/packages/computer/src/tools/ai.test.ts @@ -1369,10 +1369,10 @@ describe("createAITools exec tool", () => { new WorkerJavaScriptBackend({ loader: { load: () => ({ getEntrypoint: () => ({}) }) } }), ], }); - const listed = createAITools({ workspace, exec: { "worker-javascript": "" } }); + const listed = createAITools({ workspace, exec: { "worker-javascript": {} } }); const mapped = createAITools({ workspace, - exec: { "worker-javascript": "Use for data work.", shell: "" }, + exec: { "worker-javascript": { description: "Use for data work." }, shell: {} }, }); expect(inputProperties(listed.exec)).not.toContain("backend"); @@ -1565,7 +1565,7 @@ describe("createAITools exec tool", () => { }); it("rejects a backend the workspace does not have", () => { - expect(() => createAITools({ workspace: makeWorkspace(), exec: { missing: "" } })).toThrow( + expect(() => createAITools({ workspace: makeWorkspace(), exec: { missing: {} } })).toThrow( /unknown backend "missing"/, ); }); @@ -1778,7 +1778,7 @@ describe("createAITools callable exec", () => { }, }, }; - const tools = createAITools({ workspace, exec: { shell: "" } }); + const tools = createAITools({ workspace, exec: { shell: {} } }); expect(toolDescription(tools.exec)).toContain("Runs shell commands."); }); @@ -1891,7 +1891,10 @@ describe("createAITools exec with one backend", () => { it("uses the first listed backend as the default, and the deprecated shell default first", async () => { const { calls, workspace } = recordingWorkspace(false); - const listed = createAITools({ workspace, exec: { container: "Linux.", shell: "Fast." } }); + const listed = createAITools({ + workspace, + exec: { container: { description: "Linux." }, shell: { description: "Fast." } }, + }); const legacy = createAITools({ workspace, shell: { diff --git a/packages/computer/src/tools/ai.ts b/packages/computer/src/tools/ai.ts index dd8d71b8..428d7d3a 100644 --- a/packages/computer/src/tools/ai.ts +++ b/packages/computer/src/tools/ai.ts @@ -22,20 +22,20 @@ export interface CreateAIToolsOptions { read?: Omit; write?: Omit; edit?: Omit; - // The backends `exec` may run on, each with a note for the model - // ("" for none). The first is the default. Omit to offer every - // backend the Workspace has. `{}` means no exec tool. + // The backends `exec` may run on, keyed by id, each with an optional + // description for the model. The first is the default. Omit to offer + // every backend the Workspace has; `{}` means no exec tool. exec?: ExecBackends; /** - * @deprecated Use `exec`. `{ backends: { id: { description } }, - * defaultBackend }` becomes `exec: { id: description }` with the - * default listed first. Output limits move to `createExecTool`. + * @deprecated Use `exec`. `{ backends, defaultBackend }` becomes + * `exec: backends` with the default listed first. Output limits move + * to `createExecTool`. */ shell?: LegacyShellOptions; } interface LegacyShellOptions extends Omit { - backends: Record; + backends: ExecBackends; defaultBackend?: string; } @@ -83,9 +83,9 @@ function execOptions( : [defaultBackend, ...ids.filter((id) => id !== defaultBackend)]; return { ...limits, - backends: Object.fromEntries(ordered.map((id) => [id, backends[id]?.description ?? ""])), + backends: Object.fromEntries(ordered.map((id) => [id, backends[id] ?? {}])), }; } if (options.exec !== undefined) return { backends: options.exec }; - return { backends: Object.fromEntries((runtime.backendIds?.() ?? []).map((id) => [id, ""])) }; + return { backends: Object.fromEntries((runtime.backendIds?.() ?? []).map((id) => [id, {}])) }; } diff --git a/packages/computer/src/tools/exec.ts b/packages/computer/src/tools/exec.ts index 60679ef1..db62b4dd 100644 --- a/packages/computer/src/tools/exec.ts +++ b/packages/computer/src/tools/exec.ts @@ -75,13 +75,19 @@ export interface ExecWorkspaceLike { }; } +/** Options for one backend the exec tool may run on. */ +export interface ExecBackendOptions { + /** Shown to the model before the backend's own description. */ + readonly description?: string; +} + /** - * The backends the exec tool may run on, each with a note for the - * model: `{ "worker-javascript": "Use for data work." }`. The note comes - * before the backend's own description; use `""` for none. The first - * backend is the default. + * The backends the exec tool may run on, keyed by backend id: + * `{ "worker-javascript": { description: "Use for data work." } }`. + * Pass `{}` for a backend that needs nothing beyond its own + * description. The first backend is the default. */ -export type ExecBackends = Readonly>; +export type ExecBackends = Readonly>; export interface ExecToolOptions { workspace: ExecWorkspaceLike; @@ -353,7 +359,10 @@ function selectBackends( } selected = known.map((id) => ({ id, guidance: undefined })); } else { - selected = Object.entries(backends).map(([id, note]) => ({ id, guidance: note })); + selected = Object.entries(backends).map(([id, backend]) => ({ + id, + guidance: backend.description, + })); } const unknown = known === undefined ? [] : selected.filter((b) => !known.includes(b.id)); if (unknown.length > 0) { diff --git a/packages/computer/src/tools/index.ts b/packages/computer/src/tools/index.ts index a814da5d..3066fda2 100644 --- a/packages/computer/src/tools/index.ts +++ b/packages/computer/src/tools/index.ts @@ -1,6 +1,7 @@ export { type CreateAIToolsOptions, createAITools } from "./ai.js"; export { createExecTool, + type ExecBackendOptions, type ExecBackends, type ExecRuntimeHandle, type ExecStreamEvent, From fcc86e771c1f3e6c136d6d0e14406d52915b48c6 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Thu, 1 Oct 2026 10:59:34 +0100 Subject: [PATCH 4/7] computer: Move createAITools to @cloudflare/computer/tools/ai-sdk 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. --- .changeset/exec-tool-options.md | 2 ++ docs/09_tool_interface.md | 4 ++-- docs/10_project_layout.md | 7 ++++--- docs/README.md | 3 ++- examples/celld/README.md | 2 +- examples/celld/src/index.ts | 2 +- examples/mcp/src/server.ts | 2 +- examples/think/README.md | 9 +++++---- examples/think/src/agent.ts | 2 +- packages/computer/README.md | 9 +++++---- packages/computer/package.json | 4 ++++ packages/computer/rolldown.config.ts | 1 + .../computer/src/tools/{ai.test.ts => ai-sdk.test.ts} | 2 +- packages/computer/src/tools/{ai.ts => ai-sdk.ts} | 10 ++++++++++ packages/computer/src/tools/index.ts | 1 - 15 files changed, 40 insertions(+), 20 deletions(-) rename packages/computer/src/tools/{ai.test.ts => ai-sdk.test.ts} (99%) rename packages/computer/src/tools/{ai.ts => ai-sdk.ts} (88%) diff --git a/.changeset/exec-tool-options.md b/.changeset/exec-tool-options.md index 14e87f26..c6b01c0d 100644 --- a/.changeset/exec-tool-options.md +++ b/.changeset/exec-tool-options.md @@ -7,3 +7,5 @@ `WorkerShellBackend` and `CloudflareContainerBackend` 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, defaultBackend }` becomes `exec: backends` with the default first. 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"`. diff --git a/docs/09_tool_interface.md b/docs/09_tool_interface.md index a6d99a90..5c609d71 100644 --- a/docs/09_tool_interface.md +++ b/docs/09_tool_interface.md @@ -1,6 +1,6 @@ # 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: @@ -30,7 +30,7 @@ The tools wrap three Workspace surfaces: ```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; diff --git a/docs/10_project_layout.md b/docs/10_project_layout.md index 8718132f..eb558b43 100644 --- a/docs/10_project_layout.md +++ b/docs/10_project_layout.md @@ -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). diff --git a/docs/README.md b/docs/README.md index 29b0acbf..fca2cb88 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,7 +22,7 @@ It provides: - Pluggable execution backends selected through `workspace.runtime`: a Cloudflare Container shell, a just-bash Dynamic Worker, or an isolated ECMAScript-module Dynamic Worker. - Isolated JavaScript with structured input/results, durable relative imports, configured libraries, durable `node:fs/promises`, host modules such as `ws:git` and `ws:container`, and managed execution records. - Workspace constructable without a backend, for filesystem-only use cases. - - Out-of-the-box AI SDK tools for `@cloudflare/agents` through `@cloudflare/computer/tools`. + - Out-of-the-box AI SDK tools for `@cloudflare/agents` through `createAITools()` in `@cloudflare/computer/tools/ai-sdk`. It comes with the following limitations: @@ -52,6 +52,7 @@ The package ships several entrypoints: | `@cloudflare/computer/modules/git` | `createGitModule()` for `ws:git`: confined Git from isolate JavaScript. | | `@cloudflare/computer/modules/artifacts` | `createArtifactsModule()` for `ws:artifacts`: Artifacts from isolate JavaScript. | | `@cloudflare/computer/tools` | AI SDK tools for agents: read, write, edit, ls, optional exec, and optional publish. | +| `@cloudflare/computer/tools/ai-sdk` | `createAITools()`: the AI SDK tool set for a Workspace. | A consumer that only uses the container backend never imports the worker subpath, so the just-bash payload tree-shakes away. diff --git a/examples/celld/README.md b/examples/celld/README.md index 7eec14a0..5d2d2220 100644 --- a/examples/celld/README.md +++ b/examples/celld/README.md @@ -128,7 +128,7 @@ the message or `CELLD_EXPECT` to use a different expected phrase. ## Workspace tools -The agent receives these tools from `@cloudflare/computer/tools`: +The agent receives these tools from `createAITools()` in `@cloudflare/computer/tools/ai-sdk`: | Tool | Purpose | | --- | --- | diff --git a/examples/celld/src/index.ts b/examples/celld/src/index.ts index badbd6ef..ab9d23f1 100644 --- a/examples/celld/src/index.ts +++ b/examples/celld/src/index.ts @@ -5,7 +5,7 @@ import { type WorkspaceRuntimeLoader, withWorkspace, } from "@cloudflare/computer"; -import { createAITools } from "@cloudflare/computer/tools"; +import { createAITools } from "@cloudflare/computer/tools/ai-sdk"; import { routeAgentRequest } from "agents"; import { convertToModelMessages, isStepCount, streamText } from "ai"; import { createWorkersAI } from "workers-ai-provider"; diff --git a/examples/mcp/src/server.ts b/examples/mcp/src/server.ts index 9df92ad3..52774350 100644 --- a/examples/mcp/src/server.ts +++ b/examples/mcp/src/server.ts @@ -1,7 +1,7 @@ import { DynamicWorkerExecutor } from "@cloudflare/codemode"; import { codeMcpServer } from "@cloudflare/codemode/mcp"; import type { WorkspaceClient } from "@cloudflare/computer"; -import { createAITools } from "@cloudflare/computer/tools"; +import { createAITools } from "@cloudflare/computer/tools/ai-sdk"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { ToolSet } from "ai"; diff --git a/examples/think/README.md b/examples/think/README.md index 523b082a..32d88c28 100644 --- a/examples/think/README.md +++ b/examples/think/README.md @@ -24,7 +24,7 @@ would use, so no bespoke HTTP route or transport is involved. [think]: https://www.npmjs.com/package/@cloudflare/think [workspace]: ../../packages/computer -[tools]: ../../packages/computer/src/tools +[tools]: ../../packages/computer/src/tools/ai-sdk.ts [aisdk7]: https://vercel.com/blog/ai-sdk-7 ## Shape @@ -53,9 +53,10 @@ model, a Workspace, and the workspace tools. ## Tools The tools come from `createAITools()` in -[`@cloudflare/computer/tools`][tools]. This example enables the file -tools and opts into `exec` by passing a shell backend description; it -does not configure the assets publisher, so `publish` is not offered. +[`@cloudflare/computer/tools/ai-sdk`][tools]. This example offers the +file tools and an `exec` tool over both backends, each of which +describes itself to the model. It does not configure the assets +publisher, so `publish` is not offered. | Tool | What it does | | ------- | --------------------------------------------------------- | diff --git a/examples/think/src/agent.ts b/examples/think/src/agent.ts index edf4108a..c706d3f4 100644 --- a/examples/think/src/agent.ts +++ b/examples/think/src/agent.ts @@ -37,7 +37,7 @@ import { withWorkspaceContainer, } from "@cloudflare/computer/backends/container"; import { WorkerShellBackend } from "@cloudflare/computer/backends/worker-shell"; -import { createAITools } from "@cloudflare/computer/tools"; +import { createAITools } from "@cloudflare/computer/tools/ai-sdk"; import { Think } from "@cloudflare/think"; import type { ToolSet } from "ai"; import { createWorkersAI } from "workers-ai-provider"; diff --git a/packages/computer/README.md b/packages/computer/README.md index 29bc211c..0908b242 100644 --- a/packages/computer/README.md +++ b/packages/computer/README.md @@ -265,15 +265,15 @@ to a named one — see [Multiple backends](#multiple-backends). ## Tools for agents -`@cloudflare/computer/tools` ships AI SDK tools that wrap the Workspace +`@cloudflare/computer/tools/ai-sdk` ships `createAITools()`, AI SDK tools that wrap the Workspace surfaces, ready to hand to `generateText`, `streamText`, or an agent framework's `getTools()`. The default set is `read`, `ls`, `find`, -`grep`, `write`, `edit`, and `delete`; `exec` and `publish` are added -when you configure them. Read-only mode keeps `read`, `ls`, `find`, and +`grep`, `write`, `edit`, and `delete`, plus `exec` when the Workspace +has a backend and `publish` when assets are configured. Read-only mode keeps `read`, `ls`, `find`, and `grep`. ```ts -import { createAITools } from "@cloudflare/computer/tools"; +import { createAITools } from "@cloudflare/computer/tools/ai-sdk"; const tools = createAITools({ workspace, @@ -418,6 +418,7 @@ on a computerd instance. | `@cloudflare/computer/modules/git` | `createGitModule()` for `ws:git`: confined Git from isolate JavaScript. | | `@cloudflare/computer/modules/artifacts` | `createArtifactsModule()` for `ws:artifacts`: Artifacts from isolate JavaScript. | | `@cloudflare/computer/tools` | AI SDK tools for agents: `read`, `ls`, `find`, `grep`, `write`, `edit`, `delete`, and optional `exec` and `publish`. | +| `@cloudflare/computer/tools/ai-sdk` | `createAITools()`: the AI SDK tool set for a Workspace. | | `@cloudflare/computer/git` | Opt-in `isomorphic-git` glue for checkouts inside the workspace. | | `@cloudflare/computer/assets` | `createAssets` — share a workspace file to R2 as a presigned URL. | | `@cloudflare/computer/artifacts` | `createArtifact` and its CLI, an optionally session-scoped wrapper over the Cloudflare Artifacts binding. | diff --git a/packages/computer/package.json b/packages/computer/package.json index 8bbac49d..d62c738e 100644 --- a/packages/computer/package.json +++ b/packages/computer/package.json @@ -47,6 +47,10 @@ "types": "./dist/tools/index.d.ts", "import": "./dist/tools/index.js" }, + "./tools/ai-sdk": { + "types": "./dist/tools/ai-sdk.d.ts", + "import": "./dist/tools/ai-sdk.js" + }, "./backends/container": { "types": "./dist/backends/container/index.d.ts", "import": "./dist/backends/container/index.js" diff --git a/packages/computer/rolldown.config.ts b/packages/computer/rolldown.config.ts index 7179b638..67d5d7b9 100644 --- a/packages/computer/rolldown.config.ts +++ b/packages/computer/rolldown.config.ts @@ -31,6 +31,7 @@ export default defineConfig({ "artifacts/index": "src/artifacts/index.ts", "assets/index": "src/assets/index.ts", "tools/index": "src/tools/index.ts", + "tools/ai-sdk": "src/tools/ai-sdk.ts", "modules/container": "src/modules/container.ts", "modules/git": "src/modules/git.ts", "modules/artifacts": "src/modules/artifacts.ts", diff --git a/packages/computer/src/tools/ai.test.ts b/packages/computer/src/tools/ai-sdk.test.ts similarity index 99% rename from packages/computer/src/tools/ai.test.ts rename to packages/computer/src/tools/ai-sdk.test.ts index da1817c5..50b36f9f 100644 --- a/packages/computer/src/tools/ai.test.ts +++ b/packages/computer/src/tools/ai-sdk.test.ts @@ -5,8 +5,8 @@ import { WorkerJavaScriptBackend } from "../backends/worker-javascript/worker-ja import { createGitModule } from "../modules/git.js"; import type { WorkspaceRuntimeExecHandle, WorkspaceRuntimeResult } from "../runtime/types.js"; import { Workspace } from "../workspace.js"; +import { createAITools } from "./ai-sdk.js"; import { - createAITools, createDeleteTool, createEditTool, createFindTool, diff --git a/packages/computer/src/tools/ai.ts b/packages/computer/src/tools/ai-sdk.ts similarity index 88% rename from packages/computer/src/tools/ai.ts rename to packages/computer/src/tools/ai-sdk.ts index 428d7d3a..67f400a5 100644 --- a/packages/computer/src/tools/ai.ts +++ b/packages/computer/src/tools/ai-sdk.ts @@ -15,6 +15,7 @@ import { type WorkspaceLike as FileWorkspaceLike, WorkspaceFileStore } from "./f import { createWriteTool, type WriteToolOptions } from "./fs/write.js"; import { createPublishTool, type PublishWorkspaceLike } from "./publish.js"; +/** Options for {@link createAITools}. */ export interface CreateAIToolsOptions { workspace: FileWorkspaceLike & Partial & Partial; readonly?: boolean; @@ -39,6 +40,15 @@ interface LegacyShellOptions extends Omit Date: Thu, 1 Oct 2026 11:12:35 +0100 Subject: [PATCH 5/7] computer: Make the model name an exec backend 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. --- .changeset/exec-tool-options.md | 4 +-- docs/09_tool_interface.md | 8 +++--- packages/computer/src/tools/ai-sdk.test.ts | 27 ++++++++---------- packages/computer/src/tools/ai-sdk.ts | 20 ++++---------- packages/computer/src/tools/exec.ts | 32 +++++++++++++--------- 5 files changed, 43 insertions(+), 48 deletions(-) diff --git a/.changeset/exec-tool-options.md b/.changeset/exec-tool-options.md index c6b01c0d..6dd792bd 100644 --- a/.changeset/exec-tool-options.md +++ b/.changeset/exec-tool-options.md @@ -2,10 +2,10 @@ "@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. The first backend is the default, `{}` 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. +`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 `CloudflareContainerBackend` 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, defaultBackend }` becomes `exec: backends` with the default first. Output limits stay on `createExecTool`. +`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"`. diff --git a/docs/09_tool_interface.md b/docs/09_tool_interface.md index 5c609d71..6a9543a7 100644 --- a/docs/09_tool_interface.md +++ b/docs/09_tool_interface.md @@ -66,7 +66,7 @@ createAITools({ }); ``` -Each backend describes itself, and a `description` you pass comes first. The first backend is the default, and `exec: {}` means no exec tool. With one backend, `exec` has no `backend` argument and always runs there. +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` @@ -90,8 +90,8 @@ createAITools({ | `read` | default caps | Options passed to `createReadTool`. | | `write` | default caps | Options passed to `createWriteTool`. | | `edit` | default caps | Options passed to `createEditTool`. | -| `exec` | every backend | Backend id to `{ description? }`. The first is the default. `{}` omits `exec`. | -| `shell` | omitted | Deprecated. `{ backends, defaultBackend }` becomes `exec: backends` with the default first. | +| `exec` | every backend | Backend id to `{ description? }`. `{}` omits `exec`. | +| `shell` | omitted | Deprecated. `{ backends }` becomes `exec: backends`; `defaultBackend` is ignored. | ## `read` @@ -253,7 +253,7 @@ The tool offers only the arguments that can work: | --- | --- | | 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 | +| 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. diff --git a/packages/computer/src/tools/ai-sdk.test.ts b/packages/computer/src/tools/ai-sdk.test.ts index 50b36f9f..66bed393 100644 --- a/packages/computer/src/tools/ai-sdk.test.ts +++ b/packages/computer/src/tools/ai-sdk.test.ts @@ -1342,7 +1342,7 @@ describe("createAITools exec tool", () => { expect(createAITools({ workspace: withBackend, readonly: true }).exec).toBeUndefined(); }); - it("offers every workspace backend by default, the workspace default first", () => { + it("offers every workspace backend by default", () => { const workspace = new Workspace({ storage: new SQLiteTestStorage(), backends: [ @@ -1353,10 +1353,12 @@ describe("createAITools exec tool", () => { const tools = createAITools({ workspace }); const schema = z.toJSONSchema(inputSchema(tools.exec)) as { properties: { backend?: { enum?: string[] } }; + required?: string[]; }; expect(schema.properties.backend?.enum).toEqual(["shell", "worker-javascript"]); - expect(toolDescription(tools.exec)).toContain('Default backend: "shell"'); + expect(schema.required).toContain("backend"); + expect(toolDescription(tools.exec)).not.toMatch(/default backend/i); expect(toolDescription(tools.exec)).toContain('- "shell": Runs shell commands.'); expect(toolDescription(tools.exec)).toContain("ECMAScript module source"); }); @@ -1377,7 +1379,6 @@ describe("createAITools exec tool", () => { expect(inputProperties(listed.exec)).not.toContain("backend"); expect(toolDescription(listed.exec)).not.toContain('"shell"'); - expect(toolDescription(mapped.exec)).toContain('Default backend: "worker-javascript"'); expect(toolDescription(mapped.exec)).toContain("Use for data work.\n\n`command` is ECMAScript"); }); @@ -1889,23 +1890,19 @@ describe("createAITools exec with one backend", () => { expect(inputProperties(tools.exec)).toEqual(["backend", "command", "cwd", "env", "input"]); }); - it("uses the first listed backend as the default, and the deprecated shell default first", async () => { + it("requires the model to name a backend when there is a choice", async () => { const { calls, workspace } = recordingWorkspace(false); - const listed = createAITools({ + const tools = createAITools({ workspace, exec: { container: { description: "Linux." }, shell: { description: "Fast." } }, }); - const legacy = createAITools({ - workspace, - shell: { - defaultBackend: "shell", - backends: { container: { description: "Linux." }, shell: { description: "Fast." } }, - }, - }); - await executeTool(listed.exec, { command: "ls" }); - await executeTool(legacy.exec, { command: "ls" }); - expect(calls.map((call) => call.backend)).toEqual(["container", "shell"]); + expect(() => inputSchema(tools.exec).parse({ command: "ls" })).toThrow(); + await expect(executeTool(tools.exec, { command: "ls" })).resolves.toMatchObject({ + error: "Name a backend to run on.", + }); + await executeTool(tools.exec, { command: "ls", backend: "shell" }); + expect(calls.map((call) => call.backend)).toEqual(["shell"]); }); }); diff --git a/packages/computer/src/tools/ai-sdk.ts b/packages/computer/src/tools/ai-sdk.ts index 67f400a5..f0593dac 100644 --- a/packages/computer/src/tools/ai-sdk.ts +++ b/packages/computer/src/tools/ai-sdk.ts @@ -24,13 +24,13 @@ export interface CreateAIToolsOptions { write?: Omit; edit?: Omit; // The backends `exec` may run on, keyed by id, each with an optional - // description for the model. The first is the default. Omit to offer + // description for the model. Omit to offer // every backend the Workspace has; `{}` means no exec tool. exec?: ExecBackends; /** - * @deprecated Use `exec`. `{ backends, defaultBackend }` becomes - * `exec: backends` with the default listed first. Output limits move - * to `createExecTool`. + * @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`. */ shell?: LegacyShellOptions; } @@ -85,16 +85,8 @@ function execOptions( runtime: ExecWorkspaceLike["runtime"], ): Omit & { backends: ExecBackends } { if (options.shell !== undefined) { - const { backends, defaultBackend, ...limits } = options.shell; - const ids = Object.keys(backends); - const ordered = - defaultBackend === undefined - ? ids - : [defaultBackend, ...ids.filter((id) => id !== defaultBackend)]; - return { - ...limits, - backends: Object.fromEntries(ordered.map((id) => [id, backends[id] ?? {}])), - }; + const { backends, defaultBackend: _ignored, ...limits } = options.shell; + return { ...limits, backends }; } if (options.exec !== undefined) return { backends: options.exec }; return { backends: Object.fromEntries((runtime.backendIds?.() ?? []).map((id) => [id, {}])) }; diff --git a/packages/computer/src/tools/exec.ts b/packages/computer/src/tools/exec.ts index db62b4dd..08eb864a 100644 --- a/packages/computer/src/tools/exec.ts +++ b/packages/computer/src/tools/exec.ts @@ -69,8 +69,8 @@ export interface ExecWorkspaceLike { // language it runs and the modules that code can import. The tool // shows it after the caller's own text. describe?(id: string): string | undefined; - // Every registered backend id, default first. Used when the caller - // does not pick backends, and to reject an unknown id up front. + // Every registered backend id. Used when the caller does not pick + // backends, and to reject an unknown id up front. backendIds?(): string[]; }; } @@ -85,14 +85,15 @@ export interface ExecBackendOptions { * The backends the exec tool may run on, keyed by backend id: * `{ "worker-javascript": { description: "Use for data work." } }`. * Pass `{}` for a backend that needs nothing beyond its own - * description. The first backend is the default. + * description. */ export type ExecBackends = Readonly>; export interface ExecToolOptions { workspace: ExecWorkspaceLike; - // Omit to offer every backend the Workspace has, its default first. - // With exactly one backend the tool has no `backend` argument. + // Omit to offer every backend the Workspace has. With one backend + // the tool has no `backend` argument; with several the model must + // name one on every call. backends?: ExecBackends; // Per-snapshot display cap for each of stdout and stderr, in bytes. // Output past it is shown as a truncation marker. Defaults to 64 KiB. @@ -145,7 +146,6 @@ export function createExecTool(options: ExecToolOptions): Tool backend.id); const single = backendIds.length === 1; const backends = selected.map(({ id, guidance }) => { @@ -157,7 +157,7 @@ export function createExecTool(options: ExecToolOptions): Tool b.callable).map((b) => b.id)); - const description = describeTool(backends, defaultBackend); + const description = describeTool(backends); // Offer only the fields that can work: `backend` when there is a // choice, `input` when some backend accepts it. const shape: Record = { @@ -174,9 +174,8 @@ export function createExecTool(options: ExecToolOptions): Tool 0) { @@ -195,7 +194,14 @@ export function createExecTool(options: ExecToolOptions): Tool `- ${JSON.stringify(b.id)}${b.callable ? " (callable)" : ""}: ${b.text}`, ), "", - `Default backend: ${JSON.stringify(defaultBackend)}. Try this first for any command you're not sure about; if it fails with a "command not found" or a similar capability error, retry on a backend whose description covers the missing tool.`, + 'Name a backend on every call. If a command fails with a "command not found" or a similar capability error, retry on a backend whose description covers the missing tool.', `${SHELL_HINT} ${FILE_TOOLS_HINT}`, ...(callable.length === 0 ? [] @@ -346,7 +352,7 @@ function describeTool(backends: readonly DescribedBackend[], defaultBackend: str ].join("\n"); } -// Resolve the caller's choice to an ordered list, default first. +// Resolve the caller's choice to a list of backends. function selectBackends( backends: ExecBackends | undefined, runtime: ExecWorkspaceLike["runtime"], From 602192c73eb15acab90a607eba9d944ce8f6e47c Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Thu, 1 Oct 2026 11:13:48 +0100 Subject: [PATCH 6/7] examples/mcp: Name the backend in exec calls The exec tool now requires a backend when more than one is offered, and this example offers worker-shell and container-shell. --- examples/mcp/src/index.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/mcp/src/index.test.ts b/examples/mcp/src/index.test.ts index 189e062e..57d4035c 100644 --- a/examples/mcp/src/index.test.ts +++ b/examples/mcp/src/index.test.ts @@ -85,8 +85,11 @@ describe("Computer Code Mode MCP", () => { }); const file = await codemode.read({ path: "/workspace/message.txt" }); const listing = await codemode.ls({ path: "/workspace" }); - const shell = await codemode.exec({ command: "pwd" }); - const git = await codemode.exec({ command: "git init && git status --short" }); + const shell = await codemode.exec({ command: "pwd", backend: "worker-shell" }); + const git = await codemode.exec({ + command: "git init && git status --short", + backend: "worker-shell", + }); return { content: file.content, listed: listing.entries.some((entry) => entry.name === "message.txt"), From 65b176929128d0950ba7da7346edcb535d39d3cc Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Thu, 1 Oct 2026 11:16:25 +0100 Subject: [PATCH 7/7] examples/celld: Restore the wrangler cache file --- examples/celld/.wrangler/cache/cf.json | 70 +------------------------- 1 file changed, 1 insertion(+), 69 deletions(-) diff --git a/examples/celld/.wrangler/cache/cf.json b/examples/celld/.wrangler/cache/cf.json index b7282d4e..774bed0f 100644 --- a/examples/celld/.wrangler/cache/cf.json +++ b/examples/celld/.wrangler/cache/cf.json @@ -1,69 +1 @@ -{ - "httpProtocol": "HTTP/1.1", - "clientAcceptEncoding": "gzip, deflate, br", - "requestPriority": "", - "edgeRequestKeepAliveStatus": 1, - "requestHeaderNames": {}, - "clientTcpRtt": 18, - "clientQuicRtt": 0, - "colo": "DFW", - "asn": 8075, - "asOrganization": "Microsoft Limited", - "country": "US", - "isEUCountry": false, - "city": "Cheyenne", - "continent": "NA", - "region": "Wyoming", - "regionCode": "WY", - "timezone": "America/Denver", - "longitude": "-104.82025", - "latitude": "41.13998", - "postalCode": "82001", - "metroCode": "759", - "tlsVersion": "TLSv1.3", - "tlsCipher": "AEAD-AES256-GCM-SHA384", - "tlsClientRandom": "fk2JVeEAfBlrP/C+VZOSiSLmj9xN+c5P7Deo2tukmqM=", - "tlsClientCiphersSha1": "kXrN3VEKDdzz2cPKTQaKzpxVTxQ=", - "tlsClientExtensionsSha1": "1eY97BUYYO8vDaTfHQywB1pcNdM=", - "tlsClientExtensionsSha1Le": "u4wtEMFQBY18l3BzHAvORm+KGRw=", - "tlsExportedAuthenticator": { - "clientHandshake": "2f42c1b65266f736de9ab540b71507dcf00dd653d1b6612774008de57f0492b5e295b41bd3468c7634b880e61deaa0a3", - "serverHandshake": "343706c57562dfbaa23089d3966f4d64ba8e1ae3d7db043474ed644a0487b8545dbeae9a83f980f4b5cc54c4d9a04d49", - "clientFinished": "8c5bace08e1fa6903f2bdc37ea035442e4d944cd1402ea604a0b45454ec14b28fc7fb947ba3a8322df65b6abe23c5c40", - "serverFinished": "22f787324da16e80d1ae8311cf917d76f6000ecece618d6d489ef12802b1771e220fe49617f1b5074d984247142a7ff7" - }, - "tlsClientHelloLength": "1605", - "tlsClientAuth": { - "certPresented": "0", - "certVerified": "NONE", - "certRevoked": "0", - "certIssuerDN": "", - "certSubjectDN": "", - "certIssuerDNRFC2253": "", - "certSubjectDNRFC2253": "", - "certIssuerDNLegacy": "", - "certSubjectDNLegacy": "", - "certSerial": "", - "certIssuerSerial": "", - "certSKI": "", - "certIssuerSKI": "", - "certFingerprintSHA1": "", - "certFingerprintSHA256": "", - "certNotBefore": "", - "certNotAfter": "", - "certRFC9440": "", - "certRFC9440TooLarge": false, - "certChainRFC9440": "", - "certChainRFC9440TooLarge": false - }, - "verifiedBotCategory": "", - "edgeL4": { "deliveryRate": 238490 }, - "botManagement": { - "corporateProxy": false, - "verifiedBot": false, - "jsDetection": { "passed": false }, - "staticResource": false, - "detectionIds": {}, - "score": 99 - } -} +{"httpProtocol":"HTTP/1.1","clientAcceptEncoding":"gzip, deflate, br","requestPriority":"","edgeRequestKeepAliveStatus":1,"requestHeaderNames":{},"clientTcpRtt":18,"clientQuicRtt":0,"colo":"DFW","asn":8075,"asOrganization":"Microsoft Limited","country":"US","isEUCountry":false,"city":"Cheyenne","continent":"NA","region":"Wyoming","regionCode":"WY","timezone":"America/Denver","longitude":"-104.82025","latitude":"41.13998","postalCode":"82001","metroCode":"759","tlsVersion":"TLSv1.3","tlsCipher":"AEAD-AES256-GCM-SHA384","tlsClientRandom":"fk2JVeEAfBlrP/C+VZOSiSLmj9xN+c5P7Deo2tukmqM=","tlsClientCiphersSha1":"kXrN3VEKDdzz2cPKTQaKzpxVTxQ=","tlsClientExtensionsSha1":"1eY97BUYYO8vDaTfHQywB1pcNdM=","tlsClientExtensionsSha1Le":"u4wtEMFQBY18l3BzHAvORm+KGRw=","tlsExportedAuthenticator":{"clientHandshake":"2f42c1b65266f736de9ab540b71507dcf00dd653d1b6612774008de57f0492b5e295b41bd3468c7634b880e61deaa0a3","serverHandshake":"343706c57562dfbaa23089d3966f4d64ba8e1ae3d7db043474ed644a0487b8545dbeae9a83f980f4b5cc54c4d9a04d49","clientFinished":"8c5bace08e1fa6903f2bdc37ea035442e4d944cd1402ea604a0b45454ec14b28fc7fb947ba3a8322df65b6abe23c5c40","serverFinished":"22f787324da16e80d1ae8311cf917d76f6000ecece618d6d489ef12802b1771e220fe49617f1b5074d984247142a7ff7"},"tlsClientHelloLength":"1605","tlsClientAuth":{"certPresented":"0","certVerified":"NONE","certRevoked":"0","certIssuerDN":"","certSubjectDN":"","certIssuerDNRFC2253":"","certSubjectDNRFC2253":"","certIssuerDNLegacy":"","certSubjectDNLegacy":"","certSerial":"","certIssuerSerial":"","certSKI":"","certIssuerSKI":"","certFingerprintSHA1":"","certFingerprintSHA256":"","certNotBefore":"","certNotAfter":"","certRFC9440":"","certRFC9440TooLarge":false,"certChainRFC9440":"","certChainRFC9440TooLarge":false},"verifiedBotCategory":"","edgeL4":{"deliveryRate":238490},"botManagement":{"corporateProxy":false,"verifiedBot":false,"jsDetection":{"passed":false},"staticResource":false,"detectionIds":{},"score":99}} \ No newline at end of file