Repository navigation
Conversation
Code running in exec often wants the tools the model already has: a
loop that greps a hundred files is one exec call instead of a hundred
model turns. Each host that wanted this had to write its own host
module, repeating the same argument checks, export-name rules, and
result shape.
createToolBindings() in @cloudflare/computer/modules/tools builds the
ws:tools module from bindings, each a name and a function to call. It
takes a list, or a function the backend calls each time it connects,
for tools that need a Workspace that does not exist yet when the
backend is constructed. Every export takes one object of arguments and
resolves with { text, details, isError }. exec is left out by default,
since the caller is already inside exec and a run that can start runs
has no bound on how deep it goes. A tool whose name cannot be an
export fails the connection with a message naming it, rather than the
backend's generic one.
forPiTools() in @cloudflare/computer/tools/pi-ai makes those bindings
from pi. A pi-ai Tool is only a declaration, so it takes executable
tools in the shape pi-agent-core defines, or the result of
createPiTools(). A call runs the way pi's agent loop runs a model's
call: prepareArguments, then an optional validator, then execute with
a fresh call id and the call's abort signal. Passing pi-ai's
validateToolArguments holds code to the same check as the model. The
pi types stay structural, so pi is still not a build-time dependency.
馃 Changeset detectedLatest commit: 07e3316 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 |
| functions[binding.name] = async (args, context) => | ||
| binding.call(singleObject(binding.name, args), context); | ||
| } | ||
| return functions; |
There was a problem hiding this comment.
馃敶 Empty tool sets block execution
When no tools remain after filtering, createToolBindings returns an empty module. assertHostModuleExports rejects it during connection, so even executions without ws:tools fail.
Learn more
The JavaScript backend checks every host module's exports when it connects, before running any source. A tool list can be empty, or the default exec exclusion can remove its only tool. In either case, this factory returns no functions, and assertHostModuleExports rejects the entire connection. No execution can start, even one that never imports ws:tools.
Example: With createToolBindings(forPiTools([execTool])), exec is excluded. A script importing only node:path fails to start because the ws:tools factory exports nothing.
Recommended fix: Define how an empty ws:tools module is represented and ensure the backend accepts that representation. Alternatively, fail at configuration with a targeted message if empty tool sets are unsupported; test both an empty list and an all-excluded list.
Was this helpful? React with 馃憤 or 馃憥 to provide feedback.
| ): WorkspaceModuleFactory { | ||
| const exclude = new Set(options.exclude ?? ["exec"]); | ||
| const create = (_host: WorkspaceModuleHost): WorkspaceModuleFunctions => { | ||
| const functions: Record<string, WorkspaceModuleFunction> = {}; |
There was a problem hiding this comment.
馃煛 Tool named proto disappears
A tool named __proto__ passes validation, but assigning its function to functions invokes the inherited setter. The export disappears, and a sole __proto__ tool blocks backend connection.
| const functions: Record<string, WorkspaceModuleFunction> = {}; | |
| const functions: Record<string, WorkspaceModuleFunction> = Object.create(null); |
Was this helpful? React with 馃憤 or 馃憥 to provide feedback.
| functions[binding.name] = async (args, context) => | ||
| binding.call(singleObject(binding.name, args), context); |
There was a problem hiding this comment.
馃煡 Read-only executions can modify Workspace files
On a read-only backend, createToolBindings still exposes mutating agent tools and calls them without checking context.access. Those tools use the host Workspace, so write or delete can change files despite read-only execution.
Was this helpful? React with 馃憤 or 馃憥 to provide feedback.
| functions[binding.name] = async (args, context) => | ||
| binding.call(singleObject(binding.name, args), context); |
There was a problem hiding this comment.
commit: |
Code running in
execcan now import and call it's toolset.createToolBindings(), from@cloudflare/computer/modules/tools, is generic. It turns a list of bindings into thews:toolsmodule:forPiTools(), from@cloudflare/computer/tools/pi-ai, builds those bindings from pi tools. It runs each call the way pi's agent loop does:prepareArguments, then validation, thenexecute.The exec then can import the tools as any other module:
Each tool takes one object of arguments, the same object the model would pass. It returns
{ text, details, isError }, plusstructuredContentwhen the tool provides one.