computer: Update tool sets to work with new exec model - #186
mattzcarey wants to merge 10 commits into
Conversation
🦋 Changeset detectedLatest commit: 67b065c The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
| const { task } = (await request.json()) as { task?: string }; | ||
| if (!task) return new Response("body needs a task\n", { status: 400 }); | ||
|
|
||
| const agent = env.PiAgent.get(env.PiAgent.idFromName("demo")); | ||
| return new Response(`${await agent.run(task)}\n`); |
There was a problem hiding this comment.
There was a problem hiding this comment.
Intentional. These are local wrangler dev demos, unauthenticated like the other examples in this repo (worker-shell, worker-javascript, tutorial). The README covers running it locally.
| const { task } = (await request.json()) as { task?: string }; | ||
| if (!task) return new Response("body needs a task\n", { status: 400 }); | ||
|
|
||
| const agent = env.TanStackAgent.get(env.TanStackAgent.idFromName("demo")); | ||
| return new Response(`${await agent.run(task)}\n`); |
There was a problem hiding this comment.
🟥 Anonymous callers control the TanStack agent
Any caller can POST a task to TanStackAgent.run without authentication. The agent exposes workspace writes and shell execution, allowing anonymous callers to consume Workers AI quota and alter shared files.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Same as the pi example: a local wrangler dev demo, unauthenticated like the repo's other examples.
commit: |
| for (const [name, field] of Object.entries(schema.shape as Record<string, z.ZodType>)) { | ||
| if (field.safeParse(undefined).success && !field.safeParse(null).success) names.add(name); | ||
| } |
There was a problem hiding this comment.
🟡 Invalid optional arguments execute tools
When a caller passes path: null to find, absentWhenNull drops it even though the declared schema rejects null. The tool searches /workspace instead of returning an invalid-arguments error.
Learn more
The pi dispatcher removes null placeholders before validating arguments. Strict sampling can produce those placeholders for optional fields in read, write, or edit; ordinary calls to other tools do not require this conversion. Here, absentWhenNull also includes optional fields on tools such as findInputSchema. Its path schema accepts omission but not null. Removing an explicit null lets its default apply, so an invalid call runs against the workspace root.
Example: A pi caller invokes execute({ id: "1", name: "find", arguments: { pattern: "**/*.ts", path: null } }). Instead of returning isError: true for invalid arguments, the dispatcher omits path and searches /workspace.
Recommended fix: Restrict placeholder-null removal to tools using constrained sampling, or validate ordinary calls before removing nulls. Keep the intentional strict-mode behavior for read's optional fields and preserve valid null values such as callable exec.input.
Was this helpful? React with 👍 or 👎 to provide feedback.
#188) * gardener: Add review rounds (Gardener 0.1.10) * gardener: Read every comment page and the review body in pr-review-fix
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.7 to 3.1.8. - [Release notes](https://github.com/fastify/fast-uri/releases) - [Commits](fastify/fast-uri@v3.1.7...v3.1.8) --- updated-dependencies: - dependency-name: fast-uri dependency-version: 3.1.8 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Split each tool into a framework-neutral core under tools/common (schema, description, executor; zod only) and an adapter per agent library. tools/ai-sdk wraps the core with `tool()` from `ai`; tools/pi-ai and tools/tanstack-ai build their own shapes from the same core without importing their libraries, so each entry point pulls in only what it uses. createAITools stays exported from @cloudflare/computer/tools. The exec core keeps the current options: a `shell` with a backend map and a default backend. All three tool sets resolve options through the same resolveToolOptions, so they offer the same tools. The pi and TanStack adapters come from #149. Co-authored-by: aron <263346377+aron-cf@users.noreply.github.com>
Two one-shot agents on a Worker-shell Workspace: pi-ai, where `run` is the whole loop, and tanstack-ai, where chat() owns it. Both pass the one worker shell to `exec` through `shell`. `npm run local` drives each loop in Node with a scripted model, through a small shim for `cloudflare:workers`. Bumps the libraries to current releases (pi-ai 0.99, @tanstack/ai 0.63, @tanstack/ai-cloudflare 0.2) and adds both to the CI examples matrix. Docs, READMEs, and a changeset cover the two entry points. The examples come from #149. Co-authored-by: aron <263346377+aron-cf@users.noreply.github.com>
chat() passes a tool result to the adapter as multimodal content only when it is a ContentPart array. The read tool returned an image or PDF as a plain object, which TanStack JSON-stringified, so the model got the base64 as text. It now returns a text part plus an image or document part, and the test checks the shape with TanStack's own isContentPartArray. Also covers a failed pi publish, which already comes back as an error result.
Renames createPiAITools to createPiTools and createTanStackAITools to createTanStackTools, with their option and result types. The entry points stay tools/pi-ai and tools/tanstack-ai. pi checks tool arguments with TypeBox, which also compiles plain JSON Schema, so the pi tools need only zod for their own schemas. pi 0.99 also closes a constrainedSampling schema itself when the provider runs it strict. The adapter used to close read, write, and edit up front, which made every optional field required in the schema pi validates against. A provider that fell back to ordinary tool calling and left `offset` out of a read failed pi's own validateToolCall. The declarations now stay open, and execute drops a null only on an optional field that cannot take one. Tests check the declarations against pi's validateToolCall and makeStrictJsonSchema.
The tool sets landed on main with the exec options main had: a `shell` with a backend map and a default backend, and createAITools exported from @cloudflare/computer/tools. This branch has since moved exec to one `exec` option that offers every Workspace backend by default and takes its backend list from runtime.backends(), and moved createAITools to @cloudflare/computer/tools/ai-sdk. The resolution carries that exec core into tools/common/exec.ts and common/options.ts, so createAITools, createPiTools, and createTanStackTools all take `exec` (with `shell` kept as a deprecated alias). The tools/ai-sdk entry point, the examples, and the docs follow the same options.
e670853 to
67b065c
Compare
| const declaredRelative = declared.map((path) => stripMount(path, resolved.mountPoint)); | ||
| const actualRelative = resolved.paths.map((path) => stripMount(path, resolved.mountPoint)); | ||
| const difference = diffIgnore(declaredRelative, actualRelative); |
There was a problem hiding this comment.
🔴 Redundant ignored paths block connection
When ignore includes a parent and its child, assertIgnoreMatches rejects the healthy container. resolveMountIgnore drops the child as redundant, so its reported set differs from the declaration.
Learn more
The daemon resolves MOUNT_IGNORE to a minimal set. A declared parent covers every child, so resolveMountIgnore removes nested entries and reports only the parent. The host comparison does not minimize the declaration. It therefore treats an equivalent, correctly applied configuration as a mismatch and refuses every connection.
Example: With ignore: ['/node_modules', '/node_modules/.cache'], the daemon reports ['node_modules']. The host reports .cache missing, although every .cache file stays local.
Recommended fix: Normalize and collapse covered paths in the declaration before comparing it to the daemon's resolved set. Preserve the original declaration in error details.
Was this helpful? React with 👍 or 👎 to provide feedback.
| const info: ComputerdInfo = { | ||
| backend, | ||
| mountPoint, | ||
| port, | ||
| store, | ||
| ignore: describeMountIgnore(ignoreConfig), |
There was a problem hiding this comment.
🟡 Local-only paths reported without a mount
With FUSE_MOUNT=none, describeMountIgnore reports active local-only paths although no passthrough mounts. A client can accept the report while those paths still enter the VFS.
Learn more
The daemon reports its configured ignore set in /__computerd/info. It creates the passthrough layer only inside the mount branch, which is skipped when the backend is none. The host accepts the reported set as proof that paths are kept out of sync, despite there being no local-only layer.
Example: Start computerd with FUSE_MOUNT=none MOUNT_IGNORE=/node_modules. The info endpoint reports enabled: true and paths: ['node_modules'], but no FUSE operations route /workspace/node_modules to local disk.
Recommended fix: Reject a nonempty MOUNT_IGNORE when the backend is none, or explicitly report it as inactive and ensure the host check refuses an inactive declaration.
Was this helpful? React with 👍 or 👎 to provide feedback.
| const wrapped: FuseOps = { | ||
| ...ops, |
There was a problem hiding this comment.
🟡 Extended attributes fail on local-only files
For a file under MOUNT_IGNORE, getxattr and listxattr still reach the VFS. The VFS lacks local-only files, so applications receive ENOENT for existing files.
Learn more
The wrapper copies every VFS operation before overriding selected methods. The VFS xattr handlers check whether a path exists in the VFS. Local-only files are excluded from the VFS, so these handlers return ENOENT for paths the mount itself can read.
Example: With MOUNT_IGNORE=/node_modules, create /workspace/node_modules/pkg/index.js through the mount and request its extended attributes. Its FUSE getattr succeeds, but getxattr returns ENOENT.
Recommended fix: Override setxattr, getxattr, listxattr, and removexattr for ignored paths, using local filesystem behavior or matching the VFS's supported xattr semantics while checking existence on local disk.
Was this helpful? React with 👍 or 👎 to provide feedback.
| const target = localPath(path); | ||
| // O_CREAT is not implied by open(2) here; the kernel sends | ||
| // create() for that. But a flag set including O_TRUNC still has | ||
| // to reach the real file, so the flags are passed through as-is. | ||
| const fd = fs.openSync(target, flags); | ||
| cb(0, allocateHandle(fd, path)); |
Stacked on #172. Replaces #149: same pi and TanStack AI tool sets from @aron-cf, rebuilt on the
execAPI that #181 and #182 gavecreateAITools.Two new entry points, named after the library each one serves:
ai@cloudflare/computer/tools/ai-sdkcreateAITools(unchanged)@earendil-works/pi-ai@cloudflare/computer/tools/pi-aicreatePiTools@tanstack/ai@cloudflare/computer/tools/tanstack-aicreateTanStackToolsAll three take the same options,
execincluded, and build the same tools from the same core:Layout
flowchart LR subgraph common["tools/common (zod only)"] fs["fs/*: schema, description, executor"] exec["exec: defineExec() → description, inputSchema, execute"] opts["options: resolveToolOptions()"] end common --> aisdk["tools/ai-sdk<br/>tool() from ai"] common --> pi["tools/pi-ai<br/>JSON Schema + dispatcher"] common --> ts["tools/tanstack-ai<br/>Standard Schema tool list"] aisdk --> idx["tools (index)<br/>create*Tool, WorkspaceFileStore"]tool()fromai, so nothing could share them without loadingai. Each tool is now split:common/holds the schema, description, and executor, andai-sdk/tools.tswraps them.@cloudflare/computer/toolsexports exactly what it did before.defineExec()is the stack'screateExecToolminustool(): one backend means nobackendargument, several meanbackendis required, andinputappears only when a backend is callable. The pi and TanStack adapters get all of that for free.resolveToolOptions()holds theexec/ deprecatedshellhandling that lived increateAITools, so all three sets agree on which tools exist.Tree-shaking
pi and TanStack are declared as local structural types, not imports, so they are not peer dependencies at all. I bundled each entry with esbuild, keeping packages external:
tools/ai-sdkai,zodtools/pi-aizodtools/tanstack-aizod(
node:zlibalso shows up in all three, the same as on the base branch. It comes from a shared rolldown chunk, not the tools.)Changes from #149
tools/pi→tools/pi-ai,tools/tanstack→tools/tanstack-ai. The functions keep Add tools for Pi and Tanstack harnesses #149's names,createPiToolsandcreateTanStackTools.zod. The declarations now stay open: pi 0.99 closes aconstrainedSamplingschema itself when the provider runs it strict. Add tools for Pi and Tanstack harnesses #149 closedread,write, andeditup front, so a non-strict provider that left outoffsetfailed pi's ownvalidateToolCall. Tests now check the declarations againstvalidateToolCallandmakeStrictJsonSchema.tools/indexno longer re-exports the pi and TanStack sets, which would have tied them back toai.execinstead ofshell+defaultBackend. The tests cover several backends,execnarrowing to one,exec: {}, and callableinput.excludein the common grep.readreturns an image or PDF as[text part, image/document part]. TanStack only passesContentPart[]results to the adapter as multimodal content; Add tools for Pi and Tanstack harnesses #149's plain object got JSON-stringified, so the model saw base64 as text.examples/pi-aiandexamples/tanstack-ai, bump to pi-ai 0.99 /@tanstack/ai0.63 /@tanstack/ai-cloudflare0.2, and join the CI examples matrix.npm run localruns each agent loop in Node against a scripted model (a small shim coverscloudflare:workers).docs/09_tool_interface.mdhas pi and TanStack sections. READMEs and a changeset are updated.The lockfile only adds the examples' dependencies. No existing version changes.
Co-authored-by: aron 263346377+aron-cf@users.noreply.github.com