Asset Pipeline
The asset pipeline moves kernel, initrd, and rootfs images from build through to boot. Assets are per-architecture (arm64 for Apple Silicon, x86_64 for Linux/KVM), integrity-checked with BLAKE3 hashes at every stage, and distributed via a version-scoped manifest.
Profile V2 payloads are the build authority for release assets. The
capsem-admin CLI derives a temporary build workspace, renders the existing
Jinja2 Dockerfile templates, and produces per-architecture assets:
capsem.profile.v2 -> capsem-admin image build -> generated workspace -> assets/{arch}/Two build templates exist:
| Template | Output | What it does |
|---|---|---|
kernel | vmlinuz, initrd.img | Builds a minimal Linux kernel from defconfig |
rootfs | rootfs.squashfs | Builds the full guest filesystem with packages, runtimes, and tools |
The build process also cross-compiles the canonical guest binaries (capsem-pty-agent, capsem-net-proxy, capsem-dns-proxy, capsem-mcp-server, capsem-sysutil) for the target architecture and injects them into the rootfs.
Output layout
Section titled “Output layout”assets/ arm64/ vmlinuz initrd.img rootfs.squashfs vmlinuz-<hash16> initrd-<hash16>.img rootfs-<hash16>.squashfs x86_64/ vmlinuz initrd.img rootfs.squashfs vmlinuz-<hash16> initrd-<hash16>.img rootfs-<hash16>.squashfs manifest.json manifest.json.minisig B3SUMSCommands
Section titled “Commands”| Command | What it does |
|---|---|
just build-assets | Full build using config/profiles/base/coding.profile.toml: kernel + rootfs + checksums |
just run | Repack initrd with latest guest binaries, rebuild app, sign, boot |
capsem-admin image build config/profiles/base/coding.profile.toml --arch arm64 --template rootfs | Build one template for one arch |
Manifest Format
Section titled “Manifest Format”The manifest (assets/manifest.json, format 2) is a single top-level file covering every arch. Asset versions and binary versions are tracked independently with compatibility ranges (min_binary, min_assets):
{ "format": 2, "assets": { "current": "2026.0421.30", "releases": { "2026.0421.30": { "date": "2026-04-21", "deprecated": false, "min_binary": "1.0.0", "arches": { "arm64": { "vmlinuz": {"hash": "<64-char blake3>", "size": 7797248}, "initrd.img": {"hash": "<blake3>", "size": 2314963}, "rootfs.squashfs": {"hash": "<blake3>", "size": 454230016} }, "x86_64": { "...": "..." } } } } }, "binaries": { "current": "1.0.1776688771", "releases": { "1.0.1776688771": { "date": "2026-04-21", "deprecated": false, "min_assets": "2026.0421.30" } } }}Key points:
- Single file, not per-arch. Arches are nested under
assets.releases.<ver>.arches.<arch>. - Filenames are bare (
"vmlinuz", not"arm64/vmlinuz") — the arch map provides the context. - Hashes are BLAKE3, 64 lowercase hex characters. Format is validated by
asset_manager.rs; non-format-2 manifests are rejected. - Compatibility is explicit.
min_binaryon an asset release andmin_assetson a binary release define the allowed pairings for upgrades and downloads.
Two manifest producers
Section titled “Two manifest producers”| Producer | Used by | When |
|---|---|---|
docker.py:generate_checksums() | just build-assets | After full image builds |
scripts/gen_manifest.py | just _pack-initrd | After injecting updated guest binaries into initrd |
Both emit the same format-2 schema and use the same YYYY.MMDD.patch same-day increment rules. scripts/create_hash_assets.py then creates <stem>-<hex16>.<ext> hardlinks so the dev layout matches the content-addressable names used by the installed layout.
Runtime Hash Verification
Section titled “Runtime Hash Verification”Asset hashes are not baked into the binary at compile time — that would tie every binary release to a specific asset release and defeat the min_binary/min_assets compatibility model. Instead, the binary is hash-agnostic; the manifest on disk is authoritative, and its authenticity is established by a minisign signature verified against a pubkey baked into the binary (config/manifest-sign.pub, key id 93A070CBB288AC9B).
At boot (crates/capsem-core/src/vm/boot.rs):
asset_manager::load_verified_manifest_for_assets(assets, require_signature)readsmanifest.jsonfrom the assets dir or its parent, and verifies the siblingmanifest.json.minisigagainst the baked release pubkey.ManifestV2::expected_hashes_current(host_manifest_arch())looks up the kernel/initrd/rootfs hashes for the current release on the host arch (aarch64->arm64mapped).- The hashes are passed to
VmConfig::builder()viaexpected_kernel_hash/expected_initrd_hash/expected_disk_hash;VmConfig::build()hashes the files and refuses to boot on mismatch.
Failure modes:
- No manifest at all: local development layouts may skip hash verification (
[boot-audit] asset hash verification disabled) for fresh checkouts without assets built yet. Installed package layouts require a signed manifest. - Manifest present, no
.minisig: local debug builds can proceed only in development layouts. Installed macOS.pkgand Linux.deblayouts hard-fail — an untrusted manifest must not drive hash verification. - Manifest present,
.minisiginvalid: always hard-fail, regardless of build profile. A signature mismatch is a loud signal.
Manifests are signed during the release workflow (scripts/check-release-workflow.sh uses minisign -Sm assets/manifest.json). The corresponding pubkey in config/manifest-sign.pub is included via include_str! at compile time, so the signing/verification loop is self-contained and does not depend on any TLS or external trust root.
Runtime Asset Resolution
Section titled “Runtime Asset Resolution”Step 1: Find assets directory
Section titled “Step 1: Find assets directory”resolve_assets_dir() searches these locations in order, returning the first that contains vmlinuz:
CAPSEM_ASSETS_DIRenvironment variable (dev override)- macOS
.appbundleContents/Resources/ ./assets(workspace root)../../assets(from crate directory)
For each candidate, it checks per-arch first (candidate/{arch}/vmlinuz), then flat (candidate/vmlinuz).
Step 2: Resolve manifest-selected assets
Section titled “Step 2: Resolve manifest-selected assets”ManifestV2::resolve() selects the compatible asset release for the running binary, then resolves hash-named assets in either the flat development layout or the installed per-arch layout:
- Flat:
{assets_dir}/<stem>-<hash16>.<ext> - Per-arch:
{assets_dir}/{arch}/<stem>-<hash16>.<ext>
Step 3: Download if missing
Section titled “Step 3: Download if missing”If rootfs is not found locally, create_asset_manager() loads the manifest and initiates download:
- Loads
manifest.jsonfrom assets dir or its parent (handles per-arch layout) - Creates
AssetManagerwith per-arch download directory (~/.capsem/assets/{arch}/) - Downloads from GitHub Releases with HTTP resume support (Range headers)
- Verifies BLAKE3 hash after download, deletes on mismatch
- Atomically renames temp file to final path
Step 4: Boot
Section titled “Step 4: Boot”boot_vm() builds VmConfig with manifest-selected asset paths and hashes:
VmConfig::builder() .kernel_path(assets/{arch}/vmlinuz-<hash16>) + expected_kernel_hash .initrd_path(assets/{arch}/initrd-<hash16>.img) + expected_initrd_hash .disk_path(assets/{arch}/rootfs-<hash16>.squashfs) + expected_disk_hash .build() // verifies all hashesbuild() calls verify_hash() for each file — reads in 64KB chunks, computes BLAKE3, compares with expected. A HashMismatch error prevents boot entirely.
Hash Verification Summary
Section titled “Hash Verification Summary”Assets are verified at multiple points:
| When | Where | What happens on mismatch |
|---|---|---|
| After download | asset_manager.rs | Temp file deleted, download retried |
| Before boot | vm/config.rs | ConfigError::HashMismatch, boot prevented |
Both use BLAKE3 with 64-character hex format. Both checks source their expected hashes from the same manifest.json on disk — the boot check just re-reads it via load_manifest_for_assets() at boot_vm() time.
Per-Architecture Isolation
Section titled “Per-Architecture Isolation”- A Capsem binary supports exactly one architecture (no runtime switching);
std::env::consts::ARCHis used to select the manifest arch key. host_manifest_arch()mapsaarch64->arm64(the key used in the manifest).- The manifest has separate hash entries per arch — no cross-arch confusion is possible.
flowchart LR
subgraph Build
Profile[Profile V2 payload] --> Admin[capsem-admin image build]
Admin --> Builder[capsem-builder]
Builder --> Assets[assets/arm64/]
Builder --> Checksums[manifest.json]
end
subgraph Runtime
Checksums --> LoadManifest[load_manifest_for_assets]
LoadManifest --> ExpectedHashes[expected_hashes_current]
ExpectedHashes --> Boot[boot_vm]
Assets --> Resolve[resolve_assets_dir]
Resolve --> Boot
Boot --> Verify[verify_hash BLAKE3]
Verify --> VZ[VZLinuxBootLoader]
end