Repository navigation
computer: Add ws:container and pick exec backends with one option - #172
Open
mattzcarey wants to merge 13 commits into
Open
mattzcarey wants to merge 13 commits into
mattzcarey wants to merge 13 commits into
Conversation
🦋 Changeset detectedLatest commit: f153332 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 |
Contributor
|
Thanks for your interest in Cloudflare Computer. This repository does not accept unsolicited pull requests. Please use one of the accepted contribution paths instead:
If a maintainer asked you to open this pull request, they can add the |
commit: |
aron-cf
added this pull request to stack #178
September 30, 2026 21:17
Collaborator
|
@mattzcarey can you stack this on top of the trustedModules -> modules change. I can merge those now, would like to properly review this one. |
mattzcarey
force-pushed
the
feat/ws-container-module
branch
from
October 1, 2026 09:27
82ad877 to
82eacc5
Compare
This was referenced Oct 1, 2026
mattzcarey
force-pushed
the
feat/ws-container-module
branch
from
October 1, 2026 11:23
1adc1e6 to
0970232
Compare
mattzcarey
force-pushed
the
feat/ws-container-module
branch
2 times, most recently
from
October 6, 2026 19:10
d3f6a5a to
b7f43ad
Compare
mattzcarey
force-pushed
the
feat/ws-container-module
branch
from
October 6, 2026 20:23
b7f43ad to
9290816
Compare
An agent that wants both isolated JavaScript and a full Linux container has so far needed two exec backends, and the model had to pick one per command. This lets JavaScript be the only backend the model sees, with the container as a library it can call. createContainerModule() in @cloudflare/computer/modules/container is a host module factory. Installed as ws:container, its exec(command, options) runs through workspace.runtime.exec on the container backend, so the container shares the Workspace's files through the usual sync bracket. It returns the exit code and bounded output once the command finishes, kills the command when the execution is cancelled, caps the command's timeout at the host call deadline, and refuses to run on a read-only backend, since a container command can write to the Workspace whatever the isolate's access is. Its description tells the model how to call it, and reaches the exec tool through the JavaScript backend's own description.
* computer: Carry backend information through Workspace clients
A WorkspaceClient from getWorkspace() wrapped the runtime with exec,
getExec, killExec, and disposeExec only. The exec tool also asks the
runtime for backendIds, isCallable, and describe, so a client lost all
three: with exec omitted it offered no exec tool, and a callable
backend lost its input argument and its module list. Over RPC those
calls would be asynchronous, while the tool builds its schema
synchronously.
Backends are fixed when the Workspace is constructed, so the runtime
and its RPC stub now expose one backends() call, and a client takes
that snapshot when it is created and answers the three questions from
it, locally and remotely alike.
CloudflareContainerBackend now describes network access from its
egress setting instead of always claiming it, exec takes precedence
over the deprecated shell option so exec: {} always means no tool, and
the mcp README and think prompt stop implying a default backend.
* computer: Answer backend questions with one backends() call
The exec tool learned about backends through three runtime methods,
backendIds, isCallable, and describe, and every way of reaching a
Workspace had to forward all three. It now reads one list from
runtime.backends(), each entry carrying the id, whether the backend is
callable, and its description. backendIds and describe are gone, and
a Workspace client exposes the same backends() from its snapshot.
isCallable stays on the runtime for its own check before running.
* computer: Freeze the client backend snapshot and test input through it
A client returned its backend snapshot array itself, so a caller that
edited it changed what later tool sets saw. The snapshot is now frozen
once when the client is created.
The client tests only checked that the exec schema offered input. They
now send structured input through the exec tool on a local and a
remote client to a callable backend that echoes it, and check the
value comes back as the result. The mcp README no longer calls
worker-shell the default.
The exec tool's self-description for the container landed on the platform-scheduled backend, which is now LegacyContainerBackend. It moves to ContainerBackend, the backend containers should use, with its network line still following the egress mode. The legacy backend goes back to describing nothing and gets the exec tool's one-line default.
ws:container found a missing or wrong container backend only on its first exec. Its factory runs when the JavaScript backend connects and can read the Workspace's backends, so it now fails there: with no such backend, or with one that runs modules instead of shell commands. Its description also stopped promising network access. The model only sees the module's text in this setup, and whether the container can reach the network depends on the container backend's egress setting, which the module cannot know when it is built.
The ws:container docs, the exec tool docs, and the changesets named the old CloudflareContainerBackend. They now show ContainerBackend, the durable-object-scheduled backend containers should use.
Both examples ran their container through LegacyContainerBackend, the platform-scheduled backend, which no longer describes itself to the model. They now use ContainerBackend: the durable object schedules the container, the containers block names an image under images.app with scheduling_policy "durable_object", and the backend asks for standard-2 at launch, the size the old block requested.
ws:container also rejected a backend marked callable, taking that as a sign it runs modules. callable means a backend takes structured input, and a shell backend may do both, so the check refused valid backends and let through a module backend that was not callable. It now checks only that the backend exists.
ws:container needs a backend that runs shell commands. It first judged that by callable, which describes structured input instead: a shell backend may be callable, and a module backend need not be. Without any check, pointing it at the JavaScript backend would run a shell command as JavaScript, or start a nested run. runtime.backends() now reports each backend's protocol, "command" or "module", and ws:container refuses a module backend when it connects. A callable shell backend is accepted.
Import createAITools from its new home in the client test, and cover a direct execute call that names another backend.
mattzcarey
force-pushed
the
feat/ws-container-module
branch
from
October 6, 2026 21:22
9290816 to
ba4b679
Compare
…est is ws:container now asks the runtime for at most maxOutputBytes per stream, so a noisy command keeps only its end in memory and its full output in a Workspace file. The result passes on `truncated` with that file's path. A Workspace with output saving off still gets the old cut.
The default output directory is outside the JavaScript backend's root, so point isolate code at the agent's read and grep tools, and document setting output.dir under root when code needs the file itself.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #171. #181 and #182 were reviewed separately and merged into this branch.
An agent that wants both isolated JavaScript and a full Linux container has needed two
execbackends, and the model had to choose one for every command. With this PR, JavaScript can be the only backend the model sees, and the container becomes a module JavaScript imports:sequenceDiagram participant Model participant JS as worker-javascript isolate participant Mod as ws:container (host) participant RT as workspace.runtime participant C as ContainerBackend Model->>JS: exec tool, module source JS->>Mod: exec("npm test", { cwd }) Mod->>RT: exec(command, { backend: "container-shell" }) RT->>C: push Workspace changes, run command C-->>RT: output, exit code, pull changes RT-->>Mod: result Mod-->>JS: { exitCode, stdout, stderr }ws:container.createContainerModule()is a host module factory (#170). It takeshost.runtimewhen the JavaScript backend connects, and fails there if the Workspace has no such backend, or that backend'sprotocolismodule, which would run shell text as JavaScript.runtime.backends()reportsprotocolfor this, becausecallableonly describes structured input.execis a host call. Output arrives when the command finishes. Each stream keeps its last 2000 lines ormaxOutputBytes(64 KiB by default); the runtime (#204) saves the rest to a Workspace file andexecreturnstruncatedwith its path, so a noisy command never sits whole in the Durable Object's memory. The timeout is capped at the host call deadline. Cancelling the JavaScript execution kills the command, arguments are parsed strictly, andexecrefuses to run on a read-only backend.The
exectool (#181).createAIToolsmoves to@cloudflare/computer/tools/ai-sdkand takesexec, the backends the model can use keyed by id. Leavingexecout offers every backend. With several backends the model must name one on every call; there is no default.WorkerJavaScriptBackenddescribes its modules, andWorkerShellBackendandContainerBackenddescribe themselves, so{}is enough per backend.shellstays as a deprecated alias.createPiToolsandcreateTanStackTools(#191) take the sameexecoption.This breaks
import { createAITools } from "@cloudflare/computer/tools", released in 0.4.1:@cloudflare/computer/toolskeeps the individual AI SDKcreate*Toolfunctions, andExecBackendDescriptionbecomesExecBackendOptions.Workspace clients (#182). A
WorkspaceClientfromgetWorkspace()answersruntime.backends()from a frozen snapshot taken when the client is created, locally and over RPC. Tools built from thewithWorkspacemixin, or from another Worker, then get the sameexectool as tools built from a Workspace the Durable Object owns.On top of the new container runtime. #161 and #162 renamed the platform-scheduled backend to
LegacyContainerBackendand addedContainerBackend, where the Durable Object schedules the container. This branch targets the new one:ContainerBackend, with its network line followingegress: direct, through a gateway, or none.LegacyContainerBackendis unchanged frommain.ws:containerno longer promises network access. In this setup the model only reads the module's text, and network access depends on the container backend'segress.examples/thinkandexamples/mcpmove fromLegacyContainerBackendtoContainerBackend. Their containers blocks usescheduling_policy: "durable_object"withimages.app, and the backend asks forstandard-2at launch, the size the old blocks requested.thinknow needs nothing beyondcreateAITools({ workspace: this.workspace }).docs/09_tool_interface.md,docs/17_isolate_javascript.md, and the changesets nameContainerBackend.Unit tests cover the container module's argument parsing, timeout cap, kill on abort, read-only refusal, the connect-time backend check, and UTF-8 truncation, plus
ContainerBackend's description for each egress mode. The script runner suite runsimport { exec } from "ws:container"in a real Dynamic Worker against a realWorkspace. The client tests send structuredinputthrough local and remote clients.