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:

KindAgent blockPTYUse case
agentRequiredNo (headless exec)Autonomous task execution
sandboxNoneVia voidbox shellInteractive development
agent + mode: interactiveRequired (empty prompt OK)YesInteractive 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/24 on Linux/KVM; DHCP-assigned on macOS/VZ.
  • Gateway: 10.0.2.2 (mapped to host 127.0.0.1 on 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) — a FROM scratch image 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

TypeConstructorProvisioned asExample
AgentSkill::agent("claude-code")Reasoning engine designationThe LLM itself
FileSkill::file("path/to/SKILL.md")/workspace/.claude/skills/{name}.mdDomain methodology
InlineSkill::inline("name", "# content")/workspace/.claude/skills/{name}.md (content from memory)Methodology generated in code
RemoteSkill::remote("owner/repo/skill")Fetched from GitHub, written to skills/obra/superpowers/brainstorming
MCPSkill::mcp("server-name")Entry in /workspace/.mcp.json and, for Codex, ~/.codex/config.tomlStructured tool server
CLISkill::cli("jq")Expected in guest initramfsBinary tool
OCISkill::oci("python:3.12-slim", "/skills/py")Image pulled and mounted read-only at the given guest pathLanguage toolchain on demand