Disposable release runner

English | 日本語

The repository-owned release runner uses one QEMU/KVM VM per job. It is separate from the PR CI Docker runner and performs no publication by itself. The parent delivery contract is Issue #988. This source provides a builder, guest guard and foreground one-job launcher; it installs no background service and does not activate daily publication. The manual-only rc.yml workflow reads the guest's admission record with read-only permissions. It has no checkout, third-party action or publication step. Dispatching it and registering a runner are separate operational effects.

Build and verify

Use Linux with KVM access, Python 3.11+, QEMU, qemu-img, cloud-localds, and OpenBSD nc (including -N), setpriv, and a systemd user manager with delegated cgroup memory and process controllers. The commands resolve root-owned executables outside user-writable paths. Python must start without inherited loader variables; a toolcache interpreter that needs LD_LIBRARY_PATH is rejected before VM execution. Keep the image on native Linux storage with room for a private copy and a 20 GiB guest disk. The VM uses two vCPUs and 4 GiB RAM. The PC must be awake and connected when a real job is expected.

Before QEMU starts, a transient user scope verifies non-root identities, zero capabilities, no-new-privileges, at most 128 tasks, 6 GiB total memory and no swap. These limits include proxy helpers. Guest RAM is excluded from QEMU core dumps. Proxy forwarding requires setsid, so its QEMU privilege syscall filter permits that operation under the verified OS restrictions. Two guest vCPUs are not a total host CPU quota. The host account and QEMU remain trusted; this does not isolate a compromised QEMU from other files owned by the same host account.

python3 scripts/build_release_vm.py --output /absolute/new/release.qcow2
python3 scripts/release_vm.py --image /absolute/new/release.qcow2 \
  --image-sha256 IMAGE_DIGEST --workflow-sha ACCEPTED_WORKFLOW_COMMIT

Use scripts/smoke_release_vm_network.py --image IMAGE --image-sha256 DIGEST to boot a credential-free guest against a temporary loopback HTTP fixture. It requires the explicit proxy path to work and direct host/internet paths to fail.

Save the builder's JSON readback, including the image, base, runner, guard and bootstrap digests. Review and retain that exact artifact before authorizing a registered run. The builder verifies the dated Ubuntu base and runner archive hashes, installs dependencies from a dated Ubuntu snapshot and includes the guard from the selected source tree. A build is not a claim of bit-identical images: its resulting image digest is the identity used for admission.

The default launcher mode sends no GitHub credentials and registers nothing. It boots a private disk, exercises successful and rejected guest observations, runs the guard under the unprivileged runner account, confirms evidence is not writable by that account, powers off and removes the private disk. This does not prove a real Actions job, proxy availability or unattended publication.

Separately authorized one-job execution

After the accepted image, exact rc.yml workflow revision and proxy have been reviewed, the following changes GitHub runner registrations and requires that specific authority:

python3 scripts/release_vm.py --image /absolute/new/release.qcow2 \
  --image-sha256 IMAGE_DIGEST --workflow-sha ACCEPTED_WORKFLOW_COMMIT \
  --proxy TRUSTED_PROXY_IPV4:PORT --register-one-job

The launcher uses the existing host gh authentication only for JIT registration and exact registration cleanup. It never copies that authentication into the guest. A fresh ai-teams-release-INSTANCE name and release-only label bind one JIT job. The disposable seed delivers the JIT value without command arguments or console output. Raw guest output is withheld on failure. The job's GITHUB_TOKEN still comes from GitHub and its permissions remain workflow-owned.

The guest is restricted to an explicit numeric host proxy through QEMU user network forwarding; no ordinary guest egress, inbound host port, host filesystem mount or Docker socket is configured. Review the proxy's destination policy separately. The credential-free builder uses ordinary networking to fetch its pinned inputs; that network is never selected for a registered guest.

The trusted launcher seals image/instance/registration/workflow/expiry evidence as a root-owned read-only guest file. The runner user has no sudo grant. The pre-job hook accepts only this repository's rc.yml on main, its exact reviewed workflow SHA, schedule or workflow_dispatch, matching runner name and attempt one. It claims admission once and grants no publication authority. A runner label or caller-supplied JSON outside this sealed image is not attestation.

GitHub runner v2.336.0 prepares action archives before the pre-job hook. The hook rejects workflow step execution; it does not promise that no untrusted archive was downloaded. The entire guest is discarded after any job, including a rejected job. This is not a runner group or server-side workflow restriction.

Failure and recovery

One invocation has at most one VM and one registration, a 30-minute lifetime, 4 MiB output bound, and a 20 GiB guest disk. The existing finite Linux process owner contains QEMU and its proxy children before scratch cleanup. Timeout, cancellation, wrong image, unsupported backing storage, stale evidence or a guest failure never reports successful execution. Exact registration cleanup uses the existing runner owner. A malformed/unknown registration response or incomplete cleanup requires provider/process reconciliation before retrying; do not infer absence or create another registration blindly.

No persistent service, pilot or live activation is installed by these commands. Keep manual release available until the workflow/provider acceptance and ten credential-separated windows in #988 pass.