computer: Configure all isolate modules through modules - #175
mattzcarey wants to merge 6 commits into
Conversation
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.
馃 Changeset detectedLatest commit: 0019f0d 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 |
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.
33f4111 to
6c91d59
Compare
| const result = await host.git.cli({ | ||
| ...input, | ||
| cwd: await context.resolvePath(input.cwd ?? ".", { allowMissing: true }), | ||
| }); |
There was a problem hiding this comment.
馃煡 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.
Was this helpful? React with 馃憤 or 馃憥 to provide feedback.
commit: |
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.
| entries: Object.entries(value) | ||
| .filter(([, child]) => child !== undefined) | ||
| .map(([key, child]) => [key, encodeBridgeValue(child)]), |
There was a problem hiding this comment.
馃煛 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.
Was this helpful? React with 馃憤 or 馃憥 to provide feedback.
| export type { | ||
| ModuleExecutionEnvelope, | ||
| ModuleExecutionInput, | ||
| WorkspaceModule, | ||
| WorkspaceModuleBackend, | ||
| WorkspaceModuleBackendHandle, | ||
| WorkspaceModuleBackendHost, | ||
| WorkspaceModuleCallContext, | ||
| WorkspaceModuleFactory, |
There was a problem hiding this comment.
There was a problem hiding this comment.
@agent-think can you split this changeset into one per change, use one sentence for the summary and link to the relevant docs.
Stacked on #172.
WorkerJavaScriptBackendhad 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:There is now one
modulesoption, and the value's type decides what it is:stringws:*; each function is a named export(host) => functionshost.git,host.artifacts,host.runtimewhen the backend connectsnode:fsandnode:fs/promisesstay built in. Nothing underws:is installed unless configured. Git, Artifacts, and the container are prebuilt factories under@cloudflare/computer/modules/*. Their network access isallowNetworkon 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, turnsundefinedintonull, and dropsundefinedfields, so returning aGitClientresult or an interface-typed value just works.accessis what letsws:containerrefuse to run on a read-only backend.resolvePathis the same root confinement the bridge applied tows:gitbefore.The module list the model reads is built from the same
modulesoption the backend runs with, so it can't drift. The backend exposes it asdescription, the runtime returns it fromdescribe(id), and theexectool appends it to the backend's entry:A factory adds text through a
descriptionproperty, as the prebuilt modules do. An object is listed by its export names. The caller's backenddescriptionin the tool options is now optional when the backend describes itself.This PR also removes some code along the way. The
exectool builds one input schema and offersinputonly 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 readscallableanddescriptionstraight from the registered backends. The pending changesets from #170 and #172 fold into one entry that describes what ships.Breaking from main:
trustedModulesmoves intomodules, with one function per method instead of acall(method, args)handler.ws:gitandws:artifactsmust be configured.allowGitNetworkandallowArtifactNetworkbecomeallowNetworkon 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.