google/asset-pipeline
Asset building, manifest format, hash verification, and boot-time resolution for Capsem VM images. Use when debugging boot failures, manifest issues, hash mismatches, or understanding how assets flow from build to boot.
npx skills add https://github.com/google/capsem --skill asset-pipeline
How VM assets (kernel, initrd, rootfs) are built, checksummed, resolved, and verified at boot.
The selected manifest is the bible: if an artifact is not in that manifest, it
does not exist for the release, update, cache, or boot path. Never infer
membership from a cache directory, release attachment, filename, channel name,
or prior run. Fetch mutable manifests fresh. Cache only immutable artifact
bytes, address each cache entry directly by the digest recorded in the
manifest, and re-verify that digest before every use. Artifact cache identity
is channel-independent; the manifest decides which digest set belongs to a
channel/profile at that moment.
Binary and asset versions are independent:
1.3.{unix_timestamp} on the current release line -- changes every buildYYYY.MMDD.patch -- changes only on kernel/rootfs/initrd rebuildsThe manifest tracks both with compatibility ranges (min_binary, min_assets).
Runtime asset selection enforces both directions: older binaries do not hydrate
asset releases whose min_binary requires a newer binary, and new
session/download selection skips releases marked deprecated: true.
| Command | When to use |
|---------|-------------|
| just _build-assets | Full rebuild: kernel + rootfs + checksums (slow, needs docker) |
| just shell | Daily driver: repack initrd, build, sign, boot (~10s) |
| just shell "capsem-doctor" | Verify VM boots correctly after changes |
On macOS, just _build-assets, just _pack-initrd, and any Docker-backed
asset recipe depend on Colima. If Docker cannot connect but Colima appears to
be running, follow /dev-setup's Colima recovery discipline before treating
the asset build as blocked: check colima list, docker version, and
colima ssh -- docker ps; then try colima stop && colima start once and
rerun the failing recipe.
| What | Where |
|------|-------|
| Profile source config | config/profiles/<id>/ |
| Guest artifacts | guest/artifacts/ |
| Built assets (dev) | assets/{arch}/vmlinuz, initrd.img, rootfs.erofs |
| Installed assets | ~/.capsem/assets/{name}-{hash16}.{ext} (flat, hash-based) |
| Manifest | assets/manifest.json |
| Asset channel deploy root | target/release-channel/ |
| Asset channel manifest | target/release-channel/assets/<channel>/manifest.json |
| Asset channel human site | release-site/ Astro app, built from target/release-channel/ JSON |
| Checksums | assets/B3SUMS |
| Manifest generator | capsem-admin manifest generate <assets_dir> |
| Asset types + cleanup | crates/capsem-core/src/asset_manager.rs |
| Hash extraction for build.rs | crates/capsem-core/src/manifest_compat.rs |
{
"format": 2,
"assets": {
"current": "2026.0415.1",
"releases": {
"2026.0415.1": {
"date": "2026-04-15",
"deprecated": false,
"min_binary": "1.0.0",
"arches": {
"arm64": {
"vmlinuz": { "hash": "<64-char blake3>", "sha256": "<64-char sha256>", "size": 7797248 },
"initrd.img": { "hash": "...", "sha256": "...", "size": 2270154 },
"rootfs.erofs": { "hash": "...", "sha256": "...", "size": 454230016 }
}
}
}
}
},
"binaries": {
"current": "1.0.1776269479",
"releases": {
"1.0.1776269479": {
"date": "2026-04-15",
"deprecated": false,
"min_assets": "2026.0415.1"
}
}
}
}
The public producer is capsem-admin manifest generate <assets_dir>. Full
asset builds and initrd repacks feed that same profile-derived build rail so local, CI, and
corporate manifests use one contract. Corporate VM asset channels use
capsem update --assets --manifest <URL>; --manifest is URL-shaped, so local
custom manifests use file:///absolute/path/to/manifest.json, while hosted corp
channels use https://... or http://.... Do not use capsem update --corp
for asset channels: --corp provisions corporate policy config, while
corporate VM asset channels stay on the shared manifest/update path.
Digest ownership starts at that asset build/ingest boundary. Stream each asset
once there and persist both BLAKE3 identity and SHA-256 compatibility evidence
in its manifest entry. Release-channel assembly trusts complete recorded
digests for remote immutable blobs; it must not reopen the same rootfs merely
to render stable and nightly graphs. A legacy current entry missing SHA-256 may
be hydrated once from its matching current file, but historical releases must
never be compared with the flat current assets/<arch>/<logical-name> path.
When local channel output copies blobs, compute and validate both digests in
the copy stream and reuse that result for graph rendering. Digest enrichment
alone does not mint a new asset version; only BLAKE3/size identity changes do.
The public asset channel is generated from that manifest with
capsem-admin assets channel build. Do not invent a separate release-channel
source tree or alternate manifest format. The generated deploy root is
target/release-channel/; the machine artifact is
assets/<channel>/manifest.json under that root, so the stable public URL is
https://release.capsem.org/assets/stable/manifest.json.
capsem-admin writes the machine channel artifacts only: root channels.json,
per-channel manifest JSON, profile-owned image/config/evidence files,
_headers, and robots.txt. The human release pages are built by the
release-site/ Astro
app from those JSON files with
`CAPSEM_RELEASE_GRAPH=/path/to/target/release-channel CAPSEM_RELEASE_CHANNEL_DIST=/path/to/target/release-channel pnpm run
build:channel`, which overlays the root channel list, per-channel pages, and
per-profile pages into the same deploy root before channel validation or
deployment.
The graph hierarchy is strict:
channels.json lists all channels and all versioned manifest records foreach channel.
current, supported,deprecated, or revoked. Revoked records remain auditable but runtime
selection never chooses them. A record that is no longer served is simply
absent.
manifest JSON. Do not publish HMAC fields.
Packages are delivery containers; binaries are the executable files inside
those packages and must carry SHA-256, BLAKE3, version, package
provenance, and SBOM component reference.
evidence, and min_capsem_version. Profiles never advertise the selected
Capsem binary; they only declare the minimum Capsem version needed to use
that profile.
Immutable profile image blobs are referenced by instantiated URLs in the
selected channel manifest. Public releases may store large blobs in GitHub
Releases, but the release graph must publish concrete URLs for each profile
image artifact and evidence file. When a local or corporate manifest is used,
the same update mechanism applies: --manifest must be a URL, with
file:///absolute/path/to/manifest.json for local fixtures and https://...
or http://... for hosted corporate channels.
The root channel catalog makes stable/nightly switching a manifest URL choice.
Stable can point at https://release.capsem.org/assets/stable/manifest.json
while nightly points at https://release.capsem.org/assets/nightly/manifest.json.
Package postinstall and glow-up tests must use those URL-shaped inputs directly;
do not add package-time manifest converters or compatibility adapters for old
manifest shapes.
Updating the co-work nightly profile image/config must change only the nightly
channel/profile records and matching digests; stable, packages, per-binary
inventory, and other profiles must stay byte-for-byte unchanged. Use
min_capsem_version on a profile only when profile behavior requires a newer
client.
Profile publication is owned by:
just release-profile <channel> <profile>
That command calls capsem-admin release. The shared
capsem-release-<channel> lock is acquired before the source manifest is read.
The profile workflow then resolves the existing package by recorded digest,
builds exactly the selected channel/profile for arm64 and x86_64, validates the
pairing, and mutates only that profile entry. It never builds a package and
never edits another profile or channel.
Profile config, images, software inventory, OBOM, evidence, and revision are
published under an immutable identity containing channel and profile identity.
This prevents the same profile/revision label in stable and nightly from
aliasing or overwriting bytes.
When min_capsem_version is newer than the public package, the immutable
profile publication is staged but not deployed. The following
just release-binaries <channel> resolves those exact staged digests, builds
packages only, runs the complete functional/native/glow-up proof, and activates
the completed pairing. The profile bytes are not rebuilt.
The selected channel source manifest is the sole mutable authority. SBOM,
OBOM, existing attestations, and GitHub logs are the evidence; do not add a
parallel result or provenance file. Corporate manifest/profile authoring also
goes through capsem-admin; corporations do not build Capsem binaries.
The deploy workflow runs scripts/check-release-site-contract.py against
https://release.capsem.org after Cloudflare publishes the generated site. That
Python validator reuses the remote release readiness contract and must validate
the root channel catalog, selected manifest, profile-owned
image/config/evidence files, package metadata, per-binary metadata,
BLAKE3/SHA-256 content, attestation references, and cache headers rather than
only checking that files exist. The deploy smoke rejects stale public HTML: the
root and channel pages must show the same generated timestamp, manifest URL,
manifest version, package inventory, per-binary inventory, profile revision,
image artifact URLs, and evidence URLs as the fetched JSON
graph. It validates host SBOM and VM OBOM evidence document shape (SPDX 2.3 for
the host SBOM and CycloneDX for VM OBOMs). VM OBOM validation is provenance
validation, not only bomFormat: the document must declare
capsem:evidence:scope=exported-rootfs, contain Debian guest package purls, and
contain no cdx:osquery:category live-host inventory. It also validates
attestation scope, workflow, subjects, and predicate URLs against the published
host SBOM and VM OBOM evidence lists. VM asset attestations are incomplete unless
github_attestations_vm_assets is present and its predicate_url points at the
published VM OBOM evidence for the current asset release.
The deploy smoke must also verify public Cache-Control headers: mutable
release-channel pointers (/, /channels.json, and
/assets/<channel>/manifest.json) stay no-cache, must-revalidate, while
immutable asset and profile release artifacts stay
public, max-age=31536000, immutable.
Before running a live binary or profile channel deploy, create or verify the
Cloudflare Pages project serving release.capsem.org, attach the release.capsem.org
custom domain, and configure CLOUDFLARE_ACCOUNT_ID plus
CLOUDFLARE_API_TOKEN in GitHub Actions secrets. release-channel.yaml fails
before deploy if either secret is missing or
scripts/check-cloudflare-pages-project.py cannot see the Pages project through
the configured account/token, then runs scripts/check-release-site-contract.py
and smokes https://release.capsem.org/, /channels.json, and the channel
manifest through the public custom domain after Cloudflare publishes the
generated site. release-channel-staging.yaml proves this reusable deploy path
on a preview branch without invoking profile builders or package builders.
Asset-channel blobs are arch-prefixed (arm64-vmlinuz,
arm64-initrd.img, arm64-rootfs.erofs, arm64-obom.cdx.json,
arm64-software-inventory.json, and x86_64
equivalents). The v2 manifest keeps bare logical filenames inside each arch map.
Dev (repo assets/ dir -- logical names, per-arch subdirs):
assets/arm64/vmlinuz
assets/arm64/initrd.img
assets/arm64/rootfs.erofs
assets/manifest.json
Installed (~/.capsem/assets/ -- flat, hash-based filenames):
manifest.json
manifest-metadata.json
vmlinuz-2c0bd752db929642
initrd-e5e910e9ab38b873.img
rootfs-89eb92b83534d9d0.erofs
Native packages do not carry assets/manifest.json. They carry
manifest-metadata.json with the selected channel or corp manifest URL, and
postinstall runs capsem update --assets --manifest <URL> to write the live
installed manifest plus any missing profile image assets.
Hash-based naming: {stem}-{hash[..16]}{ext}. Same hash = same file across versions = natural dedup.
--kernel assets/{arch}/vmlinuz etc. to capsem-processManifestV2::resolve(binary_version, arch, base_dir) to get hash-based file paths, passes --kernel, --initrd, --rootfs individually to capsem-processVmConfig::builder().build() verifies BLAKE3 against compile-time hashes if availablecleanup_unused_assets(base_dir, manifest) removes hash-named files not referenced by any non-deprecated asset release. Also removes legacy v*/ directories.
Existing VM pins are preserved by the VM pinning rail; deprecation blocks new
selection rather than rewriting running VMs.
Hash mismatch at boot: Assets on disk don't match the hashes baked into the binary. Fix: just shell (repacks initrd, regenerates manifest, touches build.rs to force recompile).
Hash mismatch where expected and actual look identical: the two values differ
only by an algorithm tag. Digests reach boot in two spellings — asset manifests
carry bare hex, release-graph digests and the profile pins derived from them
carry blake3:<hex>.
VmConfigBuilder::verify_hash resolves both, in the one place that decides what
an expected hash means, and refuses a non-blake3 algorithm outright rather than
letting a sha256: pin masquerade as corruption it can never match. Do not add
a second reconciliation at a call site.
Log pins in full, never truncated. A 16-character slice renders both
spellings as plausible prefixes (blake3:de1d58193 looks like a hash), so a
truncated audit line hides exactly the mismatch it exists to catch.
Boot verifies the *booting profile's* pins. A channel carries one image set
per profile, so no channel-wide pointer can answer which hashes apply — the
caller passes expected_asset_hashes for the profile it is starting. Absent is
a hard error, not permission to boot unverified.
Hashes silently skipped: If build.rs can't extract hashes (manifest missing, wrong format), option_env!() returns None and verification is skipped.
Take google/asset-pipeline 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.