Sandbox runner ​
The sandbox runner executes repository code (deterministic scanners, and the tests and linters of the fix agent) in throwaway containers or Firecracker microVMs. It is an optional Compose service (profile sandbox) and is the only service that talks to the Docker daemon. Design and threat model: ADR-0012 and fix-agent-and-sandbox.md.
The runner pulls signed jobs from Valkey; nothing connects into it and it has no HTTP port. Snapshots and results travel encrypted through Valkey, so the runner shares no volume with the worker.
Placement ​
| Setup | Runner placement | Backend |
|---|---|---|
| Single server (Compose) | Same host as the rest of the stack | docker-runsc (gVisor) |
| k3s | Pods with runtimeClassName: gvisor (see kubernetes.md), or a dedicated, tainted sandbox node with Docker Engine next to containerd where only the runner pod tolerates the taint and mounts that node's /var/run/docker.sock | kubernetes-runsc or docker-runsc |
| Hosted edition | Dedicated node without database, encryption key or provider tokens; Valkey over WireGuard with the sandbox ACL user and the signing key only | docker-runsc; Firecracker on bare metal is preferred |
Residual risk on a single server: the runner and the control plane share one kernel. gVisor intercepts the system calls of sandboxed code, but a gVisor escape would land next to PostgreSQL, Valkey and the encryption key. Use a separate sandbox host when you review untrusted contributions at scale. In the enterprise edition quavon doctor fails (QD955) when the runner reports the same host as the API.
Hosts without nested virtualisation (no vmx/svm, no /dev/kvm) use gVisor with the systrap platform. docker-runc-insecure exists only for local development on Docker Desktop and is refused in production and in the enterprise edition.
Install gVisor ​
Requirements: Linux x86_64 or arm64, Docker Engine 24 or newer. KVM is not needed.
Install
runscfrom a pinned gVisor release (apt repository or the release binary with its sha512 checksum, see gvisor.dev/docs/user_guide/install).Register it with Docker using the
systrapplatform:shsudo runsc install -- --platform=systrap sudo systemctl restart docker/etc/docker/daemon.jsonthen contains:json{ "runtimes": { "runsc": { "path": "/usr/local/bin/runsc", "runtimeArgs": ["--platform=systrap"] } } }Check the runtime:
shdocker info --format '{{json .Runtimes}}' # lists runsc docker run --rm --runtime=runsc busybox dmesg | head -n 2 # prints "Starting gVisor..."
Enable the runner ​
./deploy/compose/quavon install --sandboxWith --sandbox (or when COMPOSE_PROFILES in .env already contains sandbox) the installer requires runsc in docker info, reads the group of the Docker socket, builds the sandbox images with deploy/images/sandbox/build.sh (this writes images.lock.json with the image digests), writes COMPOSE_PROFILES=sandbox and QUAVON_DOCKER_GID to .env, and waits for sandbox-runner and sandbox-egress.
The installer always creates these secrets, also without --sandbox:
| File | Content |
|---|---|
sandbox_signing_key | Key (base64) that signs jobs and results between worker and runner and encrypts snapshots |
valkey_sandbox_password, valkey_sandbox_url | Valkey ACL user sandbox for the runner (sandbox keys only) |
valkey_egress_password, valkey_egress_url | Valkey ACL user egress for the egress proxy (read-only egress tokens) |
The worker uses the sandbox when it has the signing key (QUAVON_SANDBOX_SIGNING_KEY_FILE, always set by the Compose file). Enable deterministic checks and draft fixes only after the runner passes the smoke test. The worker checks the runner health records before it queues a job: without a healthy runner that passed its escape smoke test and offers the image, a review records the scan as failed (no healthy sandbox runner) and continues, and a fix run ends as ineligible with sandbox_unavailable.
Registry egress for setup steps is off by default. To allow it, list the registries in .env, for example QUAVON_SANDBOX_REGISTRIES=registry.npmjs.org, and re-run the installer.
Smoke test ​
Run the escape smoke test on the target host after installing and after every gVisor or Docker upgrade:
./deploy/compose/quavon doctor --sandbox-smokeIt must report QD951 with backend docker-runsc, runtime runsc, platform systrap and QD957 "escape smoke test passed". The runner also repeats the smoke test at startup and every 6 hours. Any failure from QD952 to QD958 means: do not enable deterministic checks or the fix agent for that runner until it is resolved. The codes are listed in doctor.md.
Firecracker on a KVM host ​
Use the firecracker backend on a bare-metal server or a home server with hardware virtualisation. Every job runs in its own microVM, started by the Firecracker jailer with its own uid, cgroup, network namespace and chroot. The control plane can stay on another host; the runner only needs outbound access to Valkey. A VPS without nested virtualisation (for example the netcup k3s host) cannot run Firecracker; use docker-runsc there. A configured firecracker backend without a usable /dev/kvm refuses to start and never falls back to another backend.
KVM check.
grep -cE 'vmx|svm' /proc/cpuinfoprints more than 0,ls -l /dev/kvmexists, andsudo modprobe kvm_intel(orkvm_amd) succeeds.Host hardening. Turn SMT off (
echo off | sudo tee /sys/devices/system/cpu/smt/control, and the kernel argumentnosmtto keep it off), keep the host kernel at 6.1 or newer with current microcode, and use cgroup v2 only (/sys/fs/cgroup/cgroup.controllerslistscpu memory pids). Run nothing else on this host: no database, noQUAVON_ENCRYPTION_KEY, no provider tokens.Network. Put the host on an isolated segment (its own VLAN or interface), inbound closed except SSH from a management address. Outbound is WireGuard to the control plane only:
wg0with the Valkey host as the single peer andAllowedIPs = <valkey-ip>/32. The firewallOUTPUTchain allows UDP to the WireGuard endpoint, DNS and NTP if needed, and HTTPS from the egress proxy only if you allow registry egress.Valkey ACL. Use the
sandboxuser of the Compose stack (keys~quavon-sandbox:*, channels&quavon-sandbox:*, only the commands BullMQ needs, its own password) and bind Valkey for this peer only on its WireGuard address. The runner getsVALKEY_URL=redis://sandbox:<password>@<valkey-wg-ip>:6379andQUAVON_SANDBOX_SIGNING_KEY_FILE. Check the ACL withvalkey-cli --user sandbox --pass <password> GET quavon:anything, which must answerNOPERM.Install.
sudo deploy/images/firecracker/install-firecracker.sh /usr/localinstalls the pinnedfirecrackerandjailerafter checking their sha256. Build the guest kernel and root filesystems from the sandbox images (pinned by digest in a registry) withdeploy/images/firecracker/build.sh deploy/images/sandbox/images.lock.json ./fc-out /opt/quavon/firecrackerand copyvmlinux,*.ext4andimages.lock.jsonto/opt/quavon/firecracker/(owned by root, mode 0444). Installiproute2,e2fsprogsandnftables.Run the runner as root (systemd service, Node 22.18 or newer) with
QUAVON_SANDBOX_BACKEND=firecracker,QUAVON_FIRECRACKER_IMAGES_FILE=/opt/quavon/firecracker/images.lock.json,QUAVON_FIRECRACKER_CHROOT_BASE=/srv/jailer(on the same filesystem as the images),QUAVON_FIRECRACKER_UID_BASE=200000(no user or group may use200000up to200000 + concurrency - 1) andQUAVON_SANDBOX_CONCURRENCYat most half the cores.Registry egress (optional). Run
sandbox-egresson this host on a dedicated address (ip link add quavon-egress0 type dummy; ip addr add 10.201.255.1/32 dev quavon-egress0; ip link set quavon-egress0 up, proxy on10.201.255.1:3128) and setQUAVON_SANDBOX_EGRESS_PROXY=10.201.255.1:3128(an IPv4 literal),QUAVON_SANDBOX_EGRESS_NETWORK=firecrackerandQUAVON_SANDBOX_REGISTRIES. VM namespaces may reach only that address and port, and only during the setup step. Leave it unset to keep every job without network. The runner uses10.201.0.0/16and172.16.0.0/30for VM networks; the host must not use them.Smoke test.
quavon doctor --sandbox-smokemust report backendfirecracker, runtimefirecracker v1.17.0, platformkvm,QD957"escape smoke test passed", and a runner host id different from the API host. For the full suite, stop the runner service and run on the host, from a checkout of the same release:shsudo -E env "PATH=$PATH" QUAVON_TEST_FIRECRACKER_LOCK=/opt/quavon/firecracker/images.lock.json \ pnpm --filter @quavon/sandbox-runner test:kvmIt boots real VMs and checks the escape probes, the egress rules, wall-clock kills and cleanup. Re-run it after every Firecracker, kernel or image update.
Updates. Firecracker, the kernel and the root filesystems are pinned by sha256 in
deploy/images/firecracker/versions.envandimages.lock.json; the runner refuses to start on any mismatch. Rebuild, replace/opt/quavon/firecracker/*and restart the runner (stale jails and snapshots are removed at startup). The guest kernel is GPL-2.0; ship its source tarball and configuration fromfc-out/kernel-source/with it (seedeploy/images/firecracker/SOURCE-OFFER.md).
Deterministic checks ​
Deterministic checks (ast-grep with the Quavon rule pack, gitleaks, bandit, gosec, Ruff, ESLint with eslint-plugin-security) run once per review in the scanners image. Findings on changed lines join the review with a "deterministic" badge and a summary section. They are off by default; enable them per repository under Deterministic checks on the repository's Policy tab, or through the policy API (PUT /api/tenants/{tenantId}/repositories/{repositoryId}/policy, features.sast.mode = shadow to store findings only, or on to post them).
gosec runs with the Go 1.26 toolchain of the scanners image so that its type-based rules work. Modules whose go.mod requires Go 1.27 or newer cannot be loaded by it; gosec then reports a tool error for that run.
Repositories can disable tools or ignore paths in .quavon/config.yml on the base branch:
tools:
sast:
disable: [gosec]
ignorePaths: ['vendor/**']Draft fixes ​
Draft fixes (features.fix) use the same runner for the policy's setup, test and lint commands. They are off for every repository by default. Enable them per repository under Draft fixes on the repository's Policy tab, or through the policy API (features.fix.mode = shadow, approval or auto; auto is downgraded to approval on public repositories) together with the Fixer app installation. Read the threat analysis before enabling them, in particular the CI secrets note: the draft PR's branch triggers the repository's workflows.
Fixes for review findings (features.fix.reviewFixes) run on the same runner with the same commands, image, egress and limits; only the input differs (the findings of a review run instead of an issue) and the checkout is the pull request head. A commit to the pull request branch triggers that branch's workflows with the repository's secrets. Review fixes stay unavailable until the review fix part of the threat analysis is signed off.
Upgrades ​
quavon install --yes rebuilds the sandbox images and images.lock.json. Images are referenced by digest only; the runner refuses tags. Bring-your-own images are added to images.lock.json by hand and need a rebuild of the runner image, or mount the file and set QUAVON_SANDBOX_IMAGES_FILE.