computer: Configure isolate modules through one modules option - #170
mattzcarey wants to merge 2 commits into
Conversation
🦋 Changeset detectedLatest commit: 3ed0326 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 |
|
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: |
| import { call as hostCall } from ${JSON.stringify(capabilitiesImport)}; | ||
| export const call = (method, ...args) => hostCall(${JSON.stringify(`trusted/${specifier}`)}, "call", [method, ...args]); | ||
| import { call } from ${JSON.stringify(capabilitiesImport)}; | ||
| ${names.map((name, index) => `const fn${index} = (...args) => call(${namespace}, ${JSON.stringify(name)}, args);`).join("\n")} |
| import { call as hostCall } from ${JSON.stringify(capabilitiesImport)}; | ||
| export const call = (method, ...args) => hostCall(${JSON.stringify(`trusted/${specifier}`)}, "call", [method, ...args]); | ||
| import { call } from ${JSON.stringify(capabilitiesImport)}; | ||
| ${names.map((name, index) => `const fn${index} = (...args) => call(${namespace}, ${JSON.stringify(name)}, args);`).join("\n")} |
|
@mattzcarey I think this is a solid improvement. I think we can re-work these trusted modules further. They were added quite last minute during the container launch and I've not looked at them since. I think we can rip out most of the bridge code here and just rely on providing modules as import { RpcTarget } from "cloudflare:workers";
class ContainerModule extends RpcTarget {
#env: Env;
constructor(env: Env) {
super();
this.#env = env; // invisible over RPC
}
async exec(cmd: string) { /* ... */ } // callable
async kill(id: string) { /* ... */ } // callable
#spawn() { /* ... */ } // invisible over RPC
}
new WorkerJavaScriptBackend({
loader: env.LOADER,
trustedModules: {
"ws:container": new ContainerModule(env),
},
});Isolate code is unchanged: import { exec } from "ws:container";
export default () => exec("npm test");We can then delete, We can potentially use a proxy wrapper to keep the existing quotas & checks. class MeteredModule extends RpcTarget {
// NOTE: instance properties aren't visible over RPC, so this needs
// prototype-level definition or a Proxy. Worth prototyping first.
constructor(target, budget) {
super();
for (const name of methodNames(target)) {
this[name] = async (...args) => {
budget.charge(); // count, concurrency, deadline
assertRuntimeValue(args); // keep the type gate
return budget.track(target[name](...args)); // for cancelAndDrain
};
}
}
} |
3bc4799 to
3cb2999
Compare
| assertBridgeValues(callArgs); | ||
| const result = await trusted.call(method, callArgs, context); | ||
| assertBridgeValues(args); | ||
| const result = (await fn(args, context)) ?? null; |
There was a problem hiding this comment.
🟡 Host module methods lose their receiver
When an exported host method uses this, fn(args, context) invokes it without its module object. The call fails instead of returning the method's result.
| const result = (await fn(args, context)) ?? null; | |
| const result = (await fn.call(functions, args, context)) ?? null; |
Was this helpful? React with 👍 or 👎 to provide feedback.
| describe(id: string): string | undefined { | ||
| return this.#options.backends.get(id)?.description; |
There was a problem hiding this comment.
🟡 Configured modules stay hidden from models
When a model uses the exec tool, describe(id) never reaches its backend guidance. createExecTool reads only caller-supplied descriptions, so configured imports remain invisible to the model.
Learn more
The JavaScript backend now builds a description listing importable modules and exposes it through WorkspaceRuntime.describe. The exec tool constructs model-visible guidance only from its separate backends option in createExecTool; createAITools passes that option through without adding runtime descriptions. Consequently configuring modules on a backend does not inform a model using the exec tool about their names or exports.
Example: Configure modules: { "ws:weather": { forecast } } and create an exec tool with backends: { js: { description: "Run JavaScript" } }. The model sees “Run JavaScript” but never sees ws:weather or forecast.
Recommended fix: Extend the exec tool's runtime interface with optional describe(id) and append its result to each backend's caller-supplied description in createExecTool, preserving support for lightweight runtime mocks.
Was this helpful? React with 👍 or 👎 to provide feedback.
| * Modules caller source can import by specifier. | ||
| * | ||
| * A string value is JavaScript source bundled into the isolate, such as | ||
| * a library build. An object of functions, or a factory that builds | ||
| * one, is a host module: it runs in the Durable Object under a `ws:*` | ||
| * specifier, and each function becomes a named export. | ||
| * | ||
| * ```ts | ||
| * modules: { | ||
| * "tar-stream": TAR_STREAM_BUNDLE, | ||
| * "ws:git": createGitModule(), | ||
| * "ws:weather": { forecast: ([city]) => lookUpForecast(String(city)) }, | ||
| * } | ||
| * ``` | ||
| * | ||
| * `node:fs` and `node:fs/promises` are always installed and cannot be | ||
| * replaced. The constructor throws when a specifier is not allowed, and | ||
| * connecting throws when a host module's export names are not allowed. | ||
| */ | ||
| trustedModules?: Record<`ws:${string}`, WorkspaceTrustedModule>; | ||
| modules?: Record<string, WorkspaceModule>; |
There was a problem hiding this comment.
🔍 PR example differs from module configuration
The PR example still uses trustedModules, while this interface accepts host functions through modules. Its sequence diagram also names the old trusted/ bridge route. Align the PR description with the shipped API.
Was this helpful? React with 👍 or 👎 to provide feedback.
WorkerJavaScriptBackend had two ways to add imports: modules for
bundled source, and trustedModules for a single call(method, args)
host handler reached through one generic call export. 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, and the value's type says what it
is. A string is bundled source, as before. An object of functions is a
host module under a ws:* specifier, and each function becomes a named
export, so { "ws:weather": { forecast } } lets code write
import { forecast } from "ws:weather". A function is a factory the
backend calls when it connects, with the Workspace's Git client,
Artifacts client, and runtime. Each call gets its access level and a
path resolver confined to the backend root alongside the signal and
deadline, and may return any JSON-compatible value; the bridge checks
it at runtime.
Git and Artifacts become prebuilt factories under
@cloudflare/computer/modules/*, and nothing under ws: is installed
unless configured. Network access moves to allowNetwork on each
module. node:fs and node:fs/promises stay built in. Specifiers, source
module names, and object export names are checked at construction.
The backend also describes its source language and every importable
module in a description property, which workspace.runtime.describe(id)
returns, built from the same modules option it runs with. The rlm
example moves ws:model to a named batch function.
The Git CLI now takes a leading -C <path>, because agents reach for git -C instead of changing directory. ws:git rejected every -C as a path override, so those commands failed inside an isolate. A leading -C now becomes the command's working directory and goes through the same root confinement as cwd. A -C anywhere else, and --git-dir and --work-tree, are still rejected.
3cb2999 to
3ed0326
Compare
WorkerJavaScriptBackendhad two ways to add an import, plus a set that was always on:There is now one
modulesoption, and the value's type says what it is:stringws:*; each function is a named export(host) => functionshost.git,host.artifacts,host.runtimewhen the backend connectssequenceDiagram participant Code as Isolate code participant Shim as Generated ws:weather participant Bridge as Host bridge participant Fn as forecast(args, context) Code->>Shim: forecast("Lisbon") Shim->>Bridge: host/ws:weather.forecast ["Lisbon"] Bridge->>Fn: own-property lookup, then call Fn-->>Code: JSON resultnode:fsandnode:fs/promisesstay built in, and nothing underws:is installed unless configured. Git and Artifacts move out of the bridge into prebuilt factories,createGitModule()andcreateArtifactsModule()under@cloudflare/computer/modules/*. Their network access is nowallowNetworkon the module instead of a backend flag.Every host function gets
(args, { signal, deadline, access, resolvePath })and may return any JSON-compatible value. The bridge checks the result at runtime, turnsundefinedintonull, and dropsundefinedfields, so an interface-typed result such as aGitClientvalue works without a wrapper.accessandresolvePathgive every module the write check and root confinement the bridge used to apply only tows:git.Generated shims export through
export { fn0 as name }, so a reserved word such asdeleteworks (callers rename it on import). Importing a name the module does not export fails when the graph links. The bridge only dispatches to functions the module owns, so inherited members liketoStringare unreachable. Specifiers, source module names, and object export names are checked at construction. A factory's export names are checked when the backend connects.The backend also describes its source language and every importable module in
backend.description, whichworkspace.runtime.describe(id)returns. It is built from the samemodulesoption, and #171 puts it in front of the model.ws:git'sclialso accepts a leading-C <path>, which the git CLI added onmainbecause agents reach forgit -C. The path becomes the command's working directory and is confined to the backend root likecwd. A-Canywhere else,--git-dir, and--work-treeare still rejected.This breaks three things.
trustedModulesmoves intomodules, with one function per method.ws:gitandws:artifactsmust be configured.allowGitNetworkandallowArtifactNetworkbecomeallowNetworkon the matching module. Therlmexample moves toimport { batch } from "ws:model". The script runner suite runs source modules, custom host modules, and both prebuilt modules in a real Dynamic Worker, including git confinement and network denial.