mcpbeat

Asset Pipeline

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.

4k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
67
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/google/capsem --skill asset-pipeline

The instruction itself

11 sections, as written by the author

Asset Pipeline

How VM assets (kernel, initrd, rootfs) are built, checksummed, resolved, and verified at boot.

Manifest Authority

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.

Versioning

Binary and asset versions are independent:

  • Binary: 1.3.{unix_timestamp} on the current release line -- changes every build
  • Assets: YYYY.MMDD.patch -- changes only on kernel/rootfs/initrd rebuilds

The 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.

Key Commands

| 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.

File Locations

| 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 |

Manifest Format (v2)

{
  "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 for

each channel.

  • Each manifest record has one status enum value: 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.

  • Each manifest record carries SHA-256 and BLAKE3 digests for the selected

manifest JSON. Do not publish HMAC fields.

  • Each manifest keeps package artifacts separate from per-binary inventory.

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.

  • Profiles own profile images, config files, software inventory, ABOM/OBOM

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.

Release-channel Cloudflare prerequisites

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.

Disk Layouts

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.

Boot-Time Resolution

  • Dev mode: Service detects arch subdirs, passes --kernel assets/{arch}/vmlinuz etc. to capsem-process
  • Installed mode: Service reads v2 manifest, resolves ManifestV2::resolve(binary_version, arch, base_dir) to get hash-based file paths, passes --kernel, --initrd, --rootfs individually to capsem-process
  • Hash check at boot: VmConfig::builder().build() verifies BLAKE3 against compile-time hashes if available

Cleanup

cleanup_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.

Common Issues

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.

How to use it

Copy the folder

Take google/asset-pipeline from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

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.