Skip to content

computer: Let isolate code read module declarations from cf:computer - #240

Draft
mattzcarey wants to merge 1 commit into
mainfrom
cf-computer-introspection
Draft

mattzcarey wants to merge 1 commit into
mainfrom
cf-computer-introspection

Conversation

@mattzcarey

Copy link
Copy Markdown
Member

Code running on the JavaScript backend can now look up how to call any module it can import. A built-in cf:computer module returns each module's TypeScript declarations and examples, and printing a host export shows its declaration.

import * as git from "ws:git";
import { docs, types } from "cf:computer";

docs();               // every module, one line each
docs(git);            // summary, `declare module "ws:git" { ... }`, examples
types(git.cli);       // one export's declaration, with the types it uses
console.log(git.cli); // function cli(input: { argv: string[]; ... }): Promise<GitCliResult> { [host code] }

Models were guessing how to call host modules. While moving the agents harnesses to Computer, the pi harness needed several tries to get ws:git's cli signature right:

  • the exec description gives one line of prose per module, with no argument or return types
  • a host module's code runs in the Durable Object, so printing an export showed (...args) => call("host/ws:git", "cli", args), and console.log(namespace) threw, because console output went through String()
  • cli("status") and cli(["status"]) ran git with no arguments, so they returned its help text with exit code 0

How it works:

  • Declarations. Factories take types (TypeScript declarations keyed by name) and examples. The prebuilt modules declare all their exports, and ws:tools writes each tool's declaration from the description and inputSchema that forAISDKTools and forPiTools now pass through.
  • Lookups. The backend renders the declarations once per connection, and the isolate looks them up as data. No host call is made.
  • Printing. Each host stub is named after its export and prints like a V8 built-in, with { [host code] } in place of { [native code] }. Console output follows Node.js formatting.
  • ws:git errors. A call that passes anything other than one options object, or an argv starting with "git", is refused with the shape the call expects.
  • Breaking. The cf:* specifiers are reserved, so configured modules can no longer use them.

Tests ran (npm test in packages/computer). The new end-to-end cases run in workerd under both the legacy and new module registries.

A host module's code runs in the Durable Object, so the isolate only
holds stubs that forward each call. Code had nothing to inspect: the
exec description gave one line of prose per module, printing a stub
showed the forwarding code, and printing a module namespace threw,
because console output was formatted with String(). Models guessed
call shapes instead, and ws:git made the guesses look like success:
cli("status") ran git with no arguments and returned its help text
with exit code 0.

Module factories now carry TypeScript declarations in a `types`
property, keyed by the name each one declares, and short `examples`.
The prebuilt modules declare all their exports. ws:tools writes each
tool's declaration from its description and JSON Schema input, which
forAISDKTools and forPiTools now pass through on each binding.

The JavaScript backend renders the declarations once per connection
and installs `cf:computer`, a built-in module like node:fs.
`docs(target)` returns a module's summary, declarations, and examples,
and `docs()` lists every module. `types(target)` returns only the
declarations. The target is a specifier, a namespace from
`import * as`, or one host export. The cf:* namespace is reserved, so
configured modules can no longer use it.

Each host stub is named after its export and prints its declaration
with a `{ [host code] }` body, as V8 prints a built-in with
`{ [native code] }`. console output follows Node.js formatting, so
objects, maps, and errors print legibly, and a namespace prints its
module's declaration.

ws:git exports now take exactly one options object. A string, an
array, or an argv starting with "git" is refused with the shape the
call expects.
@changeset-bot

changeset-bot Bot commented Oct 14, 2026

Copy link
Copy Markdown

馃 Changeset detected

Latest commit: ef7acbf

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

}

function mentions(text: string, name: string): boolean {
const escaped = name.replace(/[$]/g, "\\$");
import { call } from ${JSON.stringify(capabilitiesImport)};
${names.map((name, index) => `const fn${index} = (...args) => call(${namespace}, ${JSON.stringify(name)}, args);`).join("\n")}
import { hostExport } from ${JSON.stringify(introspectionImport)};
${names.map((name, index) => `const fn${index} = hostExport(${module}, ${JSON.stringify(name)}, (...args) => call(${namespace}, ${JSON.stringify(name)}, args));`).join("\n")}
import { call } from ${JSON.stringify(capabilitiesImport)};
${names.map((name, index) => `const fn${index} = (...args) => call(${namespace}, ${JSON.stringify(name)}, args);`).join("\n")}
import { hostExport } from ${JSON.stringify(introspectionImport)};
${names.map((name, index) => `const fn${index} = hostExport(${module}, ${JSON.stringify(name)}, (...args) => call(${namespace}, ${JSON.stringify(name)}, args));`).join("\n")}
"@cloudflare/computer": minor
---

JavaScript backend code can import `cf:computer` for each module's TypeScript declarations, and `console.log` prints a host export's declaration. Configured modules can no longer use `cf:*` specifiers, and `ws:git` rejects calls that pass anything other than one options object; see [`cf:computer`](https://github.com/cloudflare/computer/blob/main/docs/17_isolate_javascript.md#cfcomputer).

@aron-cf aron-cf Oct 15, 2026 •

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.

So excited for this 馃コ

I'm not sure about the module name. I don't think it should be ws: prefix (which I also think should become computer: or ext: or something that implies it's an extension. Alternatives ws:debug, ws:runtime, ws:help...

Also I think now I see it, we should just have one function. help(<thing>) that returns docs, types, source...

I could also be convinced that help should just be a global:

import * as git from "ws:git";
export default () => ({
  modules: help().docs,          // every module, one line each
  git: help(git),           // ws:git's interface, summary, declarations, and examples, as markdown
  cli: help(git.cli),      // one export's interface, declaration, with the types it names
  byName: help("ws:git"),  // a specifier works too, even for a module that failed to import
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants