Skip to content

Life of a Build

When you run just run, Capsem cross-compiles guest binaries, repacks the initrd, builds the host binaries, codesigns them, and boots a VM — all in ~10 seconds. This page explains what each stage produces and which tools do the work.

flowchart TD
    subgraph stage1["1. Guest binaries"]
        CARGO_CROSS["cargo build --target\naarch64-unknown-linux-musl"]
        AGENT["capsem-pty-agent"]
        NETPROXY["capsem-net-proxy"]
        MCP["capsem-mcp-server"]
        SYSUTIL["capsem-sysutil"]
        CARGO_CROSS --> AGENT & NETPROXY & MCP & SYSUTIL
    end

    subgraph stage2["2. Initrd repack"]
        INITRD_IN["initrd.img\n(from build-assets)"]
        SCRIPTS["capsem-init + doctor\n+ bench + snapshots"]
        REPACK["cpio + gzip repack"]
        INITRD_IN --> REPACK
        AGENT & NETPROXY & MCP & SYSUTIL --> REPACK
        SCRIPTS --> REPACK
        REPACK --> INITRD_OUT["initrd.img\n(repacked)"]
    end

    subgraph stage3["3. Host binaries"]
        PNPM["pnpm install + astro build"]
        DIST["frontend/dist/"]
        CARGO_HOST["cargo build\n(6 host binaries)"]
        PNPM --> DIST --> CARGO_HOST
        CARGO_HOST --> SIGN["codesign\n(com.apple.security.virtualization)"]
    end

    subgraph stage0["0. VM images (first-time only)"]
        PROFILE["Profile V2 payload"]
        ADMIN["capsem-admin\nimage plan/build"]
        BUILDER["capsem-builder\n(Python build engine)"]
        DOCKER["Docker (via Colima)"]
        PROFILE --> ADMIN --> BUILDER --> DOCKER
        DOCKER --> VMLINUZ["vmlinuz"]
        DOCKER --> ROOTFS["rootfs.squashfs"]
        DOCKER --> INITRD_BASE["initrd.img (base)"]
    end

    INITRD_BASE -.-> INITRD_IN

    subgraph stage4["4. Boot"]
        SIGN --> BOOT["capsem-service\n+ capsem-process"]
        INITRD_OUT --> BOOT
        VMLINUZ --> BOOT
        ROOTFS --> BOOT
        BOOT --> VM["Linux VM running"]
    end

The guest agent crate (crates/capsem-agent/) produces four binaries that run inside the Linux VM, statically linked with musl:

BinaryPurposeTarget
capsem-pty-agentBridges terminal I/O over vsockaarch64-unknown-linux-musl / x86_64-unknown-linux-musl
capsem-net-proxyRelays HTTPS to host MITM proxy over vsocksame
capsem-mcp-serverMCP tool relay over vsocksame
capsem-sysutilLifecycle multi-call (shutdown/halt/poweroff/reboot/suspend)same

On macOS, cross_compile_agent() delegates to container_compile_agent() which builds natively inside a Linux container (docker). Per-arch named volumes (capsem-agent-target-{arch}) cache build artifacts. No host cross-compile toolchain needed.

On Linux (CI), cargo builds directly with the musl target. The linker config in .cargo/config.toml uses rust-lld:

[target.aarch64-unknown-linux-musl]
linker = "rust-lld"
[target.x86_64-unknown-linux-musl]
linker = "rust-lld"

just cross-compile [arch] builds everything in a container: agent binaries, frontend, and all host binaries (deb package). This catches system dep issues before CI.

Terminal window
just cross-compile # Build for host arch (arm64 on Apple Silicon)
just cross-compile x86_64 # Build x86_64 deb

The initrd is a gzipped cpio archive that the kernel unpacks into RAM at boot. The _pack-initrd recipe:

  1. Extracts the base initrd (produced by just build-assets)
  2. Copies in the freshly cross-compiled guest binaries (chmod 555, read-only)
  3. Copies in shell scripts: capsem-init (PID 1), capsem-doctor, capsem-bench, snapshots
  4. Repacks with cpio + gzip
  5. Regenerates BLAKE3 checksums (B3SUMS + manifest.json)

This is why just run is fast (~10s) — it only rebuilds what changed, not the full rootfs.

This stage has two parts: the frontend build and the Rust compilation.

The UI lives in frontend/ and is built by pnpm. The build chain:

  1. pnpm install — installs npm dependencies (Astro, Svelte, Tailwind, Preline, xterm.js, LayerChart, sql.js)
  2. astro build — compiles .astro and .svelte files into static HTML/JS/CSS in frontend/dist/
  3. The built frontend is served by capsem-gateway over HTTP (and bundled into capsem-app as offline fallback)

The frontend stack:

TechnologyRole
Astro 5Static site generator — page routing, builds the app shell
Svelte 5Reactive components — terminal view, stats charts, settings panels
Tailwind v4 + PrelineStyling — utility classes + themed CSS-only component library
xterm.js 6Terminal emulator — renders the in-VM shell
LayerChart 2Charts — session stats, cost tracking (D3-based Svelte library)
sql.jsSQLite in the browser — queries session DBs client-side

For frontend iteration without booting a VM, use just ui (Astro dev server with mock data on port 5173).

The Rust workspace produces multiple binaries. Six host binaries and the Tauri desktop app:

CrateBinaryRole
capsem-core(lib)All business logic: VM config, boot, vsock, MITM proxy, MCP endpoint, network policy, telemetry
capsem-servicecapsem-serviceBackground daemon: Axum HTTP over UDS, VM lifecycle
capsem-processcapsem-processPer-VM: boots VM, bridges vsock, manages jobs
capsemcapsemCLI: HTTP over UDS to service
capsem-mcpcapsem-mcpMCP server: stdio, bridges AI agent tool calls to service
capsem-gatewaycapsem-gatewayHTTP gateway: TCP:19222, proxies to service, WebSocket terminal
capsem-traycapsem-traySystem tray: polls gateway, shows VM status
capsem-appcapsem-appThin Tauri webview: points at gateway, bundled frontend fallback
capsem-proto(lib)Shared protocol types (host-guest, service-process IPC)
capsem-logger(lib)Session DB schema and async writer (SQLite)

On macOS, all binaries must be codesigned with the com.apple.security.virtualization entitlement or Virtualization.framework crashes. The justfile handles this automatically via the _sign recipe.

The service loads three boot assets from a signed manifest. Installed layouts use hash-named files in ~/.capsem/assets/{arch}/; development layouts use assets/{arch}/ plus hash aliases created by scripts/create_hash_assets.py:

AssetProduced byWhat it is
vmlinuzjust build-assetsCustom Linux kernel (no modules, no IP stack, 7MB)
initrd.imgjust run (repacked each time)Guest binaries + init scripts
rootfs.squashfsjust build-assetsDebian bookworm base + AI CLIs + tools

Boot sequence: capsem-service spawns capsem-process, which loads the kernel + initrd into a VM. capsem-init (PID 1) sets up overlayfs, air-gapped networking, and launches the PTY agent, network proxy, DNS proxy, MCP server, and sysutil. The host connects over vsock.

The slow path (~10 min, first-time only). capsem-admin image build reads a Profile V2 payload, materializes a generated build workspace, and produces kernel + rootfs via Docker.

Terminal window
uv run capsem-admin image build config/profiles/base/coding.profile.toml --arch arm64
uv run capsem-admin image build config/profiles/base/coding.profile.toml --dry-run --json

The builder needs Docker.

macOS — Docker runs inside a Colima VM. Minimum 12GB RAM, recommended 16GB (Tauri install-test build OOMs below 12GB).

Terminal window
# Colima setup (recommended on macOS)
brew install colima docker
colima start --vm-type vz --vz-rosetta --memory 16 --cpu 8

Linux — Docker runs natively, no memory tuning needed.

Terminal window
# Debian/Ubuntu
sudo apt install docker.io

When a vX.Y.Z tag is pushed, the release workflow runs. Jobs are parallelized to minimize wall-clock time (~18 min vs ~45 min sequential).

flowchart LR
    PF["preflight\n(macos-14, 30s)"]
    BA["build-assets\n(arm64 + x86_64\nubuntu, 10 min)"]
    T["test\n(macos-14, 8 min)"]
    BM["build-app-macos\n(macos-14, 15 min)"]
    BL["build-app-linux\n(arm64 + x86_64\nubuntu, 10 min)"]
    CR["create-release\n(ubuntu, 2 min)"]

    PF --> BA & T
    PF --> BM & BL
    BA --> BM & BL
    T --> CR
    BM --> CR
    BL --> CR
JobRunnerProduces
preflightmacos-14Validates Apple cert, Tauri key, notarization creds
build-assetsubuntu arm64 + x86_64vmlinuz, initrd.img, rootfs.squashfs per arch
testmacos-14Unit tests + coverage, frontend check, audit
build-app-macosmacos-14.pkg package, host binaries, signed manifest payload
build-app-linuxubuntu arm64 + x86_64.deb packages for both arches
create-releaseubuntuSigns manifest, verifies package payloads, creates GitHub release

Key design decisions:

  • test runs in parallel with build-assets and app builds — it gates create-release but doesn’t block compilation
  • arm64 Linux produces .deb only
  • The desktop auto-updater is disabled for this release line unless a future release ships a verified full-package updater feed

just cross-compile builds the Linux binaries inside a container and catches most issues, but the environments differ:

AspectLocal (container)CI (bare runner)
Baserust:bookwormubuntu-24.04
Nodenodesource scriptactions/setup-node
Volumesnone (clean build)none (fresh runner)

Everything below is checked by bootstrap.sh and just doctor. You don’t need to install these manually — the bootstrap script tells you exactly what’s missing.

ToolWhat it does in the build
Rust (stable)Compiles host + guest binaries
rust-lldLinker for musl cross-compilation
justTask runner — single entry point for all workflows
Node.js 24+ / pnpmBuilds the Astro + Svelte frontend
Python 3.11+ / uvRuns capsem-builder (image builds, schema generation)
Docker (via Colima on macOS)Container runtime for kernel + rootfs builds
cargo-llvm-covCode coverage (just test)
cargo-auditDependency vulnerability scanning
cargo-tauriTauri CLI for desktop app builds
b3sumBLAKE3 checksums for asset integrity
codesign (macOS)Signs binaries with virtualization entitlement