All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Daemon default listener is now AF_UNIX (0o600), TCP is opt-in with bearer-token auth.
voidbox serveno longer binds127.0.0.1:43100by default. The daemon resolves a per-uid AF_UNIX socket path via the chain$XDG_RUNTIME_DIR/voidbox.sock→$TMPDIR/voidbox-$UID.sock→/tmp/voidbox-$UID.sockand binds it with mode0o600; thevoidboxCLI client consults the same chain so a same-uid invocation auto-discovers the socket. To opt back into TCP, pass--listen tcp://host:portand provide a bearer token via--token-file,VOIDBOX_DAEMON_TOKEN_FILE, orVOIDBOX_DAEMON_TOKEN; if none is set, the daemon generates a 32-byte hex token and writes it to$XDG_CONFIG_HOME/voidbox/daemon-token(default~/.config/voidbox/daemon-token, mode0o600); thevoidboxCLI reads from this same path as a tier-3 fallback below the env vars, so the typical same-host TCP case auto-discovers the token with no further configuration. The daemon refuses to start a TCP listener with no token. All routes (includingPOST /v1/runs,GET /v1/runs/{id}/telemetry,.../stages/{name}/output-file,POST .../cancel,POST .../messages) inherit the bearer-token gate from a single chokepoint at the top ofroute_request; comparison is constant-time viasubtle::ConstantTimeEq. Closes the local cross-user RCE described as R-B4.1 / T-B4.1 in the threat model. Migration: scripts that passed--listen 127.0.0.1:43100should now pass--listen tcp://127.0.0.1:43100and configure a token; same-uid clients work without further action.
- TCP port-forward rules accept a host port of 0, meaning "bind an OS-assigned free port". The resolved
(host_port, guest_port)pairs are reported by the newSlirpBackend::port_forward_listener_ports(). This also deflakestcp_port_forward_inbound_connect_succeeds(#129): the affected tests now bind port 0, removing the fixed-port collision (a foreign listener could absorb the test's connect while the rule's bind failure is warn-only), and the test's second drain pass now applies full contract matching — a synthesized SYN surfacing there was previously discarded unrecognized, burning the test's deadline. - Experimental credential proxy — keeps the real Claude API key off the guest (RFC-0002 milestone 0). New and opt-in: off by default, enabled per run with
credential_proxy: true(YAMLllm.credential_proxy, or the builder). Existing behavior is unchanged — without the flag, provider credentials are staged into the guest exactly as before. When enabled (Claude provider, Linux/KVM only; fails closed on macOS/VZ where the listener cannot yet be bound guest-only),ANTHROPIC_API_KEYis withheld from the guest and injected host-side at egress by a per-run, TLS-terminating proxy (src/proxy/): the guest holds only a non-secret placeholder, a per-sandbox name-constrained CA (installed viaNODE_EXTRA_CA_CERTS), and a per-sandbox proxy token; the proxy checks the token, rewrites the credential header with the host-held key, and re-originates to the real upstream over fresh TLS. An automated check asserts no real credential reaches the staged guest env or files, gating the feature. Injection is a replace, not an add — the injector substitutes an existing credential header (the placeholder) and never introduces the secret into a request that carried none, so the key is never attached to an endpoint that did not present a credential. Upstream connections are SSRF-pinned (resolve once, reject internal ranges; a hostHTTPS_PROXYcannot route around it). As milestone 0 it carries documented reduced-posture deviations (in-process TLS/HTTP parser; the per-sandbox token is the sole cross-sandbox control on KVM until the egress network rule lands; Claude Code's untokened control-plane traffic toapi.anthropic.comis not yet captured — tracked in #124). The Anthropic-compatible Custom provider and codex follow in M1; OAuth in M1a. - aarch64/KVM guest support (RFC-0003, #114). VoidBox guests now boot on arm64 Linux/KVM hosts, at parity with x86_64/KVM and macOS/VZ — the conformance, oci_integration, e2e_mount, e2e_telemetry, and e2e_skill_pipeline suites all pass there, and the smoke spec runs real Claude end to end. The loader inflates gzip-compressed arm64 Images (distro
/boot/vmlinuzhas no self-decompressor; bounded at guest-RAM size) and places kernel/initramfs from the Image header'stext_offset/image_sizewith checked arithmetic; the generated DTB describes the full platform (GICv3 with a GICv2 variant chosen by aKVM_CREATE_DEVICE_TESTprobe, PSCI 0.2 vCPUs with powered-off secondaries, anns16550aUART at0x0900_0000soconsole=ttyS0works unchanged, per-device virtio-mmio nodes); IRQ injection uses the arm64KVM_IRQ_LINEpacking; guest shutdown (VcpuExit::SystemEvent) stops the VM. Device MMIO windows and interrupt numbers derive from a shared per-arch slot table; x86_64 values and the x86_64 kernel cmdline are byte-identical, pinned by unit tests. Previously, arm64/KVM boots stalled before the control channel came up (#114). VOID_BOX_CONNECT_DEADLINE_SECS— opt-in override that extends (never shortens) the control channel's 30 s connect/handshake deadline, for validation environments where guest boot legitimately exceeds it (e.g. production-size initramfs under nested virtualization). Unset means exactly the previous behavior.- aarch64/KVM device discovery convention (RFC-0003, ADR-0008): devices are declared as virtio-mmio DTB nodes (no
virtio_mmio.device=cmdline args, which cannot express GIC SPIs), andvoidbox.network=1— previously VZ-only — is the platform-neutral guest-agent network marker. - aarch64 vCPU ceilings derived from the guest memory map (ADR-0007): at most 123 vCPUs (GICv3 redistributor region must stay below the UART window) and 8 on the GICv2 fallback, both rejected with clear pre-flight errors.
docs/war-histories.md— retrospective log of expensive debugging sessions. First entry: the eprintln!/tty-backpressure stall that manifested as an iter-20 handshake hang on the userspace vsock backend under multiplex load. Entries follow a fixed shape: problem, wrong turns, decisive evidence, fix, lessons.docs/superpowers/plans/2026-04-23-guest-observability-alternatives.md— approved design for a guest-side pprof CPU + heap profiler exposed via a newProfileRequest/ProfileResponsepair on the persistent multiplex channel, invoked from the host byvoidbox profile --run-id <id> --kind cpu|heap(implementation pending). Spec also bundles the learned debugging-tools runbook (KVM_GET_REGS via gdb,perf kvm stat live --event=ioport,/dev/portPOST-debug bytes,outbmilestone markers, log-count-vs-iter scaling heuristic).rust-styleskill rule — neverprintln!/eprintln!in library or service code; usetracing::{trace,debug,info,warn,error}!. Acceptable only in tests and top-level CLImain.rsuser-facing output.- Slim microVM kernel build (
scripts/build_slim_kernel.sh) — upstream Linux v6.12.30 LTS + Firecrackermicrovm-kernel-ci-{x86_64,aarch64}-6.1.configbase + void-box additions (9p, virtiofs, overlayfs,VIRTIO_MMIO_CMDLINE_DEVICES). Ships uncompressedvmlinux(~30 MB) unifying artifact shape with macOS/VZ. DisablesCONFIG_MODULE_SIG*so builds work on OpenSSL 3 hosts (Fedora 40+).KERNEL_VER/FC_COMMIT/FC_CONFIG_MAJMINenv overrides for reproducible builds. - macOS slim-kernel cross-build —
scripts/build_slim_kernel.shnow dispatches into anubuntu:24.04container on Darwin hosts (--platformpinned from host arch, host-side cache check skips Docker on re-runs).UBUNTU_IMAGEenv var lets callers pin a digest for reproducibility. EnablesCONFIG_PCI{,_HOST_GENERIC,_HOST_COMMON}+CONFIG_VIRTIO_PCI{,_LEGACY}so the slim kernel boots on Apple Virtualization.framework (which uses virtio-PCI on arm64, unlike Firecracker's virtio-MMIO). voidbox --log-dirandVOIDBOX_LOG_DIRoverrides for file-based runtime logs- Codex CLI as first-class agent peer —
llm.provider: codexin YAML specs exec's the bundled OpenAI Codex CLI inside the guest VM with full structured observability scripts/build_codex_rootfs.sh— production Codex-capable initramfs with auto-download from GitHub releases (musl-static, no glibc shipping)scripts/build_agents_rootfs.sh— combined claude+codex flavor produced in a single buildscripts/lib/agent_rootfs_common.sh— shared rootfs helpers (sandbox user, CA certs, finalize, claude/codex binary resolution + auto-download) extracted from the Claude flavor for reuse across agent flavorssrc/observe/codex.rs— structured stream parser for Codex'sexec --jsonJSONL events (tool calls, token counts, error handling)ObserverKindenum onLlmProvider— typed dispatch replacing the binary-name string comparison for stream observer selectionLlmProvider::Codexvariant withbinary_name(),supports_claude_settings(),build_exec_args(),observer_kind(),image_flavor()methods- Auth via host
~/.codex/auth.jsonmount (ChatGPT OAuth) alongsideOPENAI_API_KEYenv var fallback - Codex MCP discovery — writes
~/.codex/config.tomlwith[mcp_servers]streamable-HTTP entries pointing at the existing void-mcp server - Per-agent docs at
docs/agents/claude.mdanddocs/agents/codex.mdwith@discovery imports in AGENTS.md examples/specs/codex_smoke.yaml(kind: agent) andexamples/specs/codex_workflow_smoke.yaml(kind: workflow)- VZ native snapshot/restore using Apple's
saveMachineStateToURL:/restoreMachineStateFromURL:APIs (macOS 14+) with a JSON sidecar (VzSnapshotMeta) carryingsession_secret,memory_mb,vcpus,network,boot_clock_secs,config_hash, andVZGenericMachineIdentifier.dataRepresentation - VZ restore device-set drift reconciliation: when caller-supplied
memory_mb/vcpus/networkdrift from the sidecar's saved values, the saved values are used so Apple's strict configuration-match check does not fail the restore SandboxBuilder::enable_snapshots(…)/SandboxConfig/BackendConfigopt-in plumbing that gates VZ'svalidateSaveRestoreSupportWithErrorcheck (cold boots that do not opt in skip the check — some device sets make Apple reject snapshot-capability validation even when the VM itself is healthy)snapshot_store::resolve_snapshot_argumentreturning aSnapshotResolutionenum (Hash/Literal/NotFound), unifying three duplicate hash-vs-literal resolution paths- Hash-pinned vendored agent binaries (R-B5c.1) —
scripts/agents/manifest.tomlpins each (agent, platform, arch) tuple to a specificversion,url, andsha256. The build scripts (build_claude_rootfs.sh,build_codex_rootfs.sh) consult the manifest as the default source of truth and fail loudly on SHA-256 mismatch, missing manifest, or missing tuple. Override env vars (CLAUDE_CODE_VERSION/CODEX_VERSION) now require a matching*_SHA256only when they differ from the manifest pin; setting them to the manifest pin is a no-op that uses the pinned SHA.CLAUDE_BIN/CODEX_BIN/ local-PATH discovery still works for local dev but emits aWARNand is documented as non-production. Manifest reader is shell + awk (scripts/lib/agent_manifest.sh) — no extra runtime deps. Weekly.github/workflows/bump-agents.ymljob (Mondays 09:00 UTC) discovers new upstream versions, computes SHA-256 in CI, and opens one PR per agent — per-arch independent (one lagging arch doesn't wedge the job).RELEASE_DIGESTS.json(schema documented indocs/release-digests.md) is published alongside each release. Maps to threat T-B5c.1.
vmm::arch::Arch::load_kernelnow takes aBootPlatform(vCPU count + populated virtio slots) — the aarch64 DTB needs both at load time; new public typesvmm::arch::VirtioSlotandvmm::arch::BootPlatformare the single source for per-arch device MMIO bases and interrupt numbers.- aarch64 GIC version selection is probe-then-create (
KVM_CREATE_DEVICE_TEST) instead of create-then-fallback: KVM allows one vGIC per VM, so a GICv2 fallback after a partially-created GICv3 could never succeed — a creation failure is now a clear hard error, and the DTB always names the version the VMM attempts. - Startup latency — cold-boot p50 cut from ~4.9 s to 252 ms (−95%) and warm-restore p50 from ~607 ms to 138 ms (−77%) on KVM. Delivered in three steps: (1) remove three hardcoded blind waits (cold 4.9 s → 3.5 s, warm 607 ms → 433 ms); (2) add
initcall_blacklist=cmos_init,i8042_initto the default kernel cmdline, skipping host-distro RTC/i8042 probe timeouts (cold 3.5 s → 1.7 s); (3) ship the slim kernel (cold 1.7 s → 252 ms). Backed byvoidbox-startup-bench --iters 20 --breakdownon Fedora 43 host. vmm::vsock_irq_threadepoll timeout tightened 200 ms → 20 ms sostop()reclaims the thread within one poll window instead of up to a full interval — dropsstop()phase from ~230 ms to ~50 ms on both cold and warm paths- Rust MSRV bumped to 1.88
- Interactive
voidbox shellsessions now route runtime logs to the daily log file and route guest console output away from the active terminal to avoid TUI corruption voidbox shellnow prefers Claude Personal when personal OAuth credentials are available via the host's cross-platform credential discovery path- Renamed
ClaudeExecOpts/ClaudeExecResult/ClaudeStreamEvent→AgentExecOpts/AgentExecResult/AgentStreamEvent(flat rename, no wrapper enum — both providers populate the same struct) - Renamed
Sandbox::exec_claude()/exec_claude_streaming()→exec_agent()/exec_agent_streaming()with&LlmProviderparameter threading - Renamed
StageResult.claude_result→agent_result - Renamed
e2e_claude_mcptest →e2e_agent_mcp(MCP infrastructure is agent-agnostic) build_claude_rootfs.shrefactored to source sharedagent_rootfs_common.shhelpersbuild_claude_rootfs.shandbuild_codex_rootfs.shauto-detect or download the respective Linux binaries when invoked from an Apple Silicon host- Claude-specific
--settingsand--mcp-configflags gated behindprovider.supports_claude_settings() - Guest network deny list is now applied once at guest init (right after
setup_network()) instead of lazily on the firstexec— closes the race window between network bring-up and first exec and makes the deny list visible on the serial console at boot - VZ auto-snapshot uses
save_state_pausedfollowed by a directstop()from the paused state, avoiding an unnecessary resume/pause round-trip host_metrics.rson macOS now uses themach2crate instead of hand-rolled Mach FFI (IntegerT,TaskFlavorT,extern "C"block)- Service-agent and output-monitor progress messages in
agent_box.rsrouted throughtracing(info!/warn!/error!/debug!) instead ofeprintln! - Snapshot format v3 → v4, bincode → postcard (
src/vmm/snapshot.rs).bincode1.x and 2.x are both flagged unmaintained (RUSTSEC-2025-0141);bincode3.0 ships as acompile_error!protest release. Swapped topostcard(actively maintained, serde-based, same scope, simpler API). The new wire format is not compatible with pre-v4 snapshots — delete~/.void-box/snapshots/to recover after upgrade.SNAPSHOT_VERSIONbumped so the version gate surfaces a clear error on stale state files. indicatif0.17 → 0.18, which drops the unmaintainednumber_prefixtransitive (RUSTSEC-2025-0119) in favor ofunit-prefix.release.yml,release-images.yml, andguest-image.ymlno longer exportCLAUDE_CODE_VERSION/CODEX_VERSION— these were the only way to drivebuild_claude_rootfs.sh/build_codex_rootfs.shbefore R-B5c.1, andscripts/agents/manifest.tomlnow owns that responsibility. Removing the env vars eliminates the drift surface where the workflow's hardcoded fallback could disagree with the manifest after a bump.AGENTS.md#Control channel I/O modelrewritten to describe the persistent multiplex design (one long-lived connection per sandbox,request_id-keyed demux, sharedFrameSenderunder aMutex, dedicated reader thread) alongside the existing "why not fully async" rationale. Points atsrc/backend/multiplex.rsas the central moduleVm::with_vcpu_countremoved (folded intoVm::new) andcpu::create_vcpu/cpu::create_vcpu_restoredreplaced bycpu::prepare_vcpu/cpu::prepare_vcpu_restored+cpu::start_vcpu— part of the aarch64 vGIC ordering fix; no pre-vCPU setup consumes a vCPU count anymore, and the post-vCPU hook receives the real count..github/workflows/e2e.yml—persistent_channel,pty_nonzero_exit_code, and the entiresnapshot_integrationstep are disabled on the Azureubuntu-latestE2E lane. All three pass locally on every host we've tried; all three fail only on the Azure nested-virt runner (distinct failure modes: handshake deadline, exit-127 sentinel, and fast-failing CLI tests). Tracked as a single follow-up for a dedicated diagnostic pass on the Azure runner; the suites remain fully enforced locally and in the validation contract
- VM teardown:
stop()andsnapshot()can no longer hang unboundedly on a wedged thread (#128 item 3). Teardown enqueuedVmCommand::Stopwith a blocking send before clearingrunningand kicking the vCPUs, so an event loop wedged inside a dispatched command's await — e.g. a service-mode exec, which carries no timeout — stopped draining the command channel and parked teardown at the send. This is the exact state captured live during #127 validation, wheresnapshot_multi_vcpuhung roughly 1 run in 3 under nested KVM. Teardown now signals first and never waits on another thread consuming a message:runningis cleared and the vCPUs kicked before anything else; the control channel is shut down via the newControlChannel::shutdown()(the first non-test caller ofMultiplexChannel::shutdown()), which fails every pending RPC dispatch slot and thereby wakes a wedged event loop so it can observe the cleared flag; theStopcommand is reduced to a non-blockingtry_sendwake. Every thread join — vCPU, event-loop, vsock-irq, net-poll — is bounded by a 10 s deadline that fails with an error naming the stuck thread (the thread is detached) instead of hanging forever.ControlChannel::shutdown()also aborts an in-flight connect/handshake retry loop within one backoff step, so an event loop establishing against a dead guest cannot outlive the join deadline, andKvmBackend::stop()/VzBackend::stop()apply the same shutdown to the backend-level channel that carries every run's RPCs (exec, telemetry, write_file). The userspace vsock worker, whose own lifecycle flag was never reached by clearingrunning, is now stopped explicitly during teardown — before device-state capture on the snapshot path, and even when a join deadline fired (non-blocking then, since the wedged thread may hold the device mutex). A worker that had to be detached leaks its eventfds instead of closing them under a still-running thread, which could otherwise write into an unrelated, reused descriptor. - Userspace vsock backend: worker death or never-start no longer leaves the listener permanently unserviced (#128 item 4). Four paths killed the worker — or never started it — while host connects kept queueing into the accept backlog until run end: a guest
STATUS=0reset (reboot or panic) stopped the worker until a DRIVER_OK that might never come; a non-EINTRepoll_waiterror or anepoll_create1failure was warn-and-die; a snapshot taken before the guest driver reached DRIVER_OK restored a device whose worker never started; and a poisoned connection-map mutex silently skipped listener registration and every subsequent sweep. The worker's lifetime is now the device's lifetime: it starts at device construction (cold boot and restore alike) and survives driver resets, so connects made while the driver is down queue in the connection map — the same buffering the backend already provides during cold boot — bounded by the host's connect deadline. Every sweep now unconditionally accepts pending connections and drains host streams, making epoll a wakeup accelerator rather than a liveness dependency: on any epoll failure the worker degrades to a 50 ms periodic sweep instead of dying. A poisoned connection-map mutex is recovered with a one-time warning instead of silently abandoning service. Because the listener is now serviced regardless of driver state, acceptance is also bounded: concurrent host connections are capped at 128 (accepts beyond the cap are dropped, closing the stream), and the 4-byte port prefix a client sends after connecting is read non-blockingly with a 2 s deadline — previously the worker blocked up to 2 s holding the connection-map lock, which the vCPU MMIO paths also need, on a client that stalled mid-prefix. Data is bounded the same way: a connection whose guest-bound buffer reaches 256 KiB, or a guest-bound packet backlog past 4 MiB, pauses reads from the host stream so backpressure lands in the socket buffer (the client's writes block or hit their send timeout) instead of host memory absorbing unbounded data while the driver is down; the worker's epoll registrations are edge-triggered so a deliberately unread stream cannot spin the sweep. - Control channel: host RPCs can no longer hang unboundedly on a stalled guest or device (#128 items 1, 2, 5). Three gaps let a guest or device stall convert into a silent host-side hang instead of an error. (1) Multiplex frame writes were blocking
write_allcalls issued directly on the caller's async task with no send timeout —tokio::time::timeoutcannot preempt a blocking syscall, the write held the sharedFrameSendermutex (stalling every other RPC's send), and for exec the write ran before any timeout wrapper was armed. Writes now run inspawn_blockingwith a 10 s per-waitSO_SNDTIMEOon the stream plus a hard 60 s per-frame deadline that also covers mutex queueing and trickle-fed writes. A send that fails before any byte reaches the stream fails only its own RPC; a send that truncates a frame mid-write (or exceeds the deadline) marks the channel dead — the wire can no longer be trusted for framing — so the next RPC reconnects, and senders already queued on the writer mutex refuse to write after the truncation. (2) The post-handshake reader detected channel death only via EOF, so a peer that wedged without closing its fd left a zombie channel thatget_or_establish_channelkept handing out; the reader now runs on a bounded 1 sSO_RCVTIMEOand checks a shutdown flag between attempts (set by explicit shutdown, by a truncating or deadline-exceeding send, or by dropping the last channel handle — which also fixes the reader thread previously leaking for the VM's lifetime when a channel was discarded) — the timeout is swallowed inside the stream adapter so mid-frame idleness never breaks framing. Channel establishment itself, which runs before every per-RPC timeout, is now bounded by the connect deadline plus a fixed margin. (3) The userspace vsock backend'swrite_to_hostissued a singlewriteon the non-blocking host stream and treated anyOk(n)as full delivery, silently dropping the tail of a guest→host frame on a short write and stalling the host's multiplex reader mid-frame. Short-write remainders are now buffered per connection (cursor-based, so flushing never memmoves the backlog under the connection-map lock) and flushed on the worker's sweep;fwd_cntadvances only for bytes actually delivered, and a deferred flush queues an unsolicitedOP_CREDIT_UPDATEso a guest parked on exhausted credit wakes when the host drains. The buffer is capped at the advertisedbuf_alloc, bounding host memory against a guest that ignores credit. None of these reproduced in normal runs after the #127 fixes, but each matched the fingerprint of thesnapshot_multi_vcpuhang captured live during #127 validation (event loop parked on an await with no timer, no I/O, and no completer). - Userspace vsock backend: connections closed by the host no longer leak and busy-spin the worker. The device never detected a host-side close:
read_from_hostfolded EOF into "no data", connections were removed only on guest-initiatedShutdown/Rst, and a write failure to a gone host application queued an RST but left the entry in the map. Each such connection kept its fd registered in the worker's level-triggered epoll set, where EOF reads as perpetually ready — so the worker's 50 ms poll loop degenerated into a busy spin holding the connection-map lock, starving the vCPU threads. Abandoned control-channel handshake retries create exactly these connections on every boot, so every VM carried a growing set of spinning fds; on 2-core CI runners the resulting starvation is what pushed later tests past the connect deadline in the first place. The worker's stream sweep (drain_host_streams) now distinguishes EOF (HostReadOutcome), queues an RST so the guest releases its side, and removes the connection — dropping the stream closes the fd, which also deregisters it from epoll. The sweep covers every connection state: an abandoned attempt whose OP_RESPONSE never completed parks inConnecting, where a sweep restricted to established connections could never reap it (validated with a load probe on Linux/KVM — 25 abandoned connections previously pinned the worker at 100% of a core indefinitely; it now settles to 0% within a second). - Userspace vsock backend: host connect no longer hangs forever when the guest is slow to boot. The control channel's
connect(2)to the backend's Unix socket was unbounded. The backend's accept loop starts only once the guest driver reaches DRIVER_OK, and the listener's accept backlog is drained only byaccept(2)— an attempt the client has already closed still occupies its slot. When guest boot exceeded the 30 s connect deadline (CPU-starved CI runners), the handshake retries of the warm-up and first-RPC establish attempts filled the 128-entry backlog, and the nextconnect(2)blocked indefinitely — inside the connector, where the retry loop's deadline (checked between attempts) could never fire. This surfaced assnapshot_integrationCI runs hanging with no output until the 60-minute job timeout.connect_unixnow appliesSO_SNDTIMEOfor the duration of the connect (cleared once connected), so a full backlog fails the attempt and the 30 s deadline produces its normaldeadline reached (connect or handshake)error. - One-shot agent runs no longer log
ERROR void_box::vmm: MicroVm dropped while still runningon success.VoidBox::runconsumes the box, so the VM could never outlive the call — but teardown fell toMicroVm'sDropsafety net, which logs an error.runnow stops the sandbox gracefully before returning, on success and on error; theDrophandler remains as a genuine safety net for abnormal paths.MicroVm::stopitself now works on current-thread tokio runtimes (e.g.#[tokio::test]), joining VM threads inline whereblock_in_placewould panic. - aarch64/KVM boot: vGIC initialized after vCPU creation (
src/vmm/). Every arm64 Linux/KVM boot failed withKVM error: Device or resource busy (os error 16):Arch::setup_vmran all GIC work — device creation, address attributes,KVM_DEV_ARM_VGIC_CTRL_INIT— before anyKVM_CREATE_VCPU, and the arm64 KVM ABI requires the opposite order (vGIC init freezes per-vCPU redistributor state; the kernel then rejects later vCPU creation with-EBUSY). Since x86_64 has the inverse constraint (irqchip before vCPUs), the arch hook is now split:setup_vm(pre-vCPU; x86 irqchip + PIT) andsetup_vm_post_vcpus(post-create, pre-run; all aarch64 GIC work). vCPU creation is split from run-thread start (prepare_vcpu*/start_vcpu) so the hook runs while every vCPU fd exists and none is running, on both cold-boot and snapshot-restore paths. Fixes #112. The remaining aarch64 platform-description gap it exposed (#114) is completed by the aarch64/KVM guest-support entry under Added. - aarch64/KVM: register IDs derived from the ABI layout (
src/vmm/arch/aarch64/cpu.rs). The hand-rolledKVM_SET_ONE_REG/KVM_GET_ONE_REGIDs placed the coproc class at bits 48+ instead of the ABI's bits 16–27 (every access landed in the kernel's sys-reg table →-ENOENT, killing cold boot at the PC write), addressed core registers in u64 words instead of 32-bit words, ignored thesp_el1/elr_el1/spsr[5]fields precedingfp_regs, and sized FPSR/FPCR as U64 instead of U32. IDs are now built fromkvm_bindingsconstants andoffset_of!on the real struct layout, with unit tests pinning them to the documented ABI values. Latent since the aarch64 port; unreachable before the #112 fix. Fixes #113. - Security:
quinn-proto0.11.14 → 0.11.15 (RUSTSEC-2026-0185 — remote memory-exhaustion DoS via unbounded out-of-order QUIC stream reassembly). Transitive bump viacargo update, reachable only throughreqwest'shttp3feature which this workspace does not enable, so the affected code is not compiled in; lockfile-only, no API impact. - Security:
rustls-webpki0.103.12 → 0.103.13 (RUSTSEC-2026-0104 — reachable panic in certificate revocation list parsing). Transitive bump viacargo update; no direct API impact. - Guest-agent
kmsg()tty-backpressure deadlock — dropped theeprintln!(msg)line fromguest-agent/src/main.rs::kmsg(), keeping only the/dev/kmsgwrite. Under the persistent multiplex control channel, the dual-write accumulated state in the guest kernel's n_tty line discipline and blockedwrite(2)onwait_event_interruptible(write_room > 0)after ~20 tight-loop iterations ofpersistent_channel_serial_exec_many(guest idle atpv_native_safe_halt, host's multiplex-reader blocked inlibc::read). Validation: 100 serial execs pass in 7.86 s on the userspace vsock backend. Full retrospective indocs/war-histories.md. - Userspace vsock: host→guest writes larger than 4 KiB (
VsockConnectionMap::queue_host_data) — the function silently truncated any buffer longer than 4 KiB to a single 4 KiB packet and dropped the rest, so any Message whose wire size exceeded the per-packet cap hung the guest-agent'sread_exact(payload)indefinitely and the host timed out after 30 s withProtocol error: IO error: Resource temporarily unavailable. Loop now produces one Rw packet per 4 KiB chunk, respects peer credit, and buffers the remainder intoconn.tx_buf+ flushes onpeer_fwd_cntadvance. Small RPCs unchanged. Latent since the userspace backend landed (f4f14ab); first user-visible trigger was the 6 KiBhackernews-api.mdskill file (~21 KiB on the wire afterWriteFileJSON framing). - x86_64 boot loader: raw
vmlinuxELF support (src/vmm/arch/x86_64/boot.rs) —load_elf_kernelnow readse_entryfrom ELF header offset0x18instead of returningkernel_end(which was the end of loaded memory, not the entry point), andread_bzimage_init_size/read_bzimage_initrd_addr_maxare gated onis_bzimage()so raw ELFs don't read garbage from offsets0x260/0x22c(which collapsed the initramfs placement window to0x0and surfaced asinitramfs too large for placement window end=0x0) - OCI rootfs mount on macOS/VZ:
setup_oci_rootfs()on the non-block-device path now mounts the virtiofs share (parsed from/proc/cmdlinevoidbox.mount*entries) before theis_dircheck, and the in-overlay shared-dir loop skips the same entry to avoid a double mount. Unblocks everysandbox.image-based spec on Apple Silicon; previously failed withOCI_FAIL_ROOTFS_MISSING. NewOCI_FAIL_LOWER_MOUNTstatus code for diagnostic granularity. - Orphan-run reconciliation on daemon restart:
reconcile_orphan_runs_on_startup()flips persisted non-terminal runs (Pending/Starting/Running) toFailedwithterminal_reason = "daemon_restarted"and emits arun.failedevent. Previously these runs were returned fromGET /v1/runsforever after a kill/restart. ReusesRunStatus::Failedto keep the serde wire format stable. - Interactive PTY shell handling on macOS/VZ: poll-based host relay, resize forwarding, and cleaner terminal lifecycle for Claude and other TUI-style programs
- Guest console routing semantics are now consistent across macOS/VZ and Linux/KVM
- Snapshot restore: capture/restore
IA32_XSSMSR to prevent XRSTORS #GP on CET-enabled kernels (6.x+) - Silent
chownfailure inprovision_claude_bootstrapnow emits awarn!with actionable remediation - Codex downloader's
EXITtrap is scoped to a subshell so it cannot clobber the caller's cleanup trap - Deterministic MAC-address bit manipulation in
deterministic_mac_address((x | 0x02) & 0xfe) now carries a bit-level comment explaining the IEEE 802 "locally administered, unicast" transform
0.1.2 - 2026-03-16
- Snapshot/Restore for KVM: base and diff snapshots with multi-vCPU support, userspace virtio-vsock backend
- Snapshot/Restore for Apple Virtualization.framework (macOS VZ)
- aarch64 architecture support for snapshots via
Archtrait refactor - Guest telemetry buffering and host metrics collection
- Daemon lifecycle events:
StageQueued,StageStarted,StageSucceeded,StageFailed,StageSkipped - Persist stage
file_outputartifacts to disk after completion GET /v1/runs/{run_id}/stages/{stage_name}/output-filedaemon endpoint for retrieving stage output files- Pipeline I/O wiring with mount-based inputs/outputs
- Host directory mounts via 9p (Linux) and virtiofs (macOS) with RW/RO support
- Shell installer (
scripts/install.sh) - DEB and RPM packaging via nfpm
- Homebrew tap distribution (macOS)
- Structured logging via
tracingwithStructuredLogger - Startup banner
snapshot_storemodule centralizing snapshot utilities- Snapshot CLI:
create,list,delete, diff snapshots - Virtio-net snapshot and restore
- OCI guest image distribution via GHCR (multi-arch: amd64 + arm64)
- macOS native support via Virtualization.framework
- LM Studio provider support and OpenClaw Telegram example
- Unified pipeline execution loop (
run_pipeline_core) - Daemon
route_requestreturns(status, content_type, body)for binary responses - Rust MSRV bumped to 1.85
- Quinn-proto updated to v0.11.14
- Renamed
e2e_mount_9ptoe2e_mountwith expanded virtiofs support - Refactored module loading logic with optional 9p kernel modules
- Replaced
info!logging withdebug!for reduced noise
- Snapshot restore: XCR0 / LAPIC timer / CID mismatch issues
- EPERM-resilient OCI layer unpack
- macOS VZ examples and Apple Silicon support
- Duplicate directory creation in artifact management
- aarch64 musl cross-linker path in guest-image workflow
- Diamond dependency conflict with virtio and vm-memory crates
- BusyBox inclusion in CI guest image
0.1.0 - 2026-02-19
- Initial release of void-box
- KVM-based micro-VM sandbox implementation
- Mock sandbox for testing and development
- Workflow composition engine with DAG support
- Native observability layer (traces, metrics, logs)
- SLIRP user-mode networking (no root required)
- Guest-agent for VM communication
- CLI tool (
voidbox) for command-line usage - Pre-built artifact distribution via GitHub releases
- Streaming tool events — real-time
[vm:NAME] tool: Bash <cmd>output during execution - Descriptive tool logging with
tool_summary()(shows command/file_path/pattern instead of tool ID) - Incremental JSONL parser (
parse_jsonl_line) for stream processing exec_agent_streaming()sandbox API (renamed fromexec_claude_streaming()in the Codex flavor effort)- HackerNews agent example (
examples/hackernews/) - Code review agent example with two-stage pipeline and remote skills
- Comprehensive CI/CD pipeline with GitHub Actions
- Comprehensive documentation:
- README with quick start guide
- Getting Started guide
- Architecture documentation
- API documentation
- Build scripts for release artifacts
- GitHub Actions workflow for automated releases
-
Sandbox Execution:
- Local KVM-based sandboxes
- Mock sandboxes for testing
- Command execution with stdin/stdout/stderr
- File operations (read/write)
- Environment variable support
-
Workflow Engine:
- Step definition and composition
- Pipeline support (pipe steps together)
- Parallel execution
- Retry logic with configurable backoff
- Context isolation between steps
-
Observability:
- OpenTelemetry-compatible tracing
- Metrics collection (counters, gauges)
- Structured logging
- Span inspection and analysis
-
CLI Tool:
voidbox exec- Execute commandsvoidbox workflow- Run workflows- Auto-detection of KVM availability
- Fallback to mock sandbox
-
Artifact Management:
- Download pre-built artifacts from releases
- Environment variable configuration
- Auto-detection of host kernel
- Artifact caching
- Switch from Node.js + npm claude-code to native claude-code binary (Bun SEA)
- SLIRP networking: DNS caching, host resolv.conf forwarding, reduced timeouts
- Guest clock sync via kernel cmdline (
voidbox.clock=<epoch_secs>) - Net-poll background thread for improved network throughput
- HLT sleep reduced from 10ms to 1ms for lower latency
- NPROC limit raised to 512 (from 256) for Bun worker threads
- Memory bump to 2048MB in HackerNews example (OOM fix)
- RLIMIT_AS re-enabled at 1GB — Bun/JSC needs only ~640MB virtual (vs V8's 10GB+)
file_outputfallback when claude-code output file is missingskipWebFetchPreflightadded to agent config defaults
- Rust workspace with library and guest-agent
- Multi-architecture support (x86_64, aarch64 ready)
- Static linking for guest-agent (musl)
- Automated release builds
- Documentation generation