Skip to content

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

Draft
mattzcarey wants to merge 3 commits into
mainfrom
cf-computer-introspection
Draft

mattzcarey wants to merge 3 commits into
mainfrom
cf-computer-introspection

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Oct 14, 2026 •

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 cloudflare: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 "cloudflare: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.
  • Errors that point back. ws:git refuses anything other than one options object of the options it takes, or an argv starting with "git", with the shape it expects. Calling an export a host module lacks, or importing a module that isn't configured, names the docs(...) call to make.
  • Scripts. A module with no exports runs as the body of the default export, so import * as git from "ws:git"; console.log(git) works as a first probe, as do top-level await, I/O, and return. Top-level console.log output, which was dropped before, is kept.
  • Breaking. The cloudflare:* 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. The pi harness example also ran against this branch with its hand-written ws:git declarations removed, so the model knew only that ws:git and cloudflare:computer exist. In four runs of two git tasks on the final code, it read the declarations by its fourth call at the latest, by printing the namespace or calling docs, and finished every task. Wrong guesses made before that were caught by the errors above.

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: b677dc7

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")}
Comment thread .changeset/cf-computer-introspection.md Outdated
"@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
});

Models given only a module's name rarely read its declarations before
calling it. They guess, and learn from the first error, so errors are
where the declarations need to be found.

A call to an export a host module doesn't have fails with a bare
"is not a function". The run now names the docs() call that lists the
module's exports. An import of a module that isn't configured, such as
node:child_process to run git, names docs(). ws:git refuses an option
it doesn't take, such as log({ cwd }), and lists the ones it does,
where before the option was ignored and the call ran at the backend
root. The cf:computer line in the description asks for a module's
docs before its first call.

The natural first probe, `import * as git from "ws:git";
console.log(git)`, failed before it ran, because the module has no
default export. A module with no exports is now a script: its imports
stay at the top and the rest becomes the body of the default export,
so top-level await, I/O, and return work. A module with named exports
but no default still fails.

Output from the top level of a module was dropped, because the stream
write is refused while the module evaluates. It is now held until the
import settles.
@pkg-pr-new

pkg-pr-new Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

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

commit: b677dc7

The built-in introspection module now lives under the cloudflare:
scheme, beside the runtime's own cloudflare:workers and
cloudflare:sockets. The whole cloudflare: namespace is reserved for
built-in modules: no configured module may use it, and importing any
other cloudflare: module, such as cloudflare:workers, fails before the
Worker is created.
@mattzcarey mattzcarey changed the title computer: Let isolate code read module declarations from cf:computer computer: Let isolate code read module declarations from cloudflare:computer Oct 19, 2026
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