arizuko

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:

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/runs from 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-vsock and /dev/net/tun on 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 the egress binary 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 pathwhat it ismode
/home/nodethe group folderRW
/opt/arizukothe skills and agent files of the releaseRO
/var/lib/sharethe shared scratch directory of the worldRW, or RO by grant
/var/lib/www/arizukothese docs, for every folderRO
/var/lib/www, ~/public_html, ~/private_htmlthe web tree and the two web folders of the folder, with the web:publish grantRO, RW, RW
/var/lib/groupsevery group, for an elevated /root turn onlyRW
/mnt/<name>an extra mount of the operator under MOUNT_ALLOWED_ROOTSRO
/run/ipca tmpfs in the guest only. The MCP socket lives here, and vsock bridges itRW

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