build-images
Skill179 starsChanged 13 days agoApache-2.0View on GitHub
Building the Capsem VM runtime (kernel, initrd, rootfs). Use when working on the runtime package set, Dockerfile templates, kernel or rootfs builds, or the capsem-builder backend.
- Installs packages
In this repository37 other files it gives its agents.
- AGENTS.mdAGENTS.md
- asset-pipelineSkill
- build-initrdSkill
- citadelSkill
- dev-benchmarkSkill
- dev-bug-reviewSkill
- dev-cacheSkill
- dev-capsem-doctorSkill
- dev-capsemSkill
- dev-ciSkill
- dev-debuggingSkill
- dev-gateSkill
- dev-installationSkill
- dev-justSkill
- dev-mcpSkill
- dev-mitm-proxySkill
- dev-rust-patternsSkill
- dev-session-debugSkill
- dev-setupSkill
- dev-skillsSkill
- dev-sprintSkill
- dev-startSkill
- dev-testing-frontendSkill
- dev-testing-hypervisorSkill
- dev-testing-pythonSkill
- dev-testingSkill
- dev-testing-vmSkill
- frontend-designSkill
- ironbankSkill
- meta-find-skillsSkill
- meta-organize-skillsSkill
- meta-skill-creationSkill
- release-processSkill
- site-architectureSkill
- site-infraSkill
- site-marketingSkill
- use-capsemSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
13 rules. About 0 are common, 4 are distinctive, and 9 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
9
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- could be automatic: a setting could enforce it
- Very specific: line 66
Use admin/just recipes for all runtime image work.
- Very specific: line 113
Add the package to runtime_apt_packages (no duplicates.
- Very specific: line 116
Verify with capsem-doctor inside a booted VM.
- Very specific: line 118
Do not edit generated Dockerfiles.
- Distinctive: line 166
Keep the verified registry alias when retagging an imported helper.
- Distinctive: line 171
Every child build includes the Linux parity base and source lane: use the shared exact-reference check before building and verify the …
- Very specific: line 181
Every architecture's base_image is required to be its immutable repository@sha256:<child-manifest> identity.
- Very specific: line 256
Add the binary target in crates/capsem-agent/Cargo.toml
- Very specific: line 257
Add the binary name to GUEST_BINARIES list in docker.py
- Very specific: line 281
Create EROFS from tar (create_erofs -- runs mkfs.erofs in a container)
- Distinctive: line 285
Read the exact kernel version and SHA-256 from the checked-in build config
- Distinctive: line 420
Rule: never assume a command exists in a slim container image.
- Very specific: line 424
All asset dependency-helper apt-get update calls use -o Acquire::Check-Valid-Until=false -o Acquire::Check-Date=false against the …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Building the Capsem VM runtime
Overview
Capsem builds one VM runtime: a kernel, initrd and rootfs per architecture.
Applications come from OCI images (images/,
resolved through images/catalog.toml), never from the runtime rootfs, so
every build of a commit produces the same asset set.
config/docker/image/is the runtime's source:build.toml(package set, EROFS settings, size budgets, kernel pin, base images),manifest.toml, and thekernel/,security/andvm/inputs.guest/artifacts/holds the core guest payloads:capsem-init, doctor, diagnostics, bench, the container launcher.capsem-admin image buildcopies those two inputs into a generated backend workspace undercache/target/build/image-workspace/<arch|all>/and runs the build there.- The Python builder backend renders Docker templates and emits assets, build ledgers, and OBOMs only when invoked by the admin build rail. Do not add product truth directly to the backend image-spec path.
Source Layout
config/
docker/ Dockerfile templates (*.j2)
docker/image/
build.toml Runtime package set, EROFS, rootfs budgets, kernel pin
manifest.toml Image manifest metadata and changelog
kernel/ defconfig.<arch> and ordered kernel patches
security/ Built-in web security inputs
vm/ Guest shell environment
guest/artifacts/ Core guest payloads: init, doctor, diagnostics, bench
images/ OCI application images and their catalog
cache/target/assets/ Generated VM assets
cache/target/packages/ Generated native packagesThe materialized backend workspace is an implementation detail, not an authoring surface. It is never a config root.
capsem-admin is a tool, not a config authority. It validates, materializes,
builds, and checks contracts; it must not grow scaffolding commands that invent
MCP, AI provider, package, or rule truth. Do not add admin config roots, guest
config roots, settings metadata, provider registries, or backend-owned
catalogs as product truth.
CLI commands
just build-assets [arch] # The runtime for one arch, or every one
just _build-assets [arch] # Same rebuild, CI-facing name
just _build-kernel arm64 # Kernel slice
just _build-rootfs arm64 # Rootfs slice
uv run --project build_system --frozen capsem-builder audit # Parse trivy/grype vulnerability outputUnderneath, the gate runs
capsem-admin image build [--config-root config] [--guest-dir guest] [--output assets] [--arch arm64|x86_64] [--template all|kernel|rootfs] [--clean] [--json].
Use admin/just recipes for all runtime image work. capsem-builder is a
backend helper only; it must not expose or document public build, validate,
inspect, mcp, render-only, or dry-run rails for image authoring.
capsem-admin image build may call private Python modules such as
capsem_builder.image.image_build_backend; agents must not make those modules
public CLI contracts.
Per-arch asset layout
cache/target/assets/
manifest.json Version, checksums, asset list
B3SUMS BLAKE3 checksums
arm64/
vmlinuz Kernel
rootfs.erofs Root filesystem
initrd.img Initial ramdisk (repacked by just exec)The approved release default is EROFS with lz4hc compression level 12
(config/docker/image/build.toml [build.erofs]).
config/docker/image/build.toml [build.rootfs] also owns two independent
release budgets: the exported tar ceiling and the final EROFS ceiling. The
builder checks the first before compression and the second before writing the
artifact ledger. Never raise a size ceiling to accommodate an unexplained
image jump; inspect the rootfs composition first.
Build Ledger
Each per-arch build emits build-ledger.log JSONL. The
rootfs.config_inputs record captures the rendered rootfs package list and
the EROFS config. Installed-package/component truth belongs in the CycloneDX
OBOM, not the build ledger.
Adding a guest tool
The runtime package set is
config/docker/image/build.toml [build.rootfs].runtime_apt_packages. Add a
package there only when Capsem's own guest machinery needs it: the
container launcher (runc, umoci, iptables, iproute2), capsem-init (auditd,
e2fsprogs), the trust store the Capsem CA lands in, or what capsem-doctor,
capsem-bench and the network diagnostics run with.
Anything a user or an agent works with -- an AI CLI, a language toolchain, a
vendor binary -- belongs in an OCI image under images/, not in the runtime.
- Add the package to
runtime_apt_packages(no duplicates; the model refuses an empty or duplicated list). - Rebuild with
just build-assets(orjust _build-rootfs <arch>). - Verify with
capsem-doctorinside a booted VM.
Do not edit generated Dockerfiles. Docker templates live under config/docker/.
Dockerfile templates
Templates live in config/docker/:
Dockerfile.rootfs-dependencies.j2-- snapshot-selected Debian packages;Dockerfile.kernel-dependencies.j2-- snapshot-selected kernel toolchain and the SHA-256-verified kernel archive;Dockerfile.rootfs.j2-- network-denied first-party rootfs assembly;Dockerfile.kernel.j2-- network-denied kernel compilation, initrd assembly, and vmlinuz extraction.
asset-dependencies is the visible resumable frontier between those pairs.
The gate materializes one input-keyed helper for every selected
architecture/template, validates its platform and identity label, and
passes only its exact image ID to the source build. Source builds always use
BuildKit network none and never use the remote CI cache. A carried frontier
must revalidate every helper; it must not silently rebuild inside the sealed
lane. The dependency helpers reuse the one checked-in Debian snapshot
authority rather than the mutable sources inherited from the base image.
Private build-helper generations
Local qualification and hosted qualification use one materialization path.
Whenever an exact input-keyed helper is absent locally, its owning materializer
first tries the platform-scoped generation under
ghcr.io/google/capsem-ci. It validates the pulled image's platform, input-key
label, and org.opencontainers.image.source label before retagging it locally.
An unavailable, unauthenticated, or invalid remote generation falls through to
the same deterministic network-open local build; there is no cache-only build
path and no opt-in environment switch.
The private inventory contains the host builder, Linux parity base, install
builder, both package builders, both guest Rust builders, the host asset-tools
builder, and the host architecture's kernel and rootfs dependency builders.
capsem-gate private-images --publish materializes that inventory before it
publishes immutable generation tags. .github/workflows/private-images.yaml
runs it weekly and on defining-input changes, independently of the release and
official OCI image workflows, and retains the latest three tagged generations
per helper and platform.
Developers who can read the repository's private packages may authenticate
Docker to GHCR for warm local qualification. Missing credentials only select
the local fallback; they do not change the resulting helper identity. Keep all
helper Dockerfiles under build_system/docker/ and dependency templates under
config/docker/; add any new network-open helper to the single inventory and
the repository-source-label guard.
Keep the verified registry alias when retagging an imported helper. BuildKit can fetch inherited lazy layers from that origin; a local-only FROM name can redirect those fetches to Docker Hub. Resolve the registry digest through the shared Docker boundary and retain exact image-ID/platform checks. Cache retirement removes all owned aliases and accounts for the image bytes once. Every child build includes the Linux parity base and source lane: use the shared exact-reference check before building and verify the identity afterward. Without a qualified registry origin, keep the verified local tag even if Docker records local RepoDigests. BuildKit can resolve those digests through Docker Hub instead of the local image store, breaking sealed source builds.
Templates use Jinja2 with variables from the admin-materialized image workspace. Do not add a second preview rail for product truth; if a build input needs validation, add it to the normal admin validation path.
Every architecture's base_image is required to be its immutable
repository@sha256:<child-manifest> identity. Do not use a mutable tag or the
multi-platform index. capsem-gate materializes a missing exact child through
the Docker daemon before the cross-execution probe and build lanes; keep this
on the ordinary guarded Docker runner, not the host-process egress broker.
Builder Internals (for modifying the builder itself)
Architecture: config/docker/image -> admin workspace -> Pydantic -> context dict -> Jinja2 -> Dockerfile
- Runtime source (
config/docker/image/andguest/artifacts/). - capsem-admin copies both into a backend build workspace.
- Pydantic models (
build_system/builder/image/models.py) parse that workspace. - Context dict (
build_system/builder/image/docker.py) feeds Jinja2 templates. - Jinja2 templates (
config/docker/) produce Dockerfiles.
Key files
| File | Role |
build_system/builder/image/models.py | All Pydantic models (enums, configs, top-level GuestImageConfig) |
build_system/builder/image/config.py | Backend loader for admin-materialized build workspaces |
build_system/builder/image/docker.py | Context builders (_rootfs_context, _kernel_context), rendering, build execution |
build_system/builder/image/image_build_backend.py | Private admin-invoked image build backend; not a public CLI |
crates/capsem-admin/src/image_build.rs | image build / image workspace: workspace materialization and the build plan |
config/docker/Dockerfile.rootfs.j2 | Rootfs Dockerfile template |
config/docker/Dockerfile.kernel.j2 | Kernel Dockerfile template |
build_system/builder/image/validate.py | Validation rules (E001-E302, W001-W012) |
build_system/builder/image/cli.py | Click CLI entry points |
Context dict (rootfs template variables)
_rootfs_context() in docker.py builds the dict passed to Dockerfile.rootfs.j2
(plus the Debian snapshot keys):
{
"arch": ArchConfig, # Per-arch settings (docker_platform, rust_target, etc.)
"arch_name": str, # "arm64" or "x86_64"
"apt_packages": list[str], # build.toml [build.rootfs].runtime_apt_packages
"guest_binaries": list[str], # GUEST_BINARIES in docker.py
}Kernel context dict
{
"arch": ArchConfig,
"arch_name": str,
"kernel_version": str, # exact checked-in release, e.g. "6.18.44"
"kernel_sha256": str, # verified before the source archive is extracted
"kernel_patches": list, # applied in order with --fuzz=0
}How to: Add a new guest binary
Guest binaries are compiled from crates/capsem-agent/. Every architecture on
every host goes through container_compile_agent(); native Linux has no second
ambient Cargo/rustup rail. The asset preflight first materializes the host
platform's config-selected exact Rust child image plus the checked-in Rust
toolchain and Cargo.lock; a foreign target also materializes its exact
config-pinned C compiler package and both the foreign-target and host-target
Cargo dependency sets. The host set covers target-gated build-script and
proc-macro dependencies that cargo fetch --target <foreign> does not select.
The actual container build runs with
--network none and Cargo --locked --offline.
Materialize only helpers the command can consume: every requested architecture on either supported host, and none for a kernel-only build. Immutable Debian guest bases remain architecture-selected separately.
- Add the binary target in
crates/capsem-agent/Cargo.toml - Add the binary name to
GUEST_BINARIESlist indocker.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 Linux .deb package. Useful for catching system
dependency issues before CI.
just _cross-compile # Build for host arch (arm64 on Apple Silicon)
just _cross-compile x86_64 # Build x86_64 debBuild pipeline (what build_image() does)
For rootfs:
- Build guest agent binaries (
cross_compile_agent-- every target uses the pre-materialized, network-denied Rust builder; a foreign target cross-compiles on the host CPU) - 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 EROFS from tar (
create_erofs-- runs mkfs.erofs in a container) - Clean up container image
For kernel:
- Read the exact kernel version and SHA-256 from the checked-in build config
- Assemble build context (defconfig, capsem-init)
- Render Dockerfile from template
docker build, verifying the downloaded source archive before extraction- Extract vmlinuz + initrd.img from image
- Clean up
The guest Rust builder workspace: do not widen the /src/* glob
container_compile_agent mounts the checkout read-only at /src and assembles
a writable workspace at /build:
for f in /src/*; do b=$(basename "$f"); \
[ "$b" != target ] && [ "$b" != crates ] && ln -s "$f" /build/; done/src/* does not match dotfiles. That exclusion is load-bearing. Widening it
breaks the build. It reads like an oversight -- .cargo/config.toml never
reaches the container, so the checked-in Cargo configuration is never applied --
and it has been "fixed" on exactly that reasoning.
Why it must stay: .cargo/config.toml declares
[target.x86_64-unknown-linux-musl]
linker = "rust-lld"On a developer host, x86_64-unknown-linux-musl is a cross target and
rust-lld is correct. Inside the Alpine builder that same triple is the host
target, so inheriting the file makes every proc-macro crate -- serde_derive,
tokio-macros -- link its host .so with rust-lld:
rust-lld: error: unable to find library -lgcc_s
rust-lld: error: unable to find library -lc
error: could not compile `tokio-macros`The rule this generalizes to: checked-in Cargo configuration is
developer-host configuration. The builder container owns its own toolchain
settings and receives them as environment on the docker run, never by reading
them out of the tree. If a container build needs a linker or a CC, pass it
explicitly.
build_system/tests/gate/test_guest_rust_builder_hermetic.py::test_container_workspace_excludes_dotfiles
fails if the glob is widened, so this cannot be rediscovered the slow way.
Cross-compiling guest binaries instead of emulating
Foreign-target guest builds used to run rustc under QEMU on the target's own
platform child. Measured on a 16-core Linux host, cold, --locked --offline,
for the six aarch64 guest binaries:
emulated (--platform linux/arm64, qemu-aarch64) | 1194.7s |
| cross-compiled from the amd64 base | 86s |
A release run compiles that graph for every architecture's rootfs and again
for assets.pack-initrds, so emulation costs tens of minutes per run.
What a cross image needs, materialized at image-build time on the same
network-open setup edge that cargo fetch --locked already uses:
apk add --no-cache ${CROSS_PACKAGES}, where config owns the exact package tuple (currentlyclang21=21.1.2-r2) and the helper identity includes it.ringis the only crate in thecapsem-agent+capsem-benchgraph that compiles C -- nothing else pullscc,cmakeorbindgen. Alpine's clang cross-compiles it for a foreign musl target with no external sysroot. The pinned Rust toolchain already suppliesrust-lld; no separate ambient linker package is installed.rustup target add "${RUST_TARGET}", after which the existingrustup target list --installedassertion proves it landed.CC_<target>=clangandCFLAGS_<target>=--target=<target>on the run.CARGO_TARGET_<TARGET>_LINKER=rust-lldon the run -- not by linking.cargo/config.tomlinto the workspace, for the reason in the section above.
The runtime build stays --network none with --locked --offline. The image
tag is keyed by the base, target, cross-package tuple, Dockerfile and lockfiles,
so a change to any of them is a different image rather than a silent reuse.
Container runtime requirements
On macOS, Docker runs inside a Colima VM with limited resources. The rootfs build runs apt and the guest Rust builds concurrently -- the default RAM allocation may cause OOM kills (exit code 137).
Minimum: 12GB RAM. Recommended: 16GB RAM, 8 CPUs.
# Colima (macOS)
colima stop && colima start --vm-type vz --vz-rosetta --memory 16 --cpu 8
# Linux: Docker runs natively, no memory tuning needed
# sudo apt install docker.iojust doctor owns the product readiness gate. capsem-builder doctor is a
backend helper used by the build rail to check container/runtime prerequisites.
The resource check lives in build_system/builder/image/doctor.py:
check_container_resources()-- checks docker info- Thresholds:
DOCKER_MIN_MEMORY_MB = 4096,DOCKER_RECOMMENDED_MEMORY_MB = 8192
Container image compatibility
Guest cross-build containers use the exact per-platform
rust:1.97.1-alpine3.23 child manifests in
config/docker/image/build.toml, never a mutable Rust tag. Those children
already own the exact toolchain, native musl target, musl headers, and compiler.
At the guarded asset-prefetch boundary,
build_system/docker/Dockerfile.guest-rust-builder resolves the Cargo.lock graph and, only
for a foreign target, adds the exact config-pinned C compiler package and Rust
target. Do not add package installation, target installation, index updates, or
downloads to container_compile_agent(); its runtime network is deliberately
none.
The local helper tag is an input cache key, not an OCI content digest. Two cold
Docker builds may have different image IDs because registry/index and layer
metadata are materialization outputs. Cargo verifies every registry package
against Cargo.lock, and the nightly/release qualification boundary is the
specific helper image materialized by that run: after this one guarded fetch
edge, the binary build is locked, offline, and network-denied. Do not claim
byte-for-byte reproducible helper images unless every remaining registry and
layer byte is independently pinned.
The selected image is a minimal Alpine image and has no Bash. Many common
utilities (file, less, vim, etc.) are NOT available. Runtime shell
commands must be POSIX sh and use only the BusyBox tools already present in
the materialized image.
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 asset dependency-helper apt-get update calls use
-o Acquire::Check-Valid-Until=false -o Acquire::Check-Date=false against the
config-owned HTTPS Debian snapshot 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-dependencies.j2Dockerfile.rootfs-dependencies.j2
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

