skills: a gate that could pass without checking, and a mailbox that never empties
Fixes the three blockers and seven should-fixes from pi@emb-7kj4vr4g's review (logstream correlation skills-provenance-review, full text in drawer_pi-devbox_reviews_e43e766641c9ec85217bc6ce). Every finding was reproduced by execution here before being fixed; two were refined by that reproduction rather than taken as given. BLOCKER 1 — the provenance gate could print OK and exit 0 without verifying. `git show <ref>:<path> | sha256sum` hashes EMPTY STDIN when the ref does not resolve, so at_ref was never empty and the UNKNOWN branch was dead code. Measured: a bogus ref reported MISMATCH — accusing the snapshot of lying when the real cause was an incomplete clone, and the operator's natural remedy for MISMATCH is to re-run the refresh, which rewrites provenance to silence the complaint; and with a 0-byte snapshot against a 0-byte upstream file it printed "OK: exactly skillset@aaaaaaa" with exit 0 for a ref that does not exist. The script already had the sha_empty idiom and had applied it to blob_sha but not to at_ref. Existence is now PROVEN with git cat-file -e before anything is hashed, at two levels (ref resolves / path exists at it) because those deserve different messages. Same defect class as the canary it replaces: a check that can succeed without checking. A second, unflagged instance of the same pipeline shape in blob_sha was found and fixed too. Exit codes split, because the old contract failed the sanctioned case: 0 truthful (including stale, with a NOTICE), 1 a lying record only, 2 cannot determine. AGENTS.md step 2 promised "the message distinguishes the two" and was the thing this branch was breaking; rewritten to state all three. BLOCKER 3 — VENDORED.md contradicted itself in the release whose stated invariant is non-contradiction: its hand-maintained provenance line named skillset 670f7f1, seven commits behind the ARG and itself the commit that told agents to hand-stamp added_by — the withdrawn instruction this work exists to stop shipping — while its cp recipe contradicted the "not cp" rule 20 lines above. Line removed (nothing forced it to move when the ARGs did); 670f7f1 kept only as a labelled cautionary example. The pi-extensions half was verified redundant (CI require_sha resolves PI_EXTENSIONS_REF) before removal. SHOULD-FIXES: `<root> --check`, the spelling VENDORED.md documented, silently ran a REFRESH because only $1 was parsed (both tools now parse all args and reject unknown ones); refresh at a detached/older HEAD silently rewound ref and bytes (now refused unless the recorded ref is an ancestor, --force to override); upstream_dirty was computed and never used in check mode; --no-skills --json printed human text and broke jq; --help was a hardcoded sed range this branch had already made stale; the fingerprint hashed SKILL.md alone so a live skill differing only in a sibling file reported "identical", and pi-extensions already ships two files, so it is now a per-skill TREE hash with the manifest field renamed skillset_snapshot_tree_sha256; the --no-skills smoke assertion was negative-only and passed on a crashed binary. mktemp+mv left files 0600 — CI was unaffected since the index records 100644, so the blast radius was local builds only, narrower than the review inferred. Snapshot resynced c04cd15 -> 5fd0d5c so the no-clone fallback carries the CORRECTED coordination protocol rather than the withdrawn one; --check is now OK with no staleness notice, and the bidirectional canary re-verified against the new bytes. Local validation is bash -n only (shellcheck, hadolint and actionlint are all absent in this container) — CI remains the shellcheck gate.
This commit is contained in:
+16
-7
@@ -480,18 +480,23 @@ run_expect "pi-devbox-version --quiet is a compact one-liner" \
|
||||
run "manifest records the vendored skill snapshot provenance" '
|
||||
j=/etc/pi-devbox/build-manifest.json
|
||||
r=$(jq -r ".skillset_snapshot_ref // empty" $j)
|
||||
s=$(jq -r ".skillset_snapshot_sha256 // empty" $j)
|
||||
echo "ref=[$r] sha256=[$s]" >&2
|
||||
s=$(jq -r ".skillset_snapshot_tree_sha256 // empty" $j)
|
||||
echo "ref=[$r] tree_sha256=[$s]" >&2
|
||||
printf "%s" "$r" | grep -qxE "[0-9a-f]{40}" \
|
||||
|| { echo "skillset_snapshot_ref is not a 40-hex commit" >&2; exit 1; }
|
||||
printf "%s" "$s" | grep -qxE "[0-9a-f]{64}" \
|
||||
|| { echo "skillset_snapshot_sha256 is not a 64-hex digest" >&2; exit 1; }
|
||||
|| { echo "skillset_snapshot_tree_sha256 is not a 64-hex digest" >&2; exit 1; }
|
||||
'
|
||||
# Recomputes over the whole DIRECTORY with the same tree_sha256() pipeline
|
||||
# Dockerfile.variant used to measure it, not a plain `sha256sum SKILL.md` —
|
||||
# a file-only compare here would pass even if the manifest recorded a
|
||||
# fingerprint over a directory that has since grown a second file (this is
|
||||
# not hypothetical: pi-extensions already ships two files for its skill).
|
||||
run "manifest skill fingerprint matches the baked snapshot" '
|
||||
j=/etc/pi-devbox/build-manifest.json
|
||||
f=/usr/local/share/pi-devbox/skills/mempalace/SKILL.md
|
||||
m=$(jq -r ".skillset_snapshot_sha256 // empty" $j)
|
||||
a=$(sha256sum "$f" | cut -d" " -f1)
|
||||
d=/usr/local/share/pi-devbox/skills/mempalace
|
||||
m=$(jq -r ".skillset_snapshot_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" ]
|
||||
'
|
||||
@@ -615,8 +620,12 @@ exec_test "pi-devbox-version reports skill sources (all baked, no skillset here)
|
||||
# version FIRST, before the baked links exist and long before the skillset
|
||||
# deploy + reconcile run last, so anything it said about skill sources would be
|
||||
# a pre-reconcile state that is about to change.
|
||||
# A bare negative (`! grep -q "skills:"`) passes if the tool crashes or
|
||||
# prints nothing at all — it cannot tell "correctly omitted the section"
|
||||
# apart from "the binary is broken". Anchor it positively: the command must
|
||||
# still succeed and still print its normal release-tag line.
|
||||
exec_test "pi-devbox-version --no-skills omits the skills section" \
|
||||
'! pi-devbox-version --no-skills | grep -q "skills:"'
|
||||
'out=$(pi-devbox-version --no-skills) && echo "$out" | grep -q "^pi-devbox " && ! echo "$out" | grep -q "skills:"'
|
||||
exec_test "entrypoint prints the version banner with --no-skills" \
|
||||
'grep -q "pi-devbox-version --no-skills" /usr/local/bin/entrypoint-user.sh'
|
||||
# The handover path itself. CI never mounts a skillset, so without this the
|
||||
|
||||
Reference in New Issue
Block a user