Skip to content

computer: Update tool sets to work with new exec model - #186

Open
mattzcarey wants to merge 10 commits into
feat/ws-container-modulefrom
feat/pi-ai-tanstack-ai-tools
Open

mattzcarey wants to merge 10 commits into
feat/ws-container-modulefrom
feat/pi-ai-tanstack-ai-tools

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Stacked on #172. Replaces #149: same pi and TanStack AI tool sets from @aron-cf, rebuilt on the exec API that #181 and #182 gave createAITools.

Two new entry points, named after the library each one serves:

Library Entry point Factory
ai @cloudflare/computer/tools/ai-sdk createAITools (unchanged)
@earendil-works/pi-ai @cloudflare/computer/tools/pi-ai createPiTools
@tanstack/ai @cloudflare/computer/tools/tanstack-ai createTanStackTools

All three take the same options, exec included, and build the same tools from the same core:

import { createPiTools } from "@cloudflare/computer/tools/pi-ai";

// Every Workspace backend by default; `exec` picks them, `exec: {}` drops the tool.
const { tools, execute } = createPiTools({ workspace });

const message = await models.complete(model, { systemPrompt, messages, tools });
for (const block of message.content) {
  if (block.type !== "toolCall") continue;
  const { content, isError } = await execute(block); // a bad call comes back as isError, not a throw
  messages.push({ role: "toolResult", toolCallId: block.id, toolName: block.name, content, isError, timestamp: Date.now() });
}
import { createTanStackTools } from "@cloudflare/computer/tools/tanstack-ai";

const tools = createTanStackTools({
  workspace,
  exec: { "worker-shell": { description: "Use for quick checks." } },
  approve: "mutating", // needsApproval on write, edit, delete, exec, publish
});
chat({ adapter, messages, tools, abortController });

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"]
Loading
  • On the stack, every tool called tool() from ai, so nothing could share them without loading ai. Each tool is now split: common/ holds the schema, description, and executor, and ai-sdk/tools.ts wraps them. @cloudflare/computer/tools exports exactly what it did before.
  • defineExec() is the stack's createExecTool minus tool(): one backend means no backend argument, several mean backend is required, and input appears only when a backend is callable. The pi and TanStack adapters get all of that for free.
  • resolveToolOptions() holds the exec / deprecated shell handling that lived in createAITools, 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:

Entry External imports
tools/ai-sdk ai, zod
tools/pi-ai zod
tools/tanstack-ai zod

(node:zlib also 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

  • Entry points are named after their packages: tools/pi → tools/pi-ai, tools/tanstack → tools/tanstack-ai. The functions keep Add tools for Pi and Tanstack harnesses #149's names, createPiTools and createTanStackTools.
  • pi checks arguments with TypeBox, which also takes plain JSON Schema, so the pi tools need nothing beyond zod. The declarations now stay open: pi 0.99 closes a constrainedSampling schema itself when the provider runs it strict. Add tools for Pi and Tanstack harnesses #149 closed read, write, and edit up front, so a non-strict provider that left out offset failed pi's own validateToolCall. Tests now check the declarations against validateToolCall and makeStrictJsonSchema.
  • tools/index no longer re-exports the pi and TanStack sets, which would have tied them back to ai.
  • Uses exec instead of shell + defaultBackend. The tests cover several backends, exec narrowing to one, exec: {}, and callable input.
  • Keeps dofs: Add exclusions to recursive grep #150's grep exclude in the common grep.
  • TanStack read returns an image or PDF as [text part, image/document part]. TanStack only passes ContentPart[] 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 move to examples/pi-ai and examples/tanstack-ai, bump to pi-ai 0.99 / @tanstack/ai 0.63 / @tanstack/ai-cloudflare 0.2, and join the CI examples matrix. npm run local runs each agent loop in Node against a scripted model (a small shim covers cloudflare:workers).
  • Docs: docs/09_tool_interface.md has 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

@changeset-bot

changeset-bot Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 67b065c

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

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

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

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note

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

Devin Review found 4 potential issues.

Devin Review

Comment thread packages/computer/src/tools/pi-ai/index.ts
Comment thread packages/computer/src/tools/tanstack-ai/index.ts Outdated
Comment on lines +102 to +106
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`);

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟥 Anonymous callers control the pi agent

Any caller can POST a task to PiAgent.run without authentication. The agent exposes workspace writes and shell execution, allowing anonymous callers to consume Workers AI quota and alter shared files.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Comment on lines +75 to +79
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`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟥 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Same as the pi example: a local wrangler dev demo, unauthenticated like the repo's other examples.

@pkg-pr-new

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

Copy link
Copy Markdown

Open in StackBlitz

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

commit: 67b065c

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note

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

Devin Review found 1 new potential issue.

Devin Review

Comment on lines +314 to +316
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

scuffi and others added 3 commits October 1, 2026 16:33
#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>
aron-cf and others added 6 commits October 2, 2026 12:01
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.
@aron-cf
aron-cf force-pushed the feat/pi-ai-tanstack-ai-tools branch from e670853 to 67b065c Compare October 2, 2026 11:30
@aron-cf aron-cf changed the title computer: Add pi-ai and TanStack AI tool sets computer: Update tool sets to work with new exec model Oct 2, 2026

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 4 new potential issues.

Devin Review

Comment on lines +146 to +148
const declaredRelative = declared.map((path) => stripMount(path, resolved.mountPoint));
const actualRelative = resolved.paths.map((path) => stripMount(path, resolved.mountPoint));
const difference = diffIgnore(declaredRelative, actualRelative);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +681 to +686
const info: ComputerdInfo = {
backend,
mountPoint,
port,
store,
ignore: describeMountIgnore(ignoreConfig),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +227 to +228
const wrapped: FuseOps = {
...ops,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +296 to +301
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));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟥 Local-only symlinks reach container files

When an ignored path is a symlink outside its disk root, openSync follows it. Reads and writes through the mount then reach unrelated container files.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants