A vat granted a host (PUT /<vat>?egress=<host> on the admin socket) gets Egress as its globalOutbound, with the host in ctx.props where the vat can't change it. Egress forwards matching requests, WebSocket upgrades included, to the switchboard and refuses the rest with a 403; with no grant, fetch still throws. The switchboard is a separate workerd: a Switchboard DO that calls fetch and returns the response, so a 101's webSocket passes through. toybox reaches it over a unix socket in HTTP proxy style, which keeps the full URL, scheme included. Its network service is the outer fence, with TLS trusting the browser CAs; systemd units for it join toybox's, and only the toybox group can open its socket. The loaded Worker is now named by vat and incarnation, not by code hash: the same code with a different grant must not share a Worker. The new incarnation table sits beside the old ones; vats installed before it read as incarnation 0 with no grant. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
toybox
Vats whose code is installed at runtime: each vat is a Durable Object whose app runs as a facet on code loaded through workerd's Worker Loader. Think vatAdmin.createVat(bundle), with a supervisor per vat.
- supervisor.js:
Vatstores code by SHA-256 and runs the app on it;PublicandAdminare the two entrypoints;Egressis a vat'sfetch. - switchboard.js: a separate workerd process, the vats' only way out: a forward proxy for HTTP and WebSockets.
- config.capnp: the sockets have no addresses; whoever starts workerd hands them in, so holding a socket is the authority it grants.
| socket | reaches | what its holder can do |
|---|---|---|
http |
Public |
GET /<vat>/…: talk to a vat's app |
admin |
Admin |
PUT /<vat> with an ES module body: install it and (re)start the app; its storage carries over. ?egress=<host> grants the app that one host. |
An app must export class App extends DurableObject (see apps/). It gets an empty env. With no grant, fetch throws; with one, its globalOutbound is Egress with the granted host in ctx.props, which forwards matching requests, WebSocket upgrades included, to the switchboard and refuses the rest with a 403.
toybox reaches the switchboard over a unix socket (-e switchboard=unix:…), in HTTP proxy style so the full URL, scheme included, survives the hop.
Run it
pnpm install
pnpm test # real workerds: install, upgrade, isolation, egress grants, fetch and WebSocket out, crash and restart
pnpm switchboard # systemd-socket-activate on ./switchboard.sock
pnpm start # likewise on 127.0.0.1:8787 and ./admin.sock; state in ./state
Then, in a third terminal:
curl -X PUT --unix-socket admin.sock --data-binary @apps/counter-v1.js http://toybox/alice
curl localhost:8787/alice # v1 count 1
curl -X PUT --unix-socket admin.sock --data-binary @apps/counter-v2.js http://toybox/alice
curl localhost:8787/alice # v2 count 2
curl -X PUT --unix-socket admin.sock --data-binary @apps/egress-probe.js 'http://toybox/erin?egress=example.com'
curl 'localhost:8787/erin?url=https://example.com/' # got: <!doctype html>…
curl 'localhost:8787/erin?url=https://example.org/' # got: egress to example.org not granted
Under systemd
systemd/ has socket and service pairs for toybox and its switchboard, assuming this directory is installed at /opt/toybox. toybox gets no network of its own (PrivateNetwork=yes): only the two sockets it is handed and the switchboard's socket, which only members of the toybox group can open. The switchboard has the network, fenced by IPAddressDeny=; switchboard.capnp allows local addresses only so the tests can reach a local echo server.
Neither unit has been run under the system manager yet.
PrivateNetwork= needs the system manager; as a systemd-run --user unit, systemd logs "lack privileges for network namespace, proceeding without".