Skip to content

computer: Configure all isolate modules through modules - #175

Closed
mattzcarey wants to merge 6 commits into
feat/ws-container-modulefrom
feat/unified-modules
Closed

mattzcarey wants to merge 6 commits into
feat/ws-container-modulefrom
feat/unified-modules

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Stacked on #172.

WorkerJavaScriptBackend had two ways to add an import, plus a set that was always on, and the model learned about modules from a separate hand-written string:

new WorkerJavaScriptBackend({
  modules: { "tar-stream": BUNDLE },           // source only
  trustedModules: { "ws:model": { batch } },   // host functions
  allowGitNetwork: true,                       // gates the always-on ws:git
  allowArtifactNetwork: true,                  // gates the always-on ws:artifacts
});
createAITools({ shell: { backends: { js: { description: "...and ws:git exports clone, status..." } } } });

There is now one modules option, and the value's type decides what it is:

import { createContainerModule } from "@cloudflare/computer/modules/container";
import { createGitModule } from "@cloudflare/computer/modules/git";

new WorkerJavaScriptBackend({
  loader: env.LOADER,
  modules: {
    "tar-stream": BUNDLE,                                   // string: bundled source
    "ws:weather": { forecast: ([city]) => lookUp(city) },   // object: host functions
    "ws:container": createContainerModule(),                // factory: (host) => functions
    "ws:git": createGitModule({ allowNetwork: true }),
  },
});

createAITools({ workspace, shell: { backends: { "worker-javascript": {} } } });
Value Meaning
string Source bundled into the isolate, with no host access
object of functions Host module under ws:*; each function is a named export
(host) => functions Host module built from host.git, host.artifacts, host.runtime when the backend connects

node:fs and node:fs/promises stay built in. Nothing under ws: is installed unless configured. Git, Artifacts, and the container are prebuilt factories under @cloudflare/computer/modules/*. Their network access is allowNetwork on the module, not a backend flag.

Every host function gets (args, { signal, deadline, access, resolvePath }) and may return any JSON-compatible value. The bridge checks results at runtime, turns undefined into null, and drops undefined fields, so returning a GitClient result or an interface-typed value just works. access is what lets ws:container refuse to run on a read-only backend. resolvePath is the same root confinement the bridge applied to ws:git before.

The module list the model reads is built from the same modules option the backend runs with, so it can't drift. The backend exposes it as description, the runtime returns it from describe(id), and the exec tool appends it to the backend's entry:

`command` is ECMAScript module source, run in an isolated JavaScript runtime. ...
Code has no direct network access.

Modules code can import:
- `node:fs/promises` (also `node:fs`): the workspace's files. ...
- `tar-stream`: a bundled library.
- `ws:weather`: exports `forecast`.
- `ws:container`: Runs shell commands in a full Linux container that shares this workspace's files. ...
- `ws:git`: The workspace's Git repository tools: `status({ dir })`, ...

A factory adds text through a description property, as the prebuilt modules do. An object is listed by its export names. The caller's backend description in the tool options is now optional when the backend describes itself.

This PR also removes some code along the way. The exec tool builds one input schema and offers input only when a backend accepts it. Source module name checks run once at construction instead of on every execution. The tool and the container module share one UTF-8 truncation helper. The runtime reads callable and description straight from the registered backends. The pending changesets from #170 and #172 fold into one entry that describes what ships.

Breaking from main:

  • trustedModules moves into modules, with one function per method instead of a call(method, args) handler.
  • ws:git and ws:artifacts must be configured.
  • allowGitNetwork and allowArtifactNetwork become allowNetwork on the matching module.

Specifiers and object export names are checked at construction. A factory's export names are checked when the backend connects. The script runner suite runs every prebuilt module in a real Dynamic Worker, and a tools test checks the generated description against a real WorkerJavaScriptBackend.

A trusted module used to be one call(method, args, context) handler,
and caller code reached it through a single generic export:
call("batch", requests). Every host module had to dispatch on a method
string by hand, and code in the isolate could not import the functions
it wanted by name.

A trusted module is now an object of host functions. Each function
becomes a named export, so { "ws:container": { exec } } lets caller
code write import { exec } from "ws:container". Each function receives
the arguments as an array and a { signal, deadline } context. Importing
a name the module does not export fails when the module graph links,
and the bridge only dispatches to functions the module owns, so
inherited members such as toString are not reachable.

The backend now checks specifiers and export names at construction
rather than on the first execution. Export names must be JavaScript
identifier names other than default and then. The rlm example moves
ws:model to a named batch function.
The exec tool always offered a backend argument, even when only one
backend was configured. The model saw an enum with a single value and a
description written for choosing between several backends, including
advice to retry a failed command somewhere else.

With exactly one backend the tool now has no backend argument and
always runs there, and defaultBackend becomes optional. The description
talks about what that backend does rather than about picking one. A
single callable backend describes command as module source and keeps
input; a single shell backend drops input, which it could never accept.
A backend value sent anyway is removed by the schema.

With more than one backend the tool is unchanged, and defaultBackend is
still required. The output still names the backend that ran.
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() builds a trusted module that installs as
ws:container on a WorkerJavaScriptBackend. 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, and caps the
command's timeout at the host call deadline. The runtime is resolved on
each call so the module can be built before the Workspace that owns
both backends.

describeContainerModule() returns text for the JavaScript backend's
tool description so the model knows the module exists.
@mattzcarey mattzcarey added the allow-pr Allow a PR to remain open. label Sep 30, 2026
@changeset-bot

changeset-bot Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

馃 Changeset detected

Latest commit: 0019f0d

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

WorkerJavaScriptBackend had two ways to add imports: modules for
bundled source and trustedModules for host functions. On top of those,
node:fs, ws:git, and ws:artifacts were always installed, with git and
artifacts special-cased in the bridge and gated by allowGitNetwork and
allowArtifactNetwork.

There is now one modules option. A string is bundled source, as
before. A host module, built with defineModule(), runs in the Durable
Object under a ws:* specifier and exports its functions by name. A
factory form receives the Workspace's Git client, Artifacts client, and
runtime when the backend connects, and every call gets its access level
and a path resolver confined to the backend root alongside the signal
and deadline.

Git, Artifacts, and the container become prebuilt host modules under
@cloudflare/computer/modules/*, and nothing under ws: is installed
unless it is configured. Network access moves to allowNetwork on the
git and artifacts modules. The container module takes the runtime from
the host instead of a getter and refuses to run on a read-only
backend. node:fs and node:fs/promises stay built in.

@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 potential issue.

Devin Review

Comment thread packages/computer/src/modules/git.ts Outdated
Comment on lines +85 to +88
const result = await host.git.cli({
...input,
cwd: await context.resolvePath(input.cwd ?? ".", { allowMissing: true }),
});

@devin-ai-integration devin-ai-integration Bot Sep 30, 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.

馃煡 Git CLI paths escape backend root

A cli call with an absolute directory argument bypasses context.resolvePath, which checks only cwd. runInit uses the argument instead, allowing repository writes outside the backend root.

Devin Review


Was this helpful? React with 馃憤 or 馃憥 to provide feedback.

@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@175

commit: 0019f0d

A host module no longer needs a wrapper. In modules, an object of
functions is a host module and a function is a factory that builds one
from the Workspace's Git client, Artifacts client, and runtime. The
prebuilt modules are factories. Host functions may return any value;
the bridge already checks results at runtime, and now treats undefined
as null and drops undefined object fields the way JSON does, so
wrappers no longer need a JSON round trip to satisfy the types.

The JavaScript backend now describes its source language and every
importable module, built from the same modules option it runs with. A
factory contributes its description property and an object is listed
by its export names. Backends expose this as description, the runtime
returns it from describe(id), and the exec tool adds it to each
backend's entry. The caller's own description becomes optional, so the
module list the model reads cannot drift from what is installed, and
describeContainerModule() goes away.

The exec tool builds one input schema and offers input only when some
backend accepts it. Source module name checks move from every
execution to construction, and the exec tool and container module
share one UTF-8 truncation helper.

@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 2 new potential issues.

Devin Review

Comment on lines +281 to +283
entries: Object.entries(value)
.filter(([, child]) => child !== undefined)
.map(([key, child]) => [key, encodeBridgeValue(child)]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

馃煛 Omitted result fields exhaust response limits

When a host function returns many undefined fields, assertResponseWithin counts them despite their removal during encoding. A small encoded result can fail the response limit.

Learn more

Host functions may return plain objects with undefined properties. The bridge omits these properties in encodeBridgeValue, but assertResponseWithin still visits every property before encoding. Its 4096-node cap and byte estimate can reject a result whose actual wire representation fits the limits.

Example: A host function returns an object with 4,100 optional fields set to undefined and one field set to true. Encoding would send only the true field, but the response checker rejects it for having too many values.

Recommended fix: Make assertResponseWithin skip undefined-valued object entries just as encodeBridgeValue does. Keep the final encoded-byte check for the actual payload.

Devin Review


Was this helpful? React with 馃憤 or 馃憥 to provide feedback.

Comment on lines 73 to +81
export type {
ModuleExecutionEnvelope,
ModuleExecutionInput,
WorkspaceModule,
WorkspaceModuleBackend,
WorkspaceModuleBackendHandle,
WorkspaceModuleBackendHost,
WorkspaceModuleCallContext,
WorkspaceModuleFactory,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

馃攳 Migration example conflicts with exported API

The PR description still shows defineModule() as the migration path, but the public API now accepts host functions and factories directly. Update the migration example before release.

Devin Review


Was this helpful? React with 馃憤 or 馃憥 to provide feedback.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@agent-think can you split this changeset into one per change, use one sentence for the summary and link to the relevant docs.

@mattzcarey

Copy link
Copy Markdown
Member Author

Folded into #170 (modules), #171 (exec tool), and #172 (ws:container), which now carry this design directly.

@mattzcarey mattzcarey closed this Oct 1, 2026
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