createVat (POST / on the admin socket) makes a vat with newUniqueId and returns its id, which addresses it only there. A vat is reachable only where it is mounted: ?mount=<labels> on each start, as ?egress= is granted; absent means none. The ingress reads a request's labels from its query string, plus a Toybox-Labels header for secret ones (a URL ends up in logs and Referers). It routes to the vat whose mount labels the request carries, every one: stricter than Flower's no-conflict match, since an ingress client holds no constrained handle and a label it omits must not match. Registration keeps Flower's rule, so a mount must conflict with every other and no request can carry two. The app keeps the whole path and sees its connection labels in Toybox-Connection-Labels. A vat from before ids lives at idFromName(name). PUT /by-name/<name> reaches it on the admin socket; it stays unreachable from outside until mounted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
5.5 KiB
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. Vats have no names: creating one returns its id, which addresses it only on the admin socket. Everything else a vat can do or be reached by is granted explicitly: egress by labels it may fetch within, ingress by labels it is mounted at.
- supervisor.js:
Vatstores code by SHA-256 and runs the app on it;Directoryholds the mounts;Public(the ingress) andAdminare the two entrypoints;Egressis an egress grant. - switchboard.js: a separate workerd process, the vats' only way out, after Flower's switchboard: each of its sockets serves a Cap'n Web
Handlewith fixed labels, andconstrain(labels)returns a narrower one. - labels.js: Flower's label rule, shared by both: labels only accumulate, a missing key matches anything, and a key both sides set must agree. Ingress lookup is stricter (below).
- 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 |
reach the vat mounted at labels the request carries |
admin |
Admin |
POST / with an ES module body: create a vat and start it; the reply is {"vat": <id>, "hash": <code hash>}. PUT /<id> with a body: upgrade it; its storage carries over. PUT /by-name/<name>: the same, for a vat from before ids. |
Every start states the vat's grants, as JSON labels in query parameters; absent means none:
?egress=<labels>: what the app may fetch (below).?mount=<labels>: where the ingress finds it. A mount must conflict with every other one (some shared key, different values), so no request can carry two mounts' labels; an overlapping mount is a 409.
Ingress
A request's labels are its query parameters, plus a Toybox-Labels header (JSON) for secret ones. A URL ends up in logs and Referers, so a secret label belongs in the header: a webkey page keeps the secret in its fragment and sends it in-band. A key given twice is a 400.
The request reaches the vat whose mount labels it carries, every one with the same value. That is stricter than Flower's match (no conflicting key): an ingress client holds no constrained handle, so a label it omits must not match, or a secret label would protect nothing. The path is the app's own. The app sees the mount's and the request's labels together in Toybox-Connection-Labels (JSON), as a Flower server gets its connection labels. Responses carry Referrer-Policy: no-referrer.
Egress
An app must export class App extends DurableObject (see apps/). Its global fetch always throws: nothing in a vat is ambient. A grant arrives as env.egress, an Egress with the labels in ctx.props. Its fetch, WebSocket upgrades included, goes through a switchboard handle constrained to those labels; the switchboard refuses the rest, which comes back as a 403. env.egress.constrain(labels) returns a narrower Egress. It is an object, so the app can pass it to whichever of its parts should have it.
| label | constrains |
|---|---|
scheme |
http or https |
host |
host name or address, no port |
port |
explicit, or the scheme's default |
network |
set by the switchboard socket: public or local |
The switchboard has two sockets, so two root handles: public (no loopback or private addresses) and local. toybox holds one, as an external service (-e switchboard=unix:…): pnpm start and the systemd units give it public; the tests give theirs local to reach an echo server.
Run it
pnpm install
pnpm test # real workerds: createVat, upgrade, mounts, secret labels, egress grants and constrain, fetch and WebSocket out, crash and restart
pnpm switchboard # systemd-socket-activate on ./switchboard-public.sock and ./switchboard-local.sock
pnpm start # likewise on 127.0.0.1:8787 and ./admin.sock; state in ./state
Then, in a third terminal:
admin() { curl -sg --unix-socket admin.sock "$@"; }
alice=$(admin -X POST --data-binary @apps/counter-v1.js 'http://toybox/?mount={"vat":"alice"}' | jq -r .vat)
curl 'localhost:8787/?vat=alice' # v1 count 1
admin -X PUT --data-binary @apps/counter-v2.js "http://toybox/$alice?mount={\"vat\":\"alice\"}"
curl 'localhost:8787/?vat=alice' # v2 count 2
admin -X POST --data-binary @apps/egress-probe.js 'http://toybox/?mount={"vat":"erin"}&egress={"host":"example.com"}'
curl 'localhost:8787/?vat=erin&url=https://example.com/' # got: <!doctype html>…
curl 'localhost:8787/?vat=erin&url=https://example.org/' # got: egress refused: 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 public socket, which only members of the toybox group can open. The switchboard binds its local socket in its own 0700 runtime directory, so no other unit holds that handle.
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".