Image Labels & Recipe Hash
The Docker image labels jackin❯ stamps on derived role images, what each means, and how the recipe hash that drives warm reuse is calculated
This page documents the label schema jackin❯ stamps on local derived role images and how the recipe hash that decides warm reuse is computed. It is the source of truth for contributors; keep it in sync with crates/jackin-runtime/src/runtime/image.rs and crates/jackin-runtime/src/runtime/naming.rs.
The derived image is always a thin jackin❯ overlay (capsule, agents, normalization) built FROM a local role base image jk_<role>__base:<sha>. The overlay never depends on the published image's mutable :latest tag — jackin❯ always materializes an immutable local base first, from one of two sources:
- Fresh published image → pull
docker.io/.../jackin-the-architect:latest, verify its Docker labels prove it was built from the current role commit, and tag it as the localjk_<role>__base:<sha>(no role rebuild, no restamp Docker build). The overlay then derivesFROMthat local tag. - Published image missing/outdated, or a custom construct is in play → build the base locally from the role Dockerfile (construct
FROMoverridden byJACKIN_CONSTRUCT_IMAGEwhen set), taggedjk_<role>__base:<sha>.
Either way the local base is reused when the tag already exists locally and its labels prove the current role commit plus construct identity — so warm launches skip the published pull entirely, and the heavy role layers are materialized once per (role commit, construct) (see Local role base).
The local derived image jk_<role>:<sha> carries the full label set below. All keys use dotted namespacing (jackin.<group>.<name>). The role-commit SHA is stored in its short, 7-character form (e.g. 4f38b4f), matching the image tag.
The reuse authority: jackin.image.recipe.hash
jackin.image.recipe.hash is the single value that decides reuse-vs-rebuild. It is the SHA-256 of the JSON-serialized ImageRecipe struct:
fn hash(&self) -> Result<String> {
let bytes = serde_json::to_vec(self)?; // serialize the whole ImageRecipe
Ok(sha256_hex(&bytes)) // → 64-char hex
}ImageRecipe contains every input that changes derived-image content:
| Recipe field | Captures |
|---|---|
version | recipe schema version (the jackin.image.recipe.version gate) |
manifest_version | jackin.role.toml schema version (e.g. v1alpha4) |
role_git_sha | short role-repo commit SHA |
role_source_ref | branch/ref |
base_image | published-base override, if any |
construct_image | which construct (custom vs official) |
generated_runtime_hash | SHA-256 of the generated derived Dockerfile (the overlay shape) |
supported_agents | the supported-agent set (sorted → canonical, so reorder ≠ rebuild) |
cache_bust | forced-rebuild value (--rebuild) |
capsule_version | jackin-capsule version baked in |
hooks_hash | SHA-256 of role hook files |
plugin_recipe_hash | SHA-256 of Claude marketplace/plugin declarations |
host_identity_strategy | UID/identity strategy |
host_uid | host UID baked into /home/agent ownership |
Agent CLI binary versions are not an ImageRecipe input: the current baked binary versions are stamped as diagnostic labels, while recipe reuse is governed by the supported-agent set and overlay shape. Claude plugin declarations are recipe input because plugins are baked into the derived image. The host UID is recipe input because /home/agent is chowned to that UID during the derived image build; reusing an image built for another UID would break owner-only syscalls such as chmod(2).
At launch jackin❯ recomputes the expected recipe from current inputs, hashes it, and compares to the stored label. Match → reuse (skip docker build, runtime-binary prep, token lookup, and the version probe). Mismatch → rebuild.
Because every recipe field is folded into this hash, a change to any of them invalidates the image even when the field has no standalone label of its own (see Dropped labels).
Schema gate: jackin.image.recipe.version
A short constant (currently v8) checked before the hash. The hash is only comparable if both sides used the same recipe schema; if a jackin❯ upgrade changes the ImageRecipe shape or label set, the version is bumped so old images fail the gate (RecipeVersionChanged) and rebuild rather than mis-comparing hashes. Bumping it invalidates all prior images at once.
Identity & version labels (local derived image)
Stamped and, where noted, checked for a precise invalidation reason:
| Label | Meaning | Precise reason on mismatch |
|---|---|---|
jackin.role.git.sha | short role-repo commit SHA | RoleGitShaChanged |
jackin.manifest.version | jackin.role.toml schema version | ManifestVersionChanged |
jackin.construct.image | construct image used (custom vs official) | ConstructImageChanged |
jackin.capsule.version | jackin-capsule version baked in | CapsuleVersionChanged |
Per-agent jackin.agent.<slug>.version labels record the baked binary versions for diagnostics. They are not precise invalidation labels; recipe validity still flows through jackin.image.recipe.hash.
Published-contract labels
Every published role image must carry exactly these two (written by role CI — see Publishing Role Images):
| Label | Meaning |
|---|---|
jackin.role.git.sha | short role-repo commit SHA the image was built from |
jackin.construct.version | construct version tag from the role Dockerfile FROM line |
jackin❯ pulls the published base and verifies jackin.role.git.sha against the role checkout's HEAD to decide whether to tag the published image as the local base, or build the base from the workspace Dockerfile. Role publishing workflows should get these labels from jackin-role publish-labels --role-git-sha <sha> so the publish path and runtime freshness check share one contract.
Local role base
The role base is always materialized locally as jk_<role>__base:<sha> — the role content with no jackin❯ overlay — so the derived overlay builds FROM a stable local tag rather than the published :latest. The base is either a local tag of a pulled, label-verified published image or a local build of the role Dockerfile (construct FROM overridden when JACKIN_CONSTRUCT_IMAGE is set). Locally built bases carry two labels:
| Label | Meaning |
|---|---|
jackin.role.git.sha | short role-repo commit SHA |
jackin.construct.image | the construct the base was built on (custom vs official) |
Tagged published bases keep the published image labels instead; reuse requires jackin.role.git.sha matching the current role HEAD, and any construct label present must still match (jackin.construct.image for local builds, jackin.construct.version for published images). So warm launches skip the published pull, and the heavy role layers are materialized once per role commit plus any declared construct identity; the overlay derives FROM the base.
Container / network labels (GC & filtering, not image recipe)
| Label | Purpose |
|---|---|
jackin.managed=true | marks every jackin-managed container/network/volume; GC filter |
jackin.kind=role|dind|prewarm-dind | resource type |
jackin.role | role key on sidecars/networks → GC maps them back to the role |
jackin.image | derived image name on the role container → image GC skips in-use images |
jackin.prewarm=true | prewarmed (unattached) resource |
jackin.keep.awake=true | workspace opted into the caffeinate reconciler |
jackin.display.name | human role name on the container, read by discovery |
Dropped labels
The following opaque jackin.recipe.* component labels were removed: role_source_ref, base_image, generated_runtime_hash, supported_agents, cache_bust, hooks_hash, host_identity_strategy. Their values still live inside ImageRecipe, so they still invalidate the image via jackin.image.recipe.hash — a mismatch simply surfaces as the generic RecipeHashChanged reason instead of a component-specific one. The single jackin.selected_agent_version label was dropped because the derived image is agent-independent; per-agent jackin.agent.<slug>.version labels remain diagnostic-only. The old standalone claude_plugin_hash label is gone because Claude plugin declarations now invalidate through plugin_recipe_hash inside the master recipe hash.