feat(manifest): record WHICH pi-extensions skill copy shipped

Closes the half deliberately left open by cac5e00's skill-floor gate, and the
more important half: "the floor is currently fresh" is a fact with a shelf
life, whereas "the image says which copy it got" keeps working.

The refresh in Dockerfile.variant is guarded by
`[ -f /opt/pi-extensions/skill/SKILL.md ]`, so a build whose clone predates the
co-located skill keeps the vendored floor and still succeeds GREEN, with nothing
in the manifest, labels or logs separating that from a normal build. Afterwards
the two are indistinguishable by inspection -- same path, same filenames, same
permissions -- which is exactly how the floor went unnoticed from 2026-07-30 to
2026-09-10.

build-manifest.json gains pi_extensions_skill_source and
pi_extensions_skill_tree_sha256, MEASURED rather than passed as build-args, per
the ground-truth rule the surrounding block already follows -- and necessarily
so, since the outcome depends on the clone's contents and no ARG could express
it. Three values, because two would force a lie: package (served bytes equal
the clone's skill/), vendored-floor (clone had no skill/ at this ref), and
divergent (both exist but differ -- e.g. the clone ships SKILL.md but not
evaluate-extension-usage.py, so the served directory is a genuine MIX). No OCI
label mirrors these deliberately: LABEL cannot take a RUN-computed value, and a
label fed from an ARG would be the claim-not-measurement being removed here.

Two smoke assertions make the record a gate: the source must be named and be
`package` -- vendored-floor FAILS rather than warns, since these images track
main where the package has shipped skill/ since fa04d20, so a fallback means
the clone did not resolve as intended -- and the tree hash is recomputed over
the served directory, because a recorded hash never recompared is a claim.

pi-devbox-version annotates the line too: "baked (package copy)" normally, or a
yellow "(FALLBACK: vendored floor)". Its existing section reports which copy is
READ at runtime; this is the one fact decided at BUILD time and unrecoverable
later. Old images degrade cleanly -- field absent, jq // empty yields nothing,
line prints plain "baked" as before (verified against this v1.8.14 manifest).

Tested by running the exact logic against this container's real layout, with
the expected value written down before each: package (served == clone),
vendored-floor (clone path absent), divergent (clone lacking the .py while the
served dir has it), and null (empty served dir) -- all four as predicted. The
five pi-devbox-version render branches likewise, including the absent-field
case. Emitted JSON validated with jq for both the populated and null forms.

Gates green: lint-shell.sh (15 files), hadolint 2.15.1, actionlint 1.7.12,
check-base-hash.sh, check-skill-floor.sh, vendor-mempalace-skill.sh --check.
This commit is contained in:
Joakim Persson
2026-09-10 20:30:16 +02:00
parent edc7659add
commit ff6fd1492a
4 changed files with 149 additions and 1 deletions
+38
View File
@@ -187,6 +187,44 @@ recorded origin was stale. Consequently no `rootfs/` bytes changed, the
smoke-test phrase canary stays valid, and this ARG alone would not have forced a
base rebuild — the node bump does that anyway.
**The silent-fallback hole is closed: the image now records WHICH `pi-extensions`
skill copy it shipped.** This was the half deliberately left open by the
`skill-floor` gate above, and it is the more important half, because "the floor is
currently fresh" is a fact with a shelf life while "the image says which copy it
got" keeps working. The refresh step in `Dockerfile.variant` is guarded by
`if [ -f /opt/pi-extensions/skill/SKILL.md ]`, so a build whose clone predates the
co-located skill kept the vendored floor and still succeeded **green**, with
nothing in the manifest, the labels or the logs distinguishing that from a normal
build. The two outcomes are indistinguishable by inspection afterwards — same
path, same filenames, same permissions — which is exactly how the floor went
unnoticed from 2026-07-30 to 2026-09-10.
`build-manifest.json` gains `pi_extensions_skill_source` and
`pi_extensions_skill_tree_sha256`, both **measured rather than passed in as
build-args**, per the ground-truth rule the rest of that block already follows —
and necessarily so here, since the outcome depends on the clone's contents and no
ARG could express it. Three values, because two would force a lie:
`package` (served bytes equal the clone's `skill/`), `vendored-floor` (the clone
had no `skill/` at this ref, so the fallback shipped), and `divergent` — both
exist but differ, e.g. the clone ships `SKILL.md` but not
`evaluate-extension-usage.py`, leaving the served directory a genuine **mix** of
package and floor. No OCI label mirrors these, deliberately: `LABEL` cannot take a
value computed in a `RUN`, and a label fed from an ARG would be precisely the
claim-not-measurement this change exists to remove.
Two `scripts/smoke-test.sh` assertions turn the record into a gate: one that the
source is named and is `package` — `vendored-floor` **fails** rather than warns,
since these images track `main` where the package has co-located `skill/` since
`fa04d20`, so a fallback means the clone did not resolve as intended — and one
that recomputes the tree hash over the served directory, because a recorded hash
that is never recompared is a claim rather than a measurement. `pi-devbox-version`
also annotates the line: `pi-extensions baked (package copy)` on the normal path,
and a yellow `(FALLBACK: vendored floor — clone had no skill/)` otherwise. Its
existing skill section reports which copy is being **read** at runtime; this is
the one fact that is decided at **build** time and cannot be recovered later.
Older images degrade cleanly — the field is absent, `jq // empty` yields nothing,
and the line prints plain `baked` exactly as before.
---
## v1.8.14 — 2026-09-08
+40
View File
@@ -473,6 +473,44 @@ RUN set -e; \
if [ -d "$_snap_dir" ] && [ -n "$(find "$_snap_dir" -type f -print -quit)" ]; then \
SKILL_SNAP="\"$(tree_sha256 "$_snap_dir")\""; \
fi; \
# ── WHICH pi-extensions skill copy actually shipped ──
# Closes the silent-fallback hole. The refresh step above is guarded by
# `[ -f /opt/pi-extensions/skill/SKILL.md ]`, so a build whose clone predates
# the co-located skill (or a fork pointing at a mirror without it) keeps the
# vendored floor and still succeeds — GREEN, with nothing anywhere recording
# that a snapshot shipped instead of the package copy. Measured 2026-09-10:
# the floor had been stale since 2026-07-30, so that fallback would have
# shipped a six-week-old skill silently. The floor is fresh now and gated by
# the skill-floor CI job, but "the fallback is currently harmless" is not the
# same as "you can tell which copy you got", and only the second survives.
#
# MEASURED, never claimed, per the ground-truth rule above: the branch
# condition is re-derived from the same test the refresh step used, and the
# served bytes are then compared against the clone. A build-arg could not
# express this at all, since the outcome depends on the clone's contents.
# package served bytes == the clone's skill/ (the normal path)
# vendored-floor the clone has no skill/ at this ref (fallback shipped)
# divergent both exist but differ — e.g. the clone ships SKILL.md but
# not evaluate-extension-usage.py, so the served directory is
# a MIX of package and floor. Worth its own value: it is the
# one state neither of the other two names honestly.
# No OCI label mirrors this, deliberately: LABEL cannot take a value computed
# in a RUN, and a label fed from an ARG would be exactly the claim-not-
# measurement this block exists to avoid.
_px_dir=/usr/local/share/pi-devbox/skills/pi-extensions; \
PIEXT_SRC='null'; PIEXT_HASH='null'; \
if [ -d "$_px_dir" ] && [ -n "$(find "$_px_dir" -type f -print -quit)" ]; then \
PIEXT_HASH="\"$(tree_sha256 "$_px_dir")\""; \
if [ -f /opt/pi-extensions/skill/SKILL.md ]; then \
if [ "$(tree_sha256 "$_px_dir")" = "$(tree_sha256 /opt/pi-extensions/skill)" ]; then \
PIEXT_SRC='"package"'; \
else \
PIEXT_SRC='"divergent"'; \
fi; \
else \
PIEXT_SRC='"vendored-floor"'; \
fi; \
fi; \
{ \
echo '{'; \
echo " \"release_tag\": \"${RELEASE_TAG}\","; \
@@ -493,6 +531,8 @@ RUN set -e; \
# vendored skill directory, not one file — see tree_sha256() above.
echo " \"skillset_snapshot_ref\": \"${SKILLSET_SNAPSHOT_REF}\","; \
echo " \"skillset_snapshot_tree_sha256\": ${SKILL_SNAP},"; \
echo " \"pi_extensions_skill_source\": ${PIEXT_SRC},"; \
echo " \"pi_extensions_skill_tree_sha256\": ${PIEXT_HASH},"; \
echo " \"components\": {"; \
echo " \"pi-toolkit\": \"$(rev /opt/pi-toolkit)\","; \
echo " \"pi-extensions\": \"$(rev /opt/pi-extensions)\","; \
+26
View File
@@ -157,6 +157,11 @@ if [ "$SHOW_SKILLS" = "yes" ] && [ -d "$BAKED_SKILLS" ] && [ -d "$SKILLS_DIR" ];
# is not hypothetical.
snap_ref=$(jq -r '.skillset_snapshot_ref // empty' "$MANIFEST")
snap_sha=$(jq -r '.skillset_snapshot_tree_sha256 // empty' "$MANIFEST")
# Which pi-extensions copy the BUILD baked. Distinct from everything else in
# this section, which reports which copy is being READ at runtime: for
# pi-extensions the baked tree is itself one of two possible copies, and that
# choice was made at build time and is not recoverable by inspection.
px_src=$(jq -r '.pi_extensions_skill_source // empty' "$MANIFEST")
# Same pipeline Dockerfile.variant uses to measure the baked directory at
# build time: relative paths in `find | sort` order, each hashed, the whole
# listing folded into one sha256. Keep the two definitions identical — they
@@ -187,7 +192,28 @@ if [ "$SHOW_SKILLS" = "yes" ] && [ -d "$BAKED_SKILLS" ] && [ -d "$SKILLS_DIR" ];
_target=$(readlink -f "$_link" 2>/dev/null || echo "$_link")
case "$_target" in
"$BAKED_SKILLS"/*|"$BAKED_SKILLS")
# "baked" alone used to be the whole story. For pi-extensions it is not:
# the baked tree holds EITHER the package copy that Dockerfile.variant
# lays over the snapshot, OR the vendored floor, when the clone had no
# skill/ at that ref. The two are indistinguishable by inspection — same
# path, same filenames, same permissions — so the build records which one
# it used and this reports it. Without this line a six-week-stale
# fallback skill looks exactly like a current one, which is precisely how
# the floor went unnoticed from 2026-07-30 to 2026-09-10.
if [ "$_name" = "pi-extensions" ] && [ -n "$px_src" ]; then
case "$px_src" in
package)
printf ' %-22s baked (package copy)\n' "$_name" ;;
vendored-floor)
printf ' %-22s baked \033[33m(FALLBACK: vendored floor — clone had no skill/)\033[0m\n' "$_name" ;;
divergent)
printf ' %-22s baked \033[33m(MIXED: part package, part floor)\033[0m\n' "$_name" ;;
*)
printf ' %-22s baked\n' "$_name" ;;
esac
else
printf ' %-22s baked\n' "$_name"
fi
continue
;;
esac
+44
View File
@@ -526,6 +526,50 @@ run "manifest skill fingerprint matches the baked snapshot" '
echo "manifest=[$m] actual=[$a]" >&2
[ -n "$m" ] && [ "$m" = "$a" ]
'
# ── Which pi-extensions skill copy shipped ──────────────────────────────
# Closes the silent-fallback hole. The refresh in Dockerfile.variant is guarded
# by `[ -f /opt/pi-extensions/skill/SKILL.md ]`, so a build whose clone predates
# the co-located skill keeps the vendored floor and still succeeds GREEN, with
# nothing recording that a snapshot shipped instead of the package copy. Measured
# 2026-09-10: the floor had been stale since 2026-07-30, so that path would have
# shipped a six-week-old skill in silence. The floor is fresh now and gated by the
# skill-floor lint job, but "the fallback is currently harmless" is a fact with a
# shelf life, whereas "the image says which copy it got" keeps working.
#
# vendored-floor FAILS here rather than merely warning: these images track main,
# where the package has co-located skill/ since fa04d20, so a fallback means the
# clone did not resolve as intended and that is a defect to investigate. A fork
# deliberately pointing at a mirror without skill/ is the one case that should
# edit this assertion — which is the honest place for that decision to surface.
run "manifest names which pi-extensions skill copy shipped" '
j=/etc/pi-devbox/build-manifest.json
s=$(jq -r ".pi_extensions_skill_source // empty" $j)
h=$(jq -r ".pi_extensions_skill_tree_sha256 // empty" $j)
echo "source=[$s] tree_sha256=[$h]" >&2
printf "%s" "$h" | grep -qxE "[0-9a-f]{64}" || {
echo "pi_extensions_skill_tree_sha256 is not a 64-hex digest" >&2; exit 1; }
case "$s" in
package) ;;
vendored-floor)
echo "FALLBACK: clone had no skill/ at this ref, so the image ships the committed floor" >&2; exit 1 ;;
divergent)
echo "MIXED: served directory is part package and part floor" >&2; exit 1 ;;
*)
echo "pi_extensions_skill_source absent or unrecognised" >&2; exit 1 ;;
esac
'
# Same shape as the mempalace fingerprint check above, and for the same reason: a
# recorded hash that is never recomputed is a claim, not a measurement.
run "recorded pi-extensions skill hash matches the served bytes" '
j=/etc/pi-devbox/build-manifest.json
d=/usr/local/share/pi-devbox/skills/pi-extensions
m=$(jq -r ".pi_extensions_skill_tree_sha256 // empty" $j)
a=$( (cd "$d" && find . -type f -print | LC_ALL=C sort | xargs -r sha256sum) | sha256sum | cut -d" " -f1)
echo "manifest=[$m] actual=[$a]" >&2
[ -n "$m" ] && [ "$m" = "$a" ]
'
# OCI labels live in the image config, not the container fs — inspect them
# from the host docker rather than via `docker run`.
LBL=$(docker inspect --format '{{ index .Config.Labels "se.jordbo.pi-devbox.pi-extensions-ref" }}' "$IMAGE" 2>/dev/null || true)