google/build-images
Building Capsem VM images from profile-owned inputs. Use when working with profile package files, Docker templates, kernel builds, rootfs builds, capsem-admin image builds, or the capsem-builder backend. Covers the profile-derived build rail, multi-arch assets, build ledgers, OBOMs, Dockerfile templates, and backend internals.
npx skills add https://github.com/google/capsem --skill build-images
Capsem image builds are profile-led.
config/profiles/<profile_id>/profile.toml is the profile ledger.files, tips, build-time hooks, and packaged guest root seed files.
capsem-admin validates profile-owned inputs and materializes the generatedbackend build workspace.
ledgers, and OBOMs only when invoked by the admin build rail. Do not add
product truth directly to the backend image-spec path.
Read config/README.md before changing this layout.
config/
settings/ UI/application preferences and generated UI schema
corp/ Corporate source contracts and rule files
docker/ Dockerfile/build templates
profiles/<profile_id>/
profile.toml Source ledger; no hash/size pins
enforcement.toml Profile enforcement rules
detection.yaml Profile Sigma detections
mcp.json Profile MCP declarations
apt-packages.txt Profile apt package input
python-requirements.txt
npm-packages.txt
build.sh Profile image build hook
tips.txt Profile guest tips
root/ Guest / seed, projected by capsem-init
target/config/ Generated runtime config with asset/file evidence
guest/artifacts/ Core guest payloads: init, doctor, diagnostics, bench
assets/ Generated VM assets
packages/ Generated native packages
The materialized backend workspace may contain generated package-set files and
profile build scripts. Treat those as implementation details, not authoring
surfaces. The workspace is never a config root and never a second profile
catalog.
capsem-admin is a tool, not a config authority. It validates, materializes,
builds, and checks the profile/corp/settings contracts; it must not grow
scaffolding commands that invent profile, MCP, AI provider, package, or rule
truth outside config/profiles, config/corp, and config/settings.
Do not add admin config roots, guest config roots, settings metadata, provider
registries, or backend-owned profile catalogs as product truth. schema
validates one contract, catalog lists materialized profile instances, and UI
metadata only helps render settings.
just _build-assets code [arch] # Profile-derived asset rebuild
just _build-kernel arm64 code # Kernel slice
just _build-rootfs arm64 code # Rootfs slice
uv run capsem-builder audit # Parse trivy/grype vulnerability output
Use admin/just recipes for all product 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 profile/image authoring.
capsem-admin image build may call private Python modules such as
capsem.builder.image_build_backend; agents must not make those modules public
CLI contracts.
Full rebuild (kernel + rootfs):
just _build-assets # Runs doctor + validate + build for host arch
Individual templates:
just _build-kernel arm64
just _build-rootfs arm64
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)
Rootfs EROFS settings are profile-derived. The approved release default
is EROFS with lz4hc compression level 12.
Each per-arch build emits build-ledger.log JSONL. The
rootfs.config_inputs record captures declared profile package inputs,
rendered rootfs package lists, profile root/build-script inputs, EROFS config,
git revision, and project version. Installed-package/component truth belongs in
the CycloneDX OBOM, not the build ledger.
Profile sibling files are ledgered source inputs, but agents must not add or
hand-edit hash or size fields in profile.toml. If editing
apt-packages.txt, python-requirements.txt, npm-packages.txt, build.sh,
rules, MCP declarations, tips, or root seed files makes
capsem-admin profile check fail, fix the source contract or the
validation/materialization rail with tests. Do not "just fix the hash" in TOML.
Generated runtime asset URLs/hashes belong in target/config after
capsem-admin profile materialize, not in checked-in source TOML. Profile
materialization must recopy descriptor files and root/ payloads from source
on every run; stale generated roots are a release blocker, not a cache.
config/profiles/code/apt-packages.txt,
python-requirements.txt, or npm-packages.txt.
just _build-assets code to rebuild the rootfs.capsem-doctor inside a booted VM.Do not edit generated Dockerfiles. Docker templates live under config/docker/.
There are no image-owned AI providers. A CLI/tool exists only if the active
profile declares the package/build hook and any required guest root seed files.
to profile-owned build.sh.
config/profiles/<profile_id>/root/ so they projectinto the VM at boot.
mcp.json when relevant.provider config.
do not add settings-owned boot secrets.
just _build-assets code and verify with capsem-doctor.build.sh is executed only while constructing the rootfs image. It is the
right place for official installer commands such as Claude, AGY, or Ollama
when they cannot be represented as apt/npm/Python package inputs. It must
install stable runtime binaries under system paths such as /usr/local/bin;
anything left only under /root can be hidden by the runtime overlay.
build.sh contractRemember this rail when touching profile image contents:
config/profiles/<profile_id>/build.sh is a profile-owned build hook.session creation.
apt-packages.txt, python-requirements.txt, or npm-packages.txt.
stable system paths.
credential injection path, or local developer repair script.
decisions, or runtime settings.
profile.toml must reference it through [files.build]; thedescriptor hash/size is refreshed by the profile-derived build rail, never by hand.
build.sh changes future rootfs assets only. Rebuild assets throughthe profile-derived just/admin-tool rail before claiming a VM contains the
change.
one-off Docker or installer path is release proof.
from the VM, and inspect the generated session evidence when the tool should
produce network, model, MCP, file, process, or credential events.
Decision rule:
apt-packages.txt.python-requirements.txt.npm-packages.txt.happen while baking the immutable rootfs: use build.sh.
build.sh.Templates live in config/docker/:
Dockerfile.rootfs.j2 -- rootfs image (apt packages, Python packages, optional npm/curl package sets, profile root/build hook, diagnostics)Dockerfile.kernel.j2 -- kernel build (defconfig, modules, vmlinuz extraction)Templates use Jinja2 with variables from the admin-materialized profile image
workspace. Do not add a second preview rail for product truth; if a build input
needs validation, add it to the normal profile/admin validation path.
The data flows through four layers:
config/profiles/<id>/profile.toml) and profile-ownedsibling files.
src/capsem/builder/models.py) parse that workspace.src/capsem/builder/docker.py) feeds Jinja2 templates.config/docker/) produce Dockerfiles.| File | Role |
|------|------|
| src/capsem/builder/models.py | All Pydantic models (enums, configs, top-level GuestImageConfig) |
| src/capsem/builder/config.py | Backend loader for admin-materialized build workspaces |
| src/capsem/builder/docker.py | Context builders (_rootfs_context, _kernel_context), rendering, build execution |
| src/capsem/builder/image_build_backend.py | Private admin-invoked image build backend; not a public CLI |
| config/docker/Dockerfile.rootfs.j2 | Rootfs Dockerfile template |
| config/docker/Dockerfile.kernel.j2 | Kernel Dockerfile template |
| src/capsem/builder/validate.py | Validation rules (E001-E302, W001-W012) |
| src/capsem/builder/cli.py | Click CLI entry points |
_rootfs_context() in docker.py builds the dict passed to Dockerfile.rootfs.j2:
{
"arch": ArchConfig, # Per-arch settings (docker_platform, rust_target, etc.)
"arch_name": str, # "arm64" or "x86_64"
"apt_packages": list[str], # Materialized from profile apt-packages.txt
"python_packages": list[str], # Materialized from profile python-requirements.txt
"python_install_cmd": str, # e.g. "uv pip install --system --break-system-packages"
"npm_packages": list[str], # Materialized from profile npm-packages.txt
"profile_root_seed": bool, # Whether profile-root/ is copied into the image
"profile_build_script": bool, # Whether profile-build.sh is executed
"npm_prefix": str, # e.g. "/opt/ai-clis"
"guest_binaries": list[str], # ["capsem-pty-agent", "capsem-net-proxy", "capsem-mcp-server"]
}
{
"arch": ArchConfig,
"arch_name": str,
"kernel_version": str, # e.g. "6.6.130"
}
apt-packages.txt, npm-packages.txt, orpython-requirements.txt) when the tool has a normal package manager.
build.sh when the vendor ships an official shellinstaller. The build hook runs during rootfs construction only.
/usr/local/bin.capsem-admin; if the rail cannot expressthe change, implement it with tests first.
just _build-assets code and verify with capsem-doctor.Ollama is intentionally installed by config/profiles/<id>/build.sh, not by a
VM one-off command. That keeps Codex, Claude, AGY, and OpenAI-compatible local
testing available in every shipped profile image that declares the hook.
config/profiles/<profile_id>/apt-packages.txt,python-requirements.txt, or npm-packages.txt.
capsem-admin.just _build-assets <profile_id>.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.
crates/capsem-agent/Cargo.tomlGUEST_BINARIES list in docker.py{% for binary in guest_binaries %} to COPY + chmod 555just _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 deb
The backend workspace is generated by capsem-admin; do not author it by
hand for product behavior. Its install inputs are package-set TOML files:
[npm]
name = "Node Packages"
manager = "npm"
install_cmd = "npm install -g --prefix /opt/ai-clis"
packages = ["@scope/package"]
Profiles own CLI/tool selection. If an installer cannot be represented as a
package set, put it in config/profiles/<profile_id>/build.sh, reference it
from [files.build] in profile.toml, refresh pins with capsem-admin, and
rebuild through the admin/just rail. Do not add a provider registry under
backend-generated image workspaces.
build_image() does)For rootfs:
cross_compile_agent -- on macOS delegates to container_compile_agent which builds inside a Linux container; on Linux compiles natively)prepare_build_context) -- copies CA cert, shell configs, diagnostics, agent binariesdocker buildcreate_erofs -- runs mkfs.erofs in a container)extract_tool_versions)For kernel:
docker buildOn 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 (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.io
just 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 src/capsem/builder/doctor.py:
check_container_resources() -- checks docker infoDOCKER_MIN_MEMORY_MB = 4096, DOCKER_RECOMMENDED_MEMORY_MB = 8192The 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.
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_erofs() functionTake google/build-images from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference pip, uv, npm, docker.
Without those the skill loads but fails at the first command.