Skip to content

computer: Add ws:container and pick exec backends with one option - #172

Open
mattzcarey wants to merge 13 commits into
mainfrom
feat/ws-container-module
Open

mattzcarey wants to merge 13 commits into
mainfrom
feat/ws-container-module

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

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 exec backends, 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:

import { ContainerBackend, withWorkspaceContainer } from "@cloudflare/computer/backends/container";
import { createContainerModule } from "@cloudflare/computer/modules/container";
import { createAITools } from "@cloudflare/computer/tools/ai-sdk";

class Agent extends withWorkspaceContainer(class extends DurableObject<Env> {}) {
  workspace = new Workspace({
    storage: this.ctx.storage,
    backends: [
      new WorkerJavaScriptBackend({
        loader: this.env.LOADER,
        modules: { "ws:container": createContainerModule() },
      }),
      new ContainerBackend({
        container: () => this,
        workspace: { binding: "Agent", id: this.ctx.id.toString() },
        egress: { mode: "direct" },
      }),
    ],
  });

  getTools() {
    // Offer only JavaScript; the container is reached through ws:container.
    return createAITools({ workspace: this.workspace, exec: { "worker-javascript": {} } });
  }
}
import { exec } from "ws:container";

export default async function () {
  const { exitCode, stdout, stderr } = await exec("npm test", { cwd: "/workspace/app" });
  return { passed: exitCode === 0, stdout, stderr };
}
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 }
Loading

ws:container. createContainerModule() is a host module factory (#170). It takes host.runtime when the JavaScript backend connects, and fails there if the Workspace has no such backend, or that backend's protocol is module, which would run shell text as JavaScript. runtime.backends() reports protocol for this, because callable only describes structured input. exec is a host call. Output arrives when the command finishes. Each stream keeps its last 2000 lines or maxOutputBytes (64 KiB by default); the runtime (#204) saves the rest to a Workspace file and exec returns truncated with 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, and exec refuses to run on a read-only backend.

The exec tool (#181). createAITools moves to @cloudflare/computer/tools/ai-sdk and takes exec, the backends the model can use keyed by id. Leaving exec out offers every backend. With several backends the model must name one on every call; there is no default. WorkerJavaScriptBackend describes its modules, and WorkerShellBackend and ContainerBackend describe themselves, so {} is enough per backend. shell stays as a deprecated alias. createPiTools and createTanStackTools (#191) take the same exec option.

This breaks import { createAITools } from "@cloudflare/computer/tools", released in 0.4.1: @cloudflare/computer/tools keeps the individual AI SDK create*Tool functions, and ExecBackendDescription becomes ExecBackendOptions.

Workspace clients (#182). A WorkspaceClient from getWorkspace() answers runtime.backends() from a frozen snapshot taken when the client is created, locally and over RPC. Tools built from the withWorkspace mixin, or from another Worker, then get the same exec tool 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 LegacyContainerBackend and added ContainerBackend, where the Durable Object schedules the container. This branch targets the new one:

  • The container's self-description moves to ContainerBackend, with its network line following egress: direct, through a gateway, or none. LegacyContainerBackend is unchanged from main.
  • ws:container no longer promises network access. In this setup the model only reads the module's text, and network access depends on the container backend's egress.
  • examples/think and examples/mcp move from LegacyContainerBackend to ContainerBackend. Their containers blocks use scheduling_policy: "durable_object" with images.app, and the backend asks for standard-2 at launch, the size the old blocks requested. think now needs nothing beyond createAITools({ workspace: this.workspace }).
  • docs/09_tool_interface.md, docs/17_isolate_javascript.md, and the changesets name ContainerBackend.

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 runs import { exec } from "ws:container" in a real Dynamic Worker against a real Workspace. The client tests send structured input through local and remote clients.

@changeset-bot

changeset-bot Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f153332

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

@github-actions

Copy link
Copy Markdown
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 allow-pr label and reopen it.

@github-actions github-actions Bot closed this Sep 30, 2026
@mattzcarey mattzcarey added the allow-pr Allow a PR to remain open. label Sep 30, 2026
@mattzcarey mattzcarey reopened this Sep 30, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

@pkg-pr-new

pkg-pr-new Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

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

commit: f153332

@aron-cf
aron-cf added this pull request to stack #178 September 30, 2026 21:17
@aron-cf

aron-cf commented Sep 30, 2026

Copy link
Copy Markdown
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.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@mattzcarey
mattzcarey force-pushed the feat/ws-container-module branch from 1adc1e6 to 0970232 Compare October 1, 2026 11:23
@mattzcarey mattzcarey changed the title computer: Add ws:container for isolate JavaScript computer: Add ws:container and pick exec backends with one option Oct 1, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@mattzcarey
mattzcarey force-pushed the feat/ws-container-module branch 2 times, most recently from d3f6a5a to b7f43ad Compare October 6, 2026 19:10
Base automatically changed from feat/exec-tool-single-backend to main October 6, 2026 20:23
@mattzcarey
mattzcarey force-pushed the feat/ws-container-module branch from b7f43ad to 9290816 Compare October 6, 2026 20:23
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.
mattzcarey and others added 10 commits October 6, 2026 14:16
* 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
mattzcarey force-pushed the feat/ws-container-module branch from 9290816 to ba4b679 Compare October 6, 2026 21:22
devin-ai-integration[bot]

This comment was marked as resolved.

…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.
devin-ai-integration[bot]

This comment was marked as resolved.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

allow-pr Allow a PR to remain open.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants