Build System
Capsem image builds are profile driven. capsem-admin is the enterprise-facing
CLI for profile creation, validation, image planning, asset verification, and
manifest generation. capsem-builder is the lower-level Python build engine it
uses to validate build inputs, render Jinja2 Dockerfiles, and produce
per-architecture VM assets.
The source of truth is the signed Profile V2 payload. Repo-local TOML under
guest/config/ is a developer input used to generate and test built-in
profiles; it is not the corporate release authority and it is not loaded by the
service at runtime.
Architecture
Section titled “Architecture”flowchart TD
subgraph Input["Source of Truth"]
PROFILE["Profile V2 payload\n(packages, tools, controls,\nVM assets, locks)"]
DEV["guest/config/*.toml\n(developer input for\nbuilt-in profiles)"]
end
subgraph Validation["Validation Layer"]
Admin["capsem-admin\nprofile/image commands"]
Models["Pydantic models\n(Profile, PackageContract,\nImagePlan, Manifest)"]
Validate["validate.py\nLinter (E001-E402, W001-W012)"]
end
subgraph Generation["Code Generation"]
Context["docker.py\n_rootfs_context()\n_kernel_context()"]
Jinja["Jinja2 Templates\nDockerfile.rootfs.j2\nDockerfile.kernel.j2"]
end
subgraph Output["Build Outputs"]
Docker["Docker Build"]
Assets["assets/{arch}/\nvmlinuz, initrd.img,\nrootfs.squashfs"]
BOM["asset manifest\nhashes + signatures + SBOM"]
end
DEV --> Admin
PROFILE --> Admin
Admin --> Models
Models --> Validate
Models --> Context
Context --> Jinja
Jinja --> Docker
Docker --> Assets
Assets --> BOM
Data flow
Section titled “Data flow”Profile payloads are the source of truth. The data flows through four layers:
- Profile V2 payloads — signed, typed declarations for packages, tools,
controls, VM assets, and editable sections. Developer TOML can derive these
profiles, but operators do not hand-edit image settings in
guest/config. - Pydantic models — type-safe validation with enums, frozen models, and cross-field validators.
- Context dicts (
docker.py) — template variables assembled from the validated config. Each template type (rootfs,kernel) has its own context builder that collects packages by manager type. - Jinja2 templates — Dockerfile output parameterized per architecture.
Three outputs are produced:
- Rendered Dockerfiles — Jinja2 templates (
Dockerfile.rootfs.j2,Dockerfile.kernel.j2) parameterized per architecture. - Verified VM assets —
vmlinuz,initrd.img, androotfs.squashfswith hashes/signatures recorded in the profile catalog path. - SBOM / asset manifests — package versions, BLAKE3 hashes, and vulnerability findings used by release verification.
Developer TOML Structure
Section titled “Developer TOML Structure”Built-in profile development still uses repo-local TOML under guest/config/.
Each file maps to a Pydantic model and feeds profile/image generation. This is
not an operator-facing configuration surface.
| File | Model | Purpose | Key Fields |
|---|---|---|---|
build.toml | BuildConfig | Architectures, compression | compression, compression_level, architectures.* |
manifest.toml | ImageManifestConfig | Image identity and changelog | name, version, description, changelog |
ai/*.toml | AiProviderConfig | AI provider definitions | api_key, network.domains, install (manager: npm/curl), cli, files |
packages/apt.toml | PackageSetConfig | Apt package set | manager, install_cmd, packages, network |
packages/python.toml | PackageSetConfig | Python package set | manager, install_cmd, packages |
mcp/*.toml | McpServerConfig | MCP server definitions | transport, command, url, args, env |
security/*.toml | Security control models | Developer seed inputs for built-in enforcement/detection profile packs | canonical rule roots, pack ids, fixtures |
vm/resources.toml | VmResourcesConfig | CPU, RAM, disk limits | cpu_count, ram_gb, scratch_disk_size_gb |
vm/environment.toml | VmEnvironmentConfig | Shell, PATH, TLS | shell.term, shell.home, shell.path, tls.ca_bundle |
kernel/defconfig.* | (raw) | Kernel configs per arch | Linux kernel defconfig files |
Example build.toml:
[build]compression = "zstd"compression_level = 15
[build.architectures.arm64]base_image = "debian:bookworm-slim"docker_platform = "linux/arm64"rust_target = "aarch64-unknown-linux-musl"kernel_branch = "6.6"kernel_image = "arch/arm64/boot/Image"defconfig = "kernel/defconfig.arm64"node_major = 24Example AI provider (ai/anthropic.toml):
[anthropic]name = "Anthropic"description = "Claude Code AI agent"enabled = true
[anthropic.api_key]name = "Anthropic API Key"env_vars = ["ANTHROPIC_API_KEY"]prefix = "sk-ant-"docs_url = "https://console.anthropic.com/settings/keys"
[anthropic.network]domains = ["*.anthropic.com", "*.claude.com"]allow_get = trueallow_post = true
[anthropic.install]manager = "curl"packages = ["https://claude.ai/install.sh"]Validation Pipeline
Section titled “Validation Pipeline”capsem-admin profile validate, capsem-admin image plan, and the lower-level
capsem-builder validate path run compiler-style diagnostics with error
codes, severity levels, and file:line references. Errors block the build;
warnings are informational.
Error Codes
Section titled “Error Codes”| Range | Category | Examples |
|---|---|---|
| E001-E002 | TOML parsing | Missing build.toml, invalid TOML syntax |
| E003-E005 | Pydantic validation | Schema violations, empty package lists, invalid enum values |
| E006 | Domain validation | URLs in domain fields, ports, path components |
| E008 | Duplicate keys | Same key in multiple files within a directory |
| E009-E010 | File content | Non-absolute paths, invalid JSON in .json file settings |
| E100-E103 | Schema / JSON | Generated JSON fails schema validation |
| E200-E202 | Cross-language | Rust/Python conformance mismatches |
| E300-E305 | Artifacts | Missing defconfig, CA cert, capsem-init, diagnostics |
| E400-E402 | Docker | Dockerfile generation failures |
Warning Codes
Section titled “Warning Codes”| Code | Description |
|---|---|
| W001 | Package sets configured but no registry in web security |
| W002 | Development packages (-dev, -devel) in package lists |
| W003 | Potential secrets detected in file content, headers, or env |
| W004 | Package set with no network config |
| W005 | Overlapping allow and block domain lists |
| W006 | Placeholder file content (TODO, FIXME) |
| W007 | Overly broad wildcard domains (*, *.com) |
| W008 | Duplicate env_vars across AI providers |
| W009 | Shell metacharacters in install_cmd |
| W010 | PATH missing essential directories (/usr/bin, /bin) |
| W011 | Wide-open network policy (both reads and writes, no block list) |
| W012 | Unknown Rust target (not a known musl target) |
Diagnostic output format:
error: [E006] config/ai/anthropic.toml: Invalid domain pattern 'https://api.anthropic.com'warning: [W003] config/mcp/capsem.toml: Potential secret in mcp.capsem.headers.AuthorizationMulti-Architecture Support
Section titled “Multi-Architecture Support”Two architectures are supported. Each is self-contained in build.toml and produces an independent asset directory.
| Architecture | Hypervisor | Docker Platform | Rust Target | Kernel Image |
|---|---|---|---|---|
| arm64 | Apple VZ (macOS) / KVM (Linux) | linux/arm64 | aarch64-unknown-linux-musl | arch/arm64/boot/Image |
| x86_64 | KVM | linux/amd64 | x86_64-unknown-linux-musl | arch/x86_64/boot/bzImage |
Output layout:
assets/ arm64/ vmlinuz initrd.img rootfs.squashfs tool-versions.txt image-inventory.json x86_64/ vmlinuz initrd.img rootfs.squashfs tool-versions.txt image-inventory.json manifest.json B3SUMSBuild Pipeline
Section titled “Build Pipeline”flowchart TD Load["Load TOML configs"] --> Validate["Validate (Pydantic + linter)"] Validate -->|errors| Abort["Abort with diagnostics"] Validate -->|clean| Arches["For each architecture"] Arches --> Cross["Cross-compile guest binaries\n(cargo build --target)"] Cross --> Render["Render Dockerfile.rootfs.j2"] Render --> Context["Assemble build context\n(CA cert, bashrc, diagnostics, binaries)"] Context --> Build["Docker build"] Build --> Export["Export container filesystem"] Export --> Squash["mksquashfs (zstd compression)"] Squash --> Versions["Extract tool versions"] Versions --> Checksums["Generate B3SUMS + manifest.json"]
The kernel build follows a parallel path:
flowchart TD KLoad["Load build.toml"] --> KResolve["Resolve kernel version\n(kernel.org LTS lookup)"] KResolve --> KRender["Render Dockerfile.kernel.j2"] KRender --> KBuild["Docker build\n(kernel compile + initrd)"] KBuild --> KExtract["Extract vmlinuz + initrd.img"]
Key implementation details:
- Container runtime auto-detection. Docker CLI.
- CI cache integration. Docker buildx with GitHub Actions cache (
type=gha) whenGITHUB_ACTIONSis set. - Kernel version resolution. Fetches the latest stable version for the configured LTS branch from
kernel.org/releases.json, falls back to a hardcoded version on network failure. - Cross-compilation. Guest agent binaries are cross-compiled with
cargo build --target {rust_target}usingrust-lldas the linker (configured in.cargo/config.toml). - Clock skew resilience. All
apt-get updatecalls use-o Acquire::Check-Valid-Until=falseto handle container VM clock drift.
Container Runtime Requirements
Section titled “Container Runtime Requirements”On macOS, Docker runs inside a Colima VM with limited resources. The rootfs build runs apt, npm, and curl-based CLI installers concurrently, requiring substantial memory.
| Threshold | RAM | Notes |
|---|---|---|
| Minimum | 12 GB | Tauri install-test cold build SIGTERMs below this (exit 143 mid-cargo) |
| Recommended | 16 GB | Comfortable margin for build-assets + install-test together |
| CI (GitHub Actions) | 7 GB | Standard runner; install-test container uses pre-baked image so no cold build |
# Colima (macOS): configure VM resourcescolima stopcolima start --vm-type vz --vz-rosetta --memory 16 --cpu 8
# Linux: Docker runs natively, no memory tuning needed# sudo apt install docker.iojust doctor and capsem-admin doctor both check these resources automatically and fail if below minimum.
Install Manager Types
Section titled “Install Manager Types”AI providers declare how their CLI gets installed via [provider.install]. The builder supports multiple install strategies:
| Manager | Template Handling | Use Case | Example |
|---|---|---|---|
npm | Batched into single npm install -g --prefix | Node.js CLI tools | Gemini CLI, Codex |
curl | Each URL gets its own RUN curl -fsSL URL | bash | Native binary installers | Claude Code |
apt | Package set (not per-provider) | System packages | coreutils, git, curl |
uv | Package set (not per-provider) | Python packages | numpy, pytest |
pip | Package set (not per-provider) | Python packages (fallback) | — |
The /root tmpfs constraint
Section titled “The /root tmpfs constraint”At runtime, /root is a tmpfs overlay — anything baked into the rootfs under /root/ during the Docker build is hidden. This matters for CLI installers that put binaries in ~/.local/bin/ or ~/.claude/bin/:
# The installer puts claude at ~/.local/bin/claude, which is /root/.local/bin/# inside the container. Since /root is tmpfs at runtime, copy to /usr/local/bin.RUN curl -fsSL https://claude.ai/install.sh | bash && \ for bin in /root/.local/bin/*; do \ [ -f "$bin" ] && install -m 555 "$bin" /usr/local/bin/; \ doneThe install -m 555 enforces the guest binary security invariant: all binaries are read-only, non-writable by the guest.
Adding a new install manager
Section titled “Adding a new install manager”To add a new manager type (e.g., cargo):
- Add the enum value to
PackageManagerinmodels.py - Collect packages in
_rootfs_context()indocker.py— create a new list variable - Pass it to the template context dict
- Add a Jinja2 block in
Dockerfile.rootfs.j2 - Add to
_INSTALL_CMDSinscaffold.py - Update tests in
test_docker.pyandtest_cli.py
Rootfs Dockerfile layer structure
Section titled “Rootfs Dockerfile layer structure”The generated Dockerfile.rootfs.j2 follows a specific ordering. Understanding this is important when adding new install steps — the /root cleanup and binary permissions are load-bearing:
flowchart TD A["1. apt packages\n(system tools, runtimes)"] --> B["2. Node.js via nvm\n(for npm-based CLIs)"] B --> C["3. uv installer\n(Python package manager)"] C --> D["4. npm install\n(Gemini CLI, Codex)"] D --> E["5. CA certificate\n+ certifi patch"] E --> F["6. Guest binaries\n(COPY + chmod 555)"] F --> G["7. Shell config + diagnostics\n(bashrc, banner, tests)"] G --> H["8. Python packages\n(uv pip install)"] H --> I["9. Security hardening\n(strip setuid, rm EXTERNALLY-MANAGED)"] I --> J["10. rm -rf /root\n(clean HOME for tmpfs)"] J --> K["11. curl installers\n(Claude Code, copy to /usr/local/bin)"] K --> L["12. Switch apt to HTTPS"] style J fill:#f9f,stroke:#333 style K fill:#bbf,stroke:#333
Step 10 and 11 ordering matters: curl installers run after the /root cleanup so there’s a clean HOME. Binaries are immediately copied to /usr/local/bin/ since /root becomes tmpfs at boot.
Manifest and BOM
Section titled “Manifest and BOM”Every build produces manifest.json at the asset root. The BOM records:
| Section | Source | Contents |
|---|---|---|
| Packages (dpkg) | dpkg-query output | Name, version, architecture |
| Packages (pip) | pip list --format json | Name, version |
| Packages (npm) | npm ls --json --global | Name, version |
| Assets | b3sum output | Filename, BLAKE3 hash, size in bytes |
| Vulnerabilities | Trivy or Grype scan | CVE ID, severity, package, installed/fixed versions |
The audit subcommand parses vulnerability scanner output and fails on CRITICAL or HIGH findings.
CLI Commands
Section titled “CLI Commands”| Command | Description | Key Options |
|---|---|---|
build | Render Dockerfiles or build images | --arch, --dry-run, --json, --template, --output, --kernel-version |
validate | Lint and validate guest config | --artifacts (check built artifacts too) |
inspect | Show config summary | --json |
audit | Parse vulnerability scan results | --scanner (trivy/grype), --input, --json |
init | Scaffold a minimal guest config directory | --force |
new | Create a new image config from a base | --from, --non-interactive, --force |
add ai-provider | Add an AI provider template | --dir, --force |
add packages | Add a package set template | --dir, --manager, --force |
add mcp | Add an MCP server template | --dir, --transport, --force |
mcp | Start MCP stdio server for builder tools | (none) |
doctor | Check build prerequisites | (none) |
Usage:
# Validate configuv run capsem-builder validate guest
# Dry-run: render Dockerfiles without buildinguv run capsem-admin image build config/profiles/base/coding.profile.toml --dry-run --json
# Build rootfs for arm64 onlyuv run capsem-admin image build config/profiles/base/coding.profile.toml --arch arm64
# Build kernel for all architecturesuv run capsem-admin image build config/profiles/base/coding.profile.toml --template kernel
# Scaffold a new image configuv run capsem-builder new my-image --from guestSettings And Schema Artifacts
Section titled “Settings And Schema Artifacts”The builder/admin tooling publishes schema artifacts for validation and docs. Those artifacts are not runtime defaults authority.
flowchart LR Profile["Profile Pydantic models"] --> PS["capsem.profile.v2 schema"] Settings["Service settings Pydantic model"] --> SS["capsem.service-settings.v2 schema"] Descriptor["Guest/UI descriptor Pydantic models"] --> DS["settings-schema.json"] PS --> CV["Cross-language\nconformance tests"] SS --> CV DS --> CV
capsem-admin validates profile and service-settings JSON/TOML through
Pydantic first, then emits structured JSON reports. Rust validates the same
closed contracts at runtime. The guest/UI descriptor schema describes renderable
settings nodes for the UI and tests.
Cross-language conformance tests verify that:
- Python and Rust agree on Service Settings V2 defaults and invalid shapes.
- Profile payload fixtures round-trip through the typed model and reject unknown fields.
- UI descriptor fixtures remain parseable in Python, Rust, and TypeScript.
This keeps Python tooling, Rust runtime contracts, and frontend rendering in lockstep without reviving generated runtime defaults.