Skip to content

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 ​

SetupRunner placementBackend
Single server (Compose)Same host as the rest of the stackdocker-runsc (gVisor)
k3sPods 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.sockkubernetes-runsc or docker-runsc
Hosted editionDedicated node without database, encryption key or provider tokens; Valkey over WireGuard with the sandbox ACL user and the signing key onlydocker-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.

  1. Install runsc from a pinned gVisor release (apt repository or the release binary with its sha512 checksum, see gvisor.dev/docs/user_guide/install).

  2. Register it with Docker using the systrap platform:

    sh
    sudo runsc install -- --platform=systrap
    sudo systemctl restart docker

    /etc/docker/daemon.json then contains:

    json
    { "runtimes": { "runsc": { "path": "/usr/local/bin/runsc", "runtimeArgs": ["--platform=systrap"] } } }
  3. Check the runtime:

    sh
    docker info --format '{{json .Runtimes}}'                 # lists runsc
    docker run --rm --runtime=runsc busybox dmesg | head -n 2  # prints "Starting gVisor..."

Enable the runner ​

sh
./deploy/compose/quavon install --sandbox

With --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:

FileContent
sandbox_signing_keyKey (base64) that signs jobs and results between worker and runner and encrypts snapshots
valkey_sandbox_password, valkey_sandbox_urlValkey ACL user sandbox for the runner (sandbox keys only)
valkey_egress_password, valkey_egress_urlValkey 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:

sh
./deploy/compose/quavon doctor --sandbox-smoke

It 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.

  1. KVM check. grep -cE 'vmx|svm' /proc/cpuinfo prints more than 0, ls -l /dev/kvm exists, and sudo modprobe kvm_intel (or kvm_amd) succeeds.

  2. Host hardening. Turn SMT off (echo off | sudo tee /sys/devices/system/cpu/smt/control, and the kernel argument nosmt to keep it off), keep the host kernel at 6.1 or newer with current microcode, and use cgroup v2 only (/sys/fs/cgroup/cgroup.controllers lists cpu memory pids). Run nothing else on this host: no database, no QUAVON_ENCRYPTION_KEY, no provider tokens.

  3. 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: wg0 with the Valkey host as the single peer and AllowedIPs = <valkey-ip>/32. The firewall OUTPUT chain allows UDP to the WireGuard endpoint, DNS and NTP if needed, and HTTPS from the egress proxy only if you allow registry egress.

  4. Valkey ACL. Use the sandbox user 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 gets VALKEY_URL=redis://sandbox:<password>@<valkey-wg-ip>:6379 and QUAVON_SANDBOX_SIGNING_KEY_FILE. Check the ACL with valkey-cli --user sandbox --pass <password> GET quavon:anything, which must answer NOPERM.

  5. Install. sudo deploy/images/firecracker/install-firecracker.sh /usr/local installs the pinned firecracker and jailer after checking their sha256. Build the guest kernel and root filesystems from the sandbox images (pinned by digest in a registry) with deploy/images/firecracker/build.sh deploy/images/sandbox/images.lock.json ./fc-out /opt/quavon/firecracker and copy vmlinux, *.ext4 and images.lock.json to /opt/quavon/firecracker/ (owned by root, mode 0444). Install iproute2, e2fsprogs and nftables.

  6. 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 use 200000 up to 200000 + concurrency - 1) and QUAVON_SANDBOX_CONCURRENCY at most half the cores.

  7. Registry egress (optional). Run sandbox-egress on 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 on 10.201.255.1:3128) and set QUAVON_SANDBOX_EGRESS_PROXY=10.201.255.1:3128 (an IPv4 literal), QUAVON_SANDBOX_EGRESS_NETWORK=firecracker and QUAVON_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 uses 10.201.0.0/16 and 172.16.0.0/30 for VM networks; the host must not use them.

  8. Smoke test. quavon doctor --sandbox-smoke must report backend firecracker, runtime firecracker v1.17.0, platform kvm, 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:

    sh
    sudo -E env "PATH=$PATH" QUAVON_TEST_FIRECRACKER_LOCK=/opt/quavon/firecracker/images.lock.json \
      pnpm --filter @quavon/sandbox-runner test:kvm

    It 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.

  9. Updates. Firecracker, the kernel and the root filesystems are pinned by sha256 in deploy/images/firecracker/versions.env and images.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 from fc-out/kernel-source/ with it (see deploy/images/firecracker/SOURCE-OFFER.md).

Deterministic checks &ZeroWidthSpace;

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:

yaml
tools:
  sast:
    disable: [gosec]
    ignorePaths: ['vendor/**']

Draft fixes &ZeroWidthSpace;

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 &ZeroWidthSpace;

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.

The open core is released under the Apache License 2.0.