| name | build-images |
| description | Building Capsem VM images with capsem-builder. Use when working with guest image configuration, Dockerfiles, kernel builds, rootfs builds, the builder CLI, or guest config TOML files. Covers the config-driven build system, guest config layout, Dockerfile templates, multi-arch support, the builder CLI commands, AND the internal architecture for modifying the builder itself (models, context flow, template variables, adding install managers). |
Building VM Images
Overview
capsem-builder is a config-driven build system. It reads TOML configs from guest/config/, renders Jinja2 Dockerfile templates, and builds kernel + rootfs via Docker. Assets output to assets/{arch}/.
Guest config layout
guest/config/
build.toml Architectures, compression, base images
manifest.toml Image name, version, changelog
ai/*.toml AI provider configs (Claude, Gemini, Codex)
packages/*.toml Package sets (apt, python)
mcp/*.toml MCP server configs
security/web.toml Web security (allow/block domains)
vm/resources.toml CPU, RAM, disk
vm/environment.toml Shell, TLS, env vars
kernel/*.defconfig Kernel defconfigs per architecture
All configs use Pydantic models for validation. Run uv run capsem-builder validate guest/ to lint.
CLI commands
uv run capsem-builder doctor guest/
uv run capsem-builder validate guest/
uv run capsem-builder build guest/ --dry-run
uv run capsem-builder build guest/ --arch arm64 --template rootfs
uv run capsem-builder build guest/ --arch arm64 --template kernel
uv run capsem-builder inspect guest/
uv run capsem-builder new my-image/ --from guest/
uv run capsem-builder audit
Building assets
Full rebuild (kernel + rootfs):
just build-assets
Individual templates:
just build-kernel arm64
just build-rootfs arm64
Per-arch asset layout
assets/
manifest.json Version, checksums, asset list
B3SUMS BLAKE3 checksums
arm64/
vmlinuz Kernel
rootfs.squashfs Root filesystem
initrd.img Initial ramdisk (repacked by just run)
Adding packages to the VM
- Edit the appropriate config in
guest/config/packages/ (apt or python TOML)
- Run
uv run capsem-builder validate guest/ to check
- Run
just build-assets to rebuild the rootfs
- Verify:
just run "capsem-doctor"
Do not edit Dockerfiles directly -- they are rendered from Jinja2 templates in src/capsem/builder/templates/.
Adding a new AI provider
- Create
guest/config/ai/<provider>.toml with provider config
- Add domain entries to
guest/config/security/web.toml if needed
- Validate:
uv run capsem-builder validate guest/
- Rebuild:
just build-assets
Dockerfile templates
Templates live in src/capsem/builder/templates/:
Dockerfile.rootfs.j2 -- rootfs image (apt packages, Python packages, AI CLIs, diagnostics)
Dockerfile.kernel.j2 -- kernel build (defconfig, modules, vmlinuz extraction)
Templates use Jinja2 with variables from the merged guest config. Preview with --dry-run.
Builder Internals (for modifying the builder itself)
Architecture: TOML -> Pydantic -> context dict -> Jinja2 -> Dockerfile
The data flows through four layers:
- TOML configs (
guest/config/) -- user-facing, declarative
- Pydantic models (
src/capsem/builder/models.py) -- validation + types
- Context dict (
src/capsem/builder/docker.py) -- template variables
- Jinja2 templates (
src/capsem/builder/templates/) -- Dockerfile output
Key files
| File | Role |
|---|
src/capsem/builder/models.py | All Pydantic models (enums, configs, top-level GuestImageConfig) |
src/capsem/builder/config.py | TOML loader: walks guest/config/, returns GuestImageConfig |
src/capsem/builder/docker.py | Context builders (_rootfs_context, _kernel_context), rendering, build execution |
src/capsem/builder/templates/Dockerfile.rootfs.j2 | Rootfs Dockerfile template |
src/capsem/builder/templates/Dockerfile.kernel.j2 | Kernel Dockerfile template |
src/capsem/builder/scaffold.py | _INSTALL_CMDS dict + scaffolding for capsem-builder new |
src/capsem/builder/validate.py | Validation rules (E001-E302, W001-W012) |
src/capsem/builder/cli.py | Click CLI entry points |
Context dict (rootfs template variables)
_rootfs_context() in docker.py builds the dict passed to Dockerfile.rootfs.j2:
{
"arch": ArchConfig,
"arch_name": str,
"apt_packages": list[str],
"python_packages": list[str],
"python_install_cmd": str,
"npm_packages": list[str],
"npm_prefix": str,
"curl_installs": list[str],
"guest_binaries": list[str],
}
Kernel context dict
{
"arch": ArchConfig,
"arch_name": str,
"kernel_version": str,
}
How to: Add a new install manager
Example: adding a curl manager so a CLI can be installed via curl | bash instead of npm.
Step 1: Add enum value to PackageManager
In src/capsem/builder/models.py:
class PackageManager(str, Enum):
APT = "apt"
UV = "uv"
PIP = "pip"
NPM = "npm"
CURL = "curl"
Step 2: Collect packages in _rootfs_context()
In src/capsem/builder/docker.py, add a new list and populate it from providers:
curl_installs: list[str] = []
for provider in config.ai_providers.values():
if provider.enabled and provider.install:
if provider.install.manager == PackageManager.CURL:
curl_installs.extend(provider.install.packages)
Add "curl_installs": curl_installs to the returned dict.
Step 3: Add template block
In src/capsem/builder/templates/Dockerfile.rootfs.j2:
{% for url in curl_installs %}
# CLI installed via installer script
RUN curl -fsSL {{ url }} | bash
{% endfor %}
Step 4: Add to scaffold
In src/capsem/builder/scaffold.py, add to _INSTALL_CMDS:
"curl": "curl -fsSL",
Step 5: Update the TOML config
In guest/config/ai/<provider>.toml:
[provider.install]
manager = "curl"
packages = ["https://example.com/install.sh"]
Step 6: Update tests
tests/test_docker.py -- context dict assertions (what's in npm_packages vs curl_installs)
tests/test_cli.py -- Dockerfile rendering assertions (corporate config tests)
How to: Change how an AI CLI is installed
- Edit
guest/config/ai/<provider>.toml -- change [provider.install] section
- If changing install manager type, may need to update
_rootfs_context() in docker.py
- Check
extract_tool_versions() in docker.py -- it hardcodes version-check paths
- Update tests in
test_docker.py and test_cli.py
- Rebuild:
just build-assets && just run "capsem-doctor"
How to: Add a new package to an existing set
- Edit
guest/config/packages/apt.toml or guest/config/packages/python.toml
- Add the package name to the
packages list
- Validate:
uv run capsem-builder validate guest/
- Rebuild:
just build-assets
How to: Add a new guest binary
Guest binaries are compiled from crates/capsem-agent/. On macOS, cross_compile_agent() delegates to container_compile_agent() which builds inside a Linux container (docker). On Linux (CI), cargo builds natively.
- Add the binary target in
crates/capsem-agent/Cargo.toml
- Add the binary name to
GUEST_BINARIES list in docker.py
- The template already loops
{% for binary in guest_binaries %} to COPY + chmod 555
Verifying Linux builds locally
just cross-compile [arch] builds everything in a container: agent binaries, frontend, and the full Tauri app (deb + AppImage). Useful for catching linuxdeploy and system dep issues before CI.
just cross-compile
just cross-compile x86_64
AI provider TOML schema
[provider_key]
name = "Provider Name"
description = "What this provider does"
enabled = true
[provider_key.cli]
key = "cli-binary-name"
name = "CLI Display Name"
[provider_key.api_key]
name = "API Key Name"
env_vars = ["ENV_VAR_NAME"]
prefix = "sk-"
docs_url = "https://..."
[provider_key.network]
domains = ["*.example.com"]
allow_get = true
allow_post = true
[provider_key.install]
manager = "npm"
prefix = "/opt/ai-clis"
packages = ["@scope/package"]
[provider_key.files.some_config]
path = "/root/.config/file.json"
content = '{"key": "value"}'
Build pipeline (what build_image() does)
For rootfs:
- Build guest agent binaries (
cross_compile_agent -- on macOS delegates to container_compile_agent which builds inside a Linux container; on Linux compiles natively)
- Assemble build context (
prepare_build_context) -- copies CA cert, shell configs, diagnostics, agent binaries
- Render Dockerfile from template
docker build
- Export container filesystem as tar
- Create squashfs from tar (
create_squashfs -- runs mksquashfs in a container)
- Extract tool versions (
extract_tool_versions)
- Clean up container image
For kernel:
- Resolve latest kernel version from kernel.org
- Assemble build context (defconfig, capsem-init)
- Render Dockerfile from template
docker build
- Extract vmlinuz + initrd.img from image
- Clean up
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 --
the default RAM allocation may cause OOM kills (exit code 137).
Minimum: 12GB RAM. Recommended: 16GB RAM, 8 CPUs.
colima stop && colima start --vm-type vz --vz-rosetta --memory 16 --cpu 8
just doctor and capsem-builder doctor both check these resources automatically.
The resource check lives in src/capsem/builder/doctor.py:
check_container_resources() -- checks docker info
- Thresholds:
DOCKER_MIN_MEMORY_MB = 4096, DOCKER_RECOMMENDED_MEMORY_MB = 8192
Container image compatibility
The container builds use rust:slim-bookworm -- a minimal Debian image. Many common utilities (file, less, vim, etc.) are NOT available. Any shell commands run inside the container must use only coreutils (ls, cp, cat, test, etc.) or tools explicitly installed via apt-get in the same RUN step.
Lesson learned: using file /output/binary to verify compiled binaries failed because file is not in slim images. Replaced with ls -l which is always available and still confirms the copy succeeded. The real validation (existence + non-zero size) is done in Python after the container exits.
Rule: never assume a command exists in a slim container image. Stick to coreutils or install what you need explicitly.
Clock skew workaround
All apt-get update calls use -o Acquire::Check-Valid-Until=false to handle container VM clock drift.
Without this, apt rejects Release files whose timestamp is in the future relative to the VM's clock.
This can occur with any container VM backend on macOS.
Files affected:
Dockerfile.kernel.j2 (line 11)
Dockerfile.rootfs.j2 (line 11)
docker.py create_squashfs() function