Skip to content

computer: Pass isolate capability calls as native RPC values - #183

Open
mattzcarey wants to merge 3 commits into
feat/ws-container-modulefrom
feat/native-rpc-modules
Open

mattzcarey wants to merge 3 commits into
feat/ws-container-modulefrom
feat/native-rpc-modules

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Stacked on #172, after #182 merged into it.

The Dynamic Worker already gets a real RPC object, the runtime bridge. Even so, every node:fs and host module call was JSON-encoded on top of it:

// before, inside the isolate
const request = JSON.stringify(args.map(encode));       // bytes → { __workspace_codec__: { type: "bytes", data: [255, 255, …] } }
const raw = await host.call(name, request);             // string in
return decode(JSON.parse(raw).result);                  // string out

Bytes travelled as arrays of numbers, about four times their real size against maxCapabilityBytes, and both sides carried an encoder and a decoder.

Values now cross as Workers RPC values:

// after
const payload = await host.call(name, args);            // real values in
if (payload.error) throw rebuild(payload.error);         // { message, code, path }
return payload.result;                                   // real values out
sequenceDiagram
  participant Code as Isolate code
  participant Bridge as Host bridge (proxy)
  participant Host as node:fs / host module
  Code->>Bridge: call("fs.writeFile", ["/a.bin", Uint8Array])
  Note over Bridge: cancelled? concurrency, call count,<br/>measure bytes, reject non-plain data
  Bridge->>Host: run with deadline and abort signal
  Host-->>Bridge: value
  Note over Bridge: measure response, response budget
  Bridge-->>Code: { result } or { error }
Loading

The bridge stays the one proxy every call goes through, so its controls are unchanged:

  • cancellation
  • concurrent and total call counts
  • per-call deadlines with an abort signal
  • draining accepted calls before an execution ends, so a late write can't land after exit

Byte budgets now measure the values themselves: UTF-8 bytes of strings and keys, raw bytes of byte arrays, and a fixed cost per scalar. The same walk rejects anything that is not plain data, including cycles. That includes functions and RPC stubs, which Workers RPC would otherwise carry into the Durable Object as live callbacks. The isolate keeps a rough size check so an obviously oversized request skips the round trip.

What changes for callers: bytes count at their real size, so a 900-byte write now fits under a 1024-byte limit where it used to be rejected. A new script runner test shows this; it fails on the old codec with "capability request exceeds 1024 bytes". Host module arguments and results are still JSON-compatible plain data, normalized as JSON would: an undefined object field is left out and an undefined array item becomes null.

Every response counts against the per-execution response budget, errors included. An error keeps its code and path only if the whole error still fits maxPayloadBytes. Every value costs at least one byte, so a request of many empty strings or objects is bounded by the byte limit too. Allowing byte arrays or streams there, for example to stream ws:container output, is now a small follow-up rather than a codec change.

The script runner suite runs every path through a real Dynamic Worker: node:fs bytes and errors with codes, host modules, the call, concurrency, and byte limits, and oversized errors. The bridge unit tests add plain-value pass-through, rejection of functions, class instances, and cycles, and UTF-8 sizing.


Devin Review

@mattzcarey mattzcarey added the allow-pr Allow a PR to remain open. label Oct 1, 2026
@changeset-bot

changeset-bot Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 039f65c

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 Patch
@cloudflare/dofs Patch
@cloudflare/computer-rpc Patch
@cloudflare/computerd Patch

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

devin-ai-integration[bot]

This comment was marked as resolved.

@pkg-pr-new

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

Copy link
Copy Markdown

Open in StackBlitz

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

commit: 2aa11a6

@mattzcarey
mattzcarey force-pushed the feat/native-rpc-modules branch from 5d1931c to aa38a8f Compare October 1, 2026 11:01
Base automatically changed from fix/exec-tool-review to feat/ws-container-module October 1, 2026 11:02
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@mattzcarey
mattzcarey force-pushed the feat/ws-container-module branch 3 times, most recently from 9290816 to ba4b679 Compare October 6, 2026 21:22
@mattzcarey
mattzcarey force-pushed the feat/native-rpc-modules branch 2 times, most recently from 039f65c to 08ef2ea Compare October 6, 2026 21:37
The Dynamic Worker already received a real RPC object, the runtime
bridge, but every node:fs and host module call was JSON-encoded on top
of it: arguments became a string with a custom codec for bytes,
arrays, and objects, and results came back the same way. Bytes
travelled as arrays of numbers, roughly four times their size against
the capability byte limit, and both sides carried an encoder and a
decoder.

Arguments and results now cross as Workers RPC values. The isolate
passes its argument list straight to host.call, and the bridge answers
with { result } or a bounded { error } carrying code and path for
node:fs. The bridge stays the single proxy for every call, so its
controls are unchanged: cancellation, concurrent and total call
counts, per-call deadlines with abort, and draining accepted calls
before an execution ends.

Byte budgets now measure the values themselves: UTF-8 bytes of strings
and keys, raw bytes of byte arrays, and a fixed cost per scalar. The
same walk rejects anything that is not plain data, including functions
and RPC stubs that Workers RPC would otherwise carry into the host as
live callbacks, and cycles.
- Error responses now count against the cumulative response budget.
- An error's path and code are kept only if the whole error still fits
  the payload limit; the message is cut to what is left.
- Every value costs at least one byte, so many empty strings or objects
  can no longer reach the host past the request limit.
- Host module arguments and results follow JSON again: undefined object
  fields are left out and undefined array items become null, so a host
  value with an optional field no longer fails the execution.
@mattzcarey
mattzcarey force-pushed the feat/native-rpc-modules branch from 08ef2ea to 2aa11a6 Compare October 6, 2026 22:59
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.

1 participant