Files
captoys/toybox
dckcandClaude Opus 5.5 7bf6eb44c7 capsite/toybox: a switchboard, the vats' only way out
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>
2026-10-13 21:44:29 -05:00
..

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: Vat stores code by SHA-256 and runs the app on it; Public and Admin are the two entrypoints; Egress is a vat's fetch.
  • 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".