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["config/profiles/<id>/profile.toml\n+ referenced sibling files"]
        ADMIN["capsem-admin image build"]
        BUILDER["capsem-builder\nbackend"]
        DOCKER["Docker (via Colima)"]
        PROFILE --> ADMIN --> BUILDER --> DOCKER
        DOCKER --> VMLINUZ["vmlinuz"]
        DOCKER --> ROOTFS["rootfs.erofs"]
        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 for the Linux .deb package. This catches system dependency 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 code)
  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)
  6. _materialize-config uses the updated manifest to generate target/config/profiles/code/profile.toml

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 for the service-unavailable screen.

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 for the service-unavailable screen
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 the selected profile from target/config/profiles in development and the installed profile directory in packaged builds. That profile selects three assets from ~/.capsem/assets/ (installed) or assets/{arch}/ (development):

AssetProduced byWhat it is
vmlinuzjust build-assets code [arch]Custom Linux kernel
initrd.imgjust run (repacked each time)Guest binaries + init scripts
rootfs.erofsjust build-assets code [arch]Debian bookworm base + AI CLIs + tools, EROFS/LZ4HC

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 + net proxy + MCP server + sysutil. The host connects over vsock.

The slow path (~10 min, first-time only). The capsem-admin image rail validates the selected profile, materializes a backend image workspace, and then uses the Python builder to produce kernel + rootfs via Docker.

Terminal window
cargo run -p capsem-admin -- image build --profile config/profiles/code/profile.toml --config-root config --arch arm64
uv run capsem-builder doctor --profile code --config-root config # check prerequisites and profile

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.erofs per arch
testmacos-14Unit tests + coverage, frontend check, audit
build-app-macosmacos-14.pkg installer, notarized + stapled
build-app-linuxubuntu arm64 + x86_64.deb packages for both arches
create-releaseubuntuPublishes packages, manifest.json, and arch-prefixed VM assets

Key design decisions:

  • test runs in parallel with build-assets and app builds — it gates create-release but doesn’t block compilation
  • Linux produces .deb only on both arm64 and x86_64. AppImage is intentionally disabled.
  • VM assets are not bundled into the installers. They are uploaded as GitHub release assets named <arch>-vmlinuz, <arch>-initrd.img, <arch>-rootfs.erofs, and <arch>-obom.cdx.json; the v2 manifest keeps bare logical names under each arch map and every downloaded asset is verified before boot.

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