arizuko › components › runed
runed
runed starts one KVM guest for each turn, bounds it with one deadline and discards it when the turn ends.
runed starts one KVM guest for each turn
runed runs the agent. When a turn must happen, runed starts one new, empty KVM guest with qemu-system-x86_64, lets the agent work inside it, and then discards the guest. This is the only substrate, and no setting selects another one.
runed is the execution plane. It serves the work queue, starts one KVM guest per turn, and enforces the run timeout. It holds /dev/kvm, /dev/vhost-vsock and /dev/net/tun, and the NET_ADMIN and SYS_ADMIN capabilities. It mounts no Docker socket. It never appends a message, because routd is the only appender. It neither signs nor creates a token. authd is the only signer, and runed reads the public keys of authd only to verify the callers of its own /v1.
runed owns its tables and creates them: the runtime tables spawns, session_log and circuit_breaker, which have no place in routd.
Guest control needs root-level access, so one small daemon holds it
A turn is one short-lived KVM guest, as the Turn primitive describes. Some daemon must hold /dev/kvm and the other device nodes that a guest needs, and only one daemon should. That access equals root on the host, so one small daemon keeps the damage of a fault small. runed is that daemon.
A guest needs more than qemu-system-x86_64 -m 2G. For each turn, runed does the following:
- It builds the staging tree and starts
virtiofsdfor it. - It brings up the tap of the guest and its rule set, and starts the egress proxy on the host side of that tap.
- It enforces
RUNED_RUN_TIMEOUT, so a runaway turn cannot run forever. - It steers a live run over the vsock
ctlframe. - It stops the guest cleanly and reaps every pid that the record of the turn names.
runed gives the guest no token. A host kernel fact keeps a turn inside its folder, across two steps. The first step is the vsock CID of the guest, which the vhost driver of the host lets no other guest claim. The second step is a uid-1000 bridge that accepts a connection only from that CID and connects only to the folder socket of that turn. So the guest holds no credential to steal.
Without runed, routd would have to hold those device nodes and mix the routing plane with guest control on the host. With two daemons, the daemon that talks to the network, routd, is not the daemon that can start processes on the host, runed.
runed sits between routd and the guest
routd
│ POST /v1/runs (scope runs:run)
▼
runed verify the caller ◄── authd public keys
│ start the egress proxy on the tap's host side; wait for /health
│ qemu-system-x86_64: one guest, its virtiofs share, its tap
▼
KVM guest runs one turn, then powers off
│ agent tools ──► routd, over vsock through the uid-1000 bridge
│ (MCP socket hosted in routd, not runed)
└── timeout / ctl steer / power off + reap every pid ──► runed
- Inputs
POST /v1/runsfrom routd, which is the contract between the two daemons. The public keys of authd, fetched once at start, which verify those requests.- Outputs
- A running guest. Status and kill on
/v1/runs/{run_id}. Spawn and session rows in its own tables. - Dependencies
/dev/kvm,/dev/vhost-vsockand/dev/net/tunon the host. authd as the JWKS source. routd, which hosts the MCP socket of each turn and receives the tool calls of the agent. runed also starts one egress proxy per turn on the tap of that guest and stops it with the turn. This is not a setting: runed refuses to start if theegressbinary is missing.
The guest sees two shares and a mount table
Everything that the agent can read or write reaches the guest through two virtio-fs shares. One is the release root filesystem, read-only. The other is a staging tree that runed builds for each turn. It has the same layout as the paths of the guest, with a mounts.json at its root that tells guest-init what to bind where.
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ release rootfs │ │ host paths, as runed sees │
│ <data>/images/rootfs-<hash>/ │ │ them: the mount table below │
└──────────────┬───────────────┘ └──────────────┬───────────────┘
│ read-only bind │ one bind per entry;
│ │ RO is a host remount
▼ ▼
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ <turn dir>/rootfs │ │ <turn dir>/tree: guest paths,│
│ │ │ plus mounts.json │
└──────────────┬───────────────┘ └──────────────┬───────────────┘
│ virtiofsd as root │ virtiofsd as the
│ share "root" │ agent's uid, share
│ │ "turn"
host ────────┼─────────────────────────────────┼──────────── guest
▼ ▼
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ overlay: rootfs lower, │ │ /turn, then one bind per │
│ 512m tmpfs upper: the │ │ manifest entry onto its │
│ agent's /, gone at power-off │ │ guest path │
└──────────────────────────────┘ └──────────────────────────────┘
/run/ipc: a 16m guest tmpfs, never on a share; /dev/shm: 1g tmpfs
Read-only means read-only on the host. runed remounts each read-only bind before virtiofsd starts, so a write fails with EROFS whatever the guest kernel does. A virtiofsd that runs as the uid of the agent serves the writable share. So a guest that claims to be root still cannot leave a file owned by root in the folder.
| guest path | what it is | mode |
|---|---|---|
/home/node | the group folder | RW |
/opt/arizuko | the skills and agent files of the release | RO |
/var/lib/share | the shared scratch directory of the world | RW, or RO by grant |
/var/lib/www/arizuko | these docs, for every folder | RO |
/var/lib/www, ~/public_html, ~/private_html | the web tree and the two web folders of the folder, with the web:publish grant | RO, RW, RW |
/var/lib/groups | every group, for an elevated /root turn only | RW |
/mnt/<name> | an extra mount of the operator under MOUNT_ALLOWED_ROOTS | RO |
/run/ipc | a tmpfs in the guest only. The MCP socket lives here, and vsock bridges it | RW |
The section Mounts of [runed/README.md] has the full table with the host paths.
One deadline bounds the guest and the agent
RUNED_RUN_TIMEOUT is the run ceiling, 20 minutes by default. It sets two limits at once. One is the hard stop of the guest. The other is the query timeout of the agent, ARIZUKO_QUERY_TIMEOUT_MS, which is RUNED_RUN_TIMEOUT − 30s. The 30-second gap lets the agent stop and deliver a summary before runed stops the guest. On a graceful shutdown, runed detaches the running guests. A guest outlives the daemon, so a restart does not stop a turn in the middle. The next runed reaps the guest from the record on disk.
runed runs alone only with its host preconditions
runed runs as a daemon, but it needs the preconditions of the KVM substrate and an authd whose keys it can fetch. It also expects routd to host the MCP socket that the guest calls. Run it only with those.
cd /srv/data/myinstance
export DATA_DIR=/srv/data/myinstance
export LISTEN_ADDR=:8080
export AUTHD_URL=http://authd:8080
export RUNED_RUN_TIMEOUT=20m
export RUNED_KERNEL_DIR=/srv/data/myinstance/images/kernel-6.1.112-40e1508980d0
export RUNED_ROOTFS_DIR=/srv/data/myinstance/images/rootfs-a35045d89e3e
# the host must expose /dev/kvm, /dev/vhost-vsock and /dev/net/tun
./runed
Without AUTHD_URL, the verifier is open. Use that only for local development. GET /health returns 200 when the process is up. Spawns that stay in state=queued show a problem: the concurrency cap is full, or a precondition of the guest went away while the daemon ran. A precondition missing at boot fails earlier: runed refuses to start, and the refusal names every unmet precondition at once.
runed does not route, append or sign
runed does not route messages, append to the message log or sign tokens. It takes a run request, starts a guest, enforces its limits and reports the result. It is the execution plane and nothing more.
Six pages cover the details
- concepts/primitives: the Turn primitive, one short-lived guest run per event.
- components/egress: the proxy that runed starts for each turn, and why the rule set of the tap confines the agent.
- reference/env: runed: every variable the substrate reads, with its default. The wiring between daemons is under split daemons, and what reaches the agent is under agent spawn.
- components/routd: the daemon that sends the run and hosts the MCP socket that the guest calls.
- [runed/README.md]: the run contract, the timeout arithmetic and the file map.
- concepts/turn: the whole turn in one figure, with the part of runed.