Files
captoys/toybox
dckcandClaude Opus 5.5 a984461cee capsite/toybox: ingress is a capability; a hello-world web vat
E(vatAdmin).makeIngress(labels) reserves a mount and returns an ingress;
whoever holds it serves requests carrying those labels by calling
E(ingress).serve(handler). A vat gets one in its params, like any other
remote object; nothing in a vat's module is an entrypoint. A request
reaches the handler as passable data (method, path, labels, body), and
the response comes back as data (status, headers, body). The handler is
a live reference for now: after a restart its mount answers 503.

vats/hello.js serves hello, world; pnpm hello creates it on a running
toybox (client.js is the Node OCapN client, shared with the tests).

pnpm start and pnpm switchboard never ran: systemd-socket-activate wants
absolute paths for unix sockets.

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. 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: Vat stores code by SHA-256 and runs the app on it; Directory holds the mounts; Public (the ingress) and Admin are the two entrypoints; Egress is 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 Handle with fixed labels, and constrain(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; a key given twice is a 400. Routing carries no secrets: a URL ends up in logs and Referers. A secret travels in-band, as on a webkey page: the page keeps it in its fragment, reaches the vat by public labels, and sends it in the body of a call, which the vat checks like a swissnum.

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 that supplies labels for it, so it reaches a vat only by naming every label of its mount. 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, 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".