Architecture
Overview
Void-Box is a composable agent runtime where each agent runs in a hardware-isolated micro-VM. On Linux this uses KVM; on macOS (Apple Silicon) it uses Virtualization.framework (VZ). The core equation is:
VoidBox = Agent(Skills) + Isolation
A VoidBox binds declared skills (MCP servers, CLI tools, procedural knowledge files, OCI images, reasoning engines) to an isolated execution environment. Boxes compose into pipelines where output flows between stages, each in a fresh VM.
Component Diagram
┌──────────────────────────────────────────────────────────────────┐
│ User / Daemon / CLI │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ VoidBox (agent_box.rs) │ │
│ │ name: "analyst" │ │
│ │ prompt: "Analyze AAPL..." │ │
│ │ skills: [claude-code, financial-data.md, market-mcp] │ │
│ │ config: memory=1024MB, vcpus=1, network=true │ │
│ └─────────────────────┬────────────────────────────────────┘ │
│ │ resolve_guest_image() → .build() → .run()
│ ┌─────────────────────▼───────────────────────────────────┐ │
│ │ OCI Client (voidbox-oci/) │ │
│ │ guest image → kernel + initramfs (auto-pull, cached) │ │
│ │ base image → rootfs (pivot_root) │ │
│ │ OCI skills → read-only mounts (/skills/...) │ │
│ │ cache: ~/.voidbox/oci/{blobs,rootfs,guest}/ │ │
│ └─────────────────────┬───────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────▼───────────────────────────────────┐ │
│ │ Sandbox (sandbox/) │ │
│ │ ┌─────────────┐ ┌──────────────┐ │ │
│ │ │ MockSandbox │ │ LocalSandbox │ │ │
│ │ │ (testing) │ │ (KVM / VZ) │ │ │
│ │ └─────────────┘ └──────┬───────┘ │ │
│ └──────────────────────────┼──────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────▼──────────────────────────────┐ │
│ │ MicroVm (vmm/) │ │
│ │ ┌────────┐ ┌────────┐ ┌─────────────┐ ┌──────────────┐ │ │
│ │ │ KVM VM │ │ vCPU │ │ VsockDevice │ │ Guest Net │ │ │
│ │ │ │ │ thread │ │ (AF_VSOCK) │ │ (SLIRP / VZ) │ │ │
│ │ └────────┘ └────────┘ └───────┬─────┘ └───────┬──────┘ │ │
│ │ Linux/KVM: virtio-blk (OCI rootfs) │ │ │
│ │ Host mounts: 9p on KVM, virtiofs on VZ │ │ │
│ │ Linux/KVM only: Seccomp-BPF on VMM thread │ │ │
│ └────────────────────────────────┼───────────────┼────────┘ │
│ │ │ │
└═══════════════════════════════════╪═══════════════╪══════════════┘
Hardware Isolation │ │
│ vsock:1234 │ guest networking
┌───────────────────────────────────▼───────────────▼───────────────┐
│ Guest VM (Linux kernel) │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ guest-agent (PID 1) │ │
│ │ - Authenticates via session secret (kernel cmdline) │ │
│ │ - Reads /etc/voidbox/allowed_commands.json │ │
│ │ - Reads /etc/voidbox/resource_limits.json │ │
│ │ - Applies setrlimit + command allowlist │ │
│ │ - Drops privileges to uid:1000 │ │
│ │ - Listens on vsock port 1234 │ │
│ │ - pivot_root to OCI rootfs (if sandbox.image set) │ │
│ │ - PTY handler: forkpty, up to 4 concurrent sessions │ │
│ └────────────────────────┬─────────────────────────────────────┘ │
│ │ fork+exec (headless) or forkpty (PTY) │
│ ┌────────────────────────▼─────────────────────────────────────┐ │
│ │ runtime CLI (claude-code, codex, or claudio mock) │ │
│ │ Headless: Claude stream-json or Codex exec --json │ │
│ │ Interactive PTY: raw terminal I/O over vsock │ │
│ │ Skills: /workspace/.claude/skills/*.md │ │
│ │ MCP: /workspace/.mcp.json or ~/.codex/config.toml │ │
│ │ OCI skills: /skills/{python,go,...} (read-only mounts) │ │
│ │ LLM: Claude API / local proxies / Codex API │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ Linux/KVM: 10.0.2.15/24 gw 10.0.2.2 dns 10.0.2.3 │
│ macOS/VZ: DHCP-assigned NAT network │
└───────────────────────────────────────────────────────────────────┘
Data Flow
Single VoidBox execution
1. VoidBox::new("name") User declares skills, prompt, config
│
2. resolve_guest_image() Resolve kernel + initramfs (resolution chain)
│ Pulls from GHCR if no local paths found
│
3. .build() Creates Sandbox (mock or local VM backend: KVM/VZ)
│ Mounts OCI rootfs + skill images if configured
│
4. .run(input, telemetry_buffer) Execution begins
│
├─ provision_security() Write resource limits + allowlist to /etc/voidbox/
├─ provision_skills() Write SKILL.md files to /workspace/.claude/skills/
│ Write MCP discovery to /workspace/.mcp.json
├─ write input Write /workspace/input.json (if piped from previous stage)
│
├─ sandbox.exec_agent_streaming()
│ Send ExecRequest over vsock
│ │
│ [vsock port 1234]
│ │
│ guest-agent receives Validates session secret
│ │ Checks command allowlist
│ │ Applies resource limits (setrlimit)
│ │ Drops privileges (uid:1000)
│ │
│ fork+exec runtime CLI Runs provider-specific JSONL mode
│ │
│ runtime executes Reads skills, calls LLM, uses tools
│ │
│ ExecResponse sent stdout/stderr/exit_code over vsock
│ │
├─ parse runtime output Extract AgentExecResult (tokens, cost, tools)
├─ read output file /workspace/output.json
│
5. StageResult box_name, agent_result, file_output
Pipeline execution
Pipeline::named("analysis", box1)
.pipe(box2) Sequential: box1.output → box2.input
.fan_out(vec![box3, box4]) Parallel: both receive box2.output
.pipe(box5) Sequential: merged [box3, box4] → box5.input
.run()
Stage flow:
box1.run(None, telemetry) → carry_data = output bytes
box2.run(carry_data, telemetry) → carry_data = output bytes
[box3, box4].run(carry, telemetry) → carry_data = JSON array merge
box5.run(carry_data, telemetry) → PipelineResult
For parallel stages (fan_out), each box runs in a separate tokio::task::JoinSet. Their outputs are merged as a JSON array for the next stage.
Interactive shell (voidbox shell)
voidbox shell --mount /project:/workspace:rw --program claude --memory-mb 4096 --vcpus 4 --network
│
├─ Auto-detect provider claude / claude-code → claude-personal (host OAuth) or claude (API key)
│ codex → codex (~/.codex/auth.json OAuth) or OPENAI_API_KEY
├─ Build shell config kind: sandbox, synthesized from CLI flags
│ (or load a spec and use the fields the shell path consumes)
│
├─ Build Sandbox kernel, initramfs, memory, vcpus, network, mounts
│ ├─ Stage credentials Provider-specific: /home/sandbox/.claude/ or /home/sandbox/.codex/
│ ├─ Write onboarding flag Skip first-run login (e.g. /home/sandbox/.claude.json for claude)
│ └─ Restore from snapshot If --snapshot or --auto-snapshot
│
├─ attach_pty(PtyOpenRequest) Connect vsock, handshake, send PtyOpen
│ │
│ [vsock port 1234]
│ │
│ guest-agent receives Validates allowlist
│ │ Acquires session slot (max 4 concurrent)
│ │ forkpty: child drops to uid:1000
│ │ Interactive mode: no RLIMIT_FSIZE
│ │
│ PtyOpened response Success or error
│ │
├─ RawModeGuard::engage() Host terminal → raw mode
│ │
│ ┌─── I/O loop (two threads) ────────────────────────────┐
│ │ Writer: stdin → PtyData frames → vsock → guest master │
│ │ Reader: guest master → PtyData frames → vsock → stdout│
│ └───────────────────────────────────────────────────────┘
│ │
│ PtyClosed { exit_code } Guest process exited
│ │
├─ drop(RawModeGuard) Restore terminal
├─ sandbox.stop() Stop VM
│
└─ exit(exit_code) Propagate guest exit code
Spec kinds:
| Kind | Agent block | PTY | Use case |
|---|---|---|---|
agent | Required | No (headless exec) | Autonomous task execution |
sandbox | None | Via voidbox shell | Interactive development |
agent + mode: interactive | Required (empty prompt OK) | Yes | Interactive agent with prompt context |
Security guarantees (same as headless exec):
Interactive PTY sessions preserve the full defense-in-depth stack:
- Layer 1: Hardware isolation (KVM/VZ) — separate kernel and memory space
- Layer 2: Seccomp-BPF on the VMM thread for Linux/KVM
- Layer 3: Session secret authentication over vsock
- Layer 4: Command allowlist — only approved binaries can be exec’d via PTY
- Layer 4: Privilege drop to uid:1000 for the PTY child process
- Layer 4: Resource limits (RLIMIT_NOFILE, RLIMIT_NPROC) applied to PTY child
- Layer 5: CIDR deny list on both backends (enforced host-side by SLIRP on Linux/KVM; enforced guest-side via blackhole routes on macOS/VZ), plus Linux/KVM-only SLIRP rate limits and connection caps
The only difference: RLIMIT_FSIZE (max file size) is skipped for interactive
sessions (PtyOpenRequest.interactive = true). Interactive sessions need to
write files freely — agent CLIs routinely produce conversation logs that exceed
100 MB. Batch exec retains the 100 MB limit as defense-in-depth.
Wire Protocol
Host and guest communicate over AF_VSOCK (port 1234) using the void-box-protocol crate. Every frame carries a 5-byte header (4-byte little-endian length + 1-byte type discriminant) followed by a typed payload — usually JSON, with raw-byte payloads for PTY I/O and the Ping/Pong authentication handshake.
┌──────────────┬───────────┬──────────────────┐
│ length (4 B) │ type (1B) │ payload (N bytes)│
└──────────────┴───────────┴──────────────────┘
Messages cover exec (headless and streaming), file operations, PTY sessions, telemetry, snapshot readiness, and shutdown. A 64 MB MAX_MESSAGE_SIZE cap bounds allocations from untrusted length fields.
See Wire Protocol for the full message-type table and payload shapes.
Guest Networking
On Linux/KVM, Void-Box uses smoltcp-based usermode networking (SLIRP) — no root, no TAP devices, no bridge configuration. On macOS/VZ, Apple’s VZNATNetworkDeviceAttachment provides NAT.
Guest VM Host
┌─────────────────────┐ ┌──────────────────┐
│ eth0: 10.0.2.15/24 │ │ │
│ gw: 10.0.2.2 │── virtio-net ──────│ SLIRP stack │
│ dns: 10.0.2.3 │ (MMIO) │ (smoltcp) │
└─────────────────────┘ │ │
│ 10.0.2.2 → NAT │
│ → 127.0.0.1 │
└──────────────────┘
- Guest IP:
10.0.2.15/24on Linux/KVM; DHCP-assigned on macOS/VZ. - Gateway:
10.0.2.2(mapped to host127.0.0.1on Linux/KVM). - Outbound TCP/UDP is NATed through the host.
- The guest reaches host services (Ollama on
:11434) via the gateway.
Deny-list enforcement, rate limits, and concurrent-connection caps live in the security model — see Security Model.
Security Model
VoidBox stacks five isolation boundaries: hardware virtualization (KVM / VZ), seccomp-BPF on the VMM thread (Linux/KVM only), session-authenticated vsock, guest-agent hardening (command allowlist, rlimits, privilege drop, timeout watchdog), and network isolation (CIDR deny list on both platforms, plus Linux/KVM-only SLIRP rate limits and connection caps).
See Security Model for each layer’s mechanics and the session-secret handshake.
Observability
Each run emits structured events across a pipeline span → stage span tree, with tool_call events, token counts, cost, and model attributes. The guest-agent streams CPU and memory telemetry over vsock, aggregated host-side and exported as OTLP metrics.
See Events and Observability for the event schema and Observability Setup for OTLP configuration.
OCI Image Support
VoidBox uses OCI images at three levels, all cached under ~/.voidbox/oci/:
- Guest image (
sandbox.guest_image) — aFROM scratchimage carrying the kernel and initramfs; auto-pulled from GHCR when installed artifacts are not on disk. - Base image (
sandbox.image) — a full container image (e.g.python:3.12-slim) used as the guest root filesystem. Linux/KVM builds an ext4 disk artifact and attaches it as virtio-blk; macOS/VZ mounts the extracted rootfs over virtiofs. - OCI skills (
skills.*.image) — container images mounted read-only at arbitrary guest paths (e.g./skills/python), each pulled and extracted independently.
See OCI Containers for resolution order, cache layout, and client internals.
Snapshots
VoidBox supports sub-second VM restore via snapshots on both backends. Linux/KVM captures and restores state directly (base snapshot plus incremental diffs); macOS/VZ wraps Apple’s native saveMachineStateToURL: / restoreMachineStateFromURL: APIs with a JSON sidecar carrying the state VoidBox needs to reconstruct an identical configuration. Snapshot features are opt-in only — no snapshot code runs unless the user declares a snapshot path.
See Snapshots for storage layout, restore flow, vCPU register order, platform-specific mechanics, and security considerations.
Developer Notes
For contributor setup, lint/test parity commands, and script usage, see
CONTRIBUTING.md.
For runtime setup commands and end-user usage examples, see README.md.
Skill Types
| Type | Constructor | Provisioned as | Example |
|---|---|---|---|
| Agent | Skill::agent("claude-code") | Reasoning engine designation | The LLM itself |
| File | Skill::file("path/to/SKILL.md") | /workspace/.claude/skills/{name}.md | Domain methodology |
| Inline | Skill::inline("name", "# content") | /workspace/.claude/skills/{name}.md (content from memory) | Methodology generated in code |
| Remote | Skill::remote("owner/repo/skill") | Fetched from GitHub, written to skills/ | obra/superpowers/brainstorming |
| MCP | Skill::mcp("server-name") | Entry in /workspace/.mcp.json and, for Codex, ~/.codex/config.toml | Structured tool server |
| CLI | Skill::cli("jq") | Expected in guest initramfs | Binary tool |
| OCI | Skill::oci("python:3.12-slim", "/skills/py") | Image pulled and mounted read-only at the given guest path | Language toolchain on demand |