Files
stack/packages/mosaic/framework/tools/wake/test-wake-preimage.sh
T
Jason WoltjeandClaude Fable 5 a8d3acef03
ci/woodpecker/pr/ci Pipeline is pending approval
feat(wake): #958 A11 preimage.sh — durable provenance for the operator-side preimage definition
The operator-owned bytes every observed_hash is computed FROM (the source
adapter, the watch-list) were unversioned: a byte change was attributable
only via an agent transcript, and reconcile surfaced it as N UNACCOUNTED
sources instead of one cause.

New tool preimage.sh (wake 0.6.15 -> 0.7.0):
- Derives the preimage set from the runtime env (resolved
  WAKE_DETECTOR_SOURCE_CMD file, WAKE_WATCH_LIST, WAKE_PREIMAGE_EXTRA);
  per file appends {ts,path,sha256,size,mtime,prev} to an append-only
  ledger and captures bytes content-addressed under
  $STATE_DIR/preimage/objects/<sha256> — prior bytes + change time from
  durable state alone (acceptance a).
- CREDENTIAL HARD GATE (acceptance b): byte capture is REFUSED — never
  redacted — for credential-store paths, secret-shaped content (digest's
  six scrub shapes as a content-deny), and files over
  WAKE_PREIMAGE_MAX_BYTES; the refused file still gets its
  hash/size/mtime row (captured:false) so change TIME survives.
- FIRST-CLASS CAUSE LINE (acceptance c): a change/deletion enqueues one
  class=actionable entry via the store allocator with path as its §2.1
  hard locator; detector poll-once and reconcile run the check as a
  PRE-step so the cause line lands at a LOWER observed_seq than the
  deltas/enumerations it explains.
- FAIL-LOUD (D2/#955 class): unresolvable adapter, corrupt ledger
  (refuses re-baseline), failed object/ledger write, failed enqueue are
  loud non-zero, never "no change"; in both integrations the pass exits
  non-zero but source observation still proceeds.

Installer unchanged (Gate A auto-enumerates the new file; the recording
site is the runtime tick where the env-derived set exists). Watch-list
schema untouched ([1,1]).

Tests: test-wake-preimage.sh P1-P12 (red-first verified: P3/P11/P12 fail
with the integration edits reverted); all 9 existing wake suites green.

Refs #958

Agent: PEPPER (sb-it-1-dt)

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NsKce8iZuSuRnu3gVMCBKB
2026-07-30 15:46:12 -05:00

349 lines
16 KiB
Bash
Executable File

#!/usr/bin/env bash
# test-wake-preimage.sh — RED-FIRST invariant harness for A11 (#958): durable
# provenance for the OPERATOR-SIDE preimage definition (preimage.sh).
#
# Each test asserts ONE #958 invariant and is designed to go RED if it
# regresses:
# P1 first-seen -> SILENT baseline (ledger rows, NO enqueue) (detector idiom)
# P2 change -> attributable from durable state alone: new ledger row with
# prev, PRIOR BYTES retrievable content-addressed, first-class actionable
# enqueued with a §2.1 hard locator (path) (acceptance a+c)
# P3 detector ordering: the preimage cause line is enqueued at a LOWER
# observed_seq than the per-source delta it explains (acceptance c)
# P4 credential-store PATH is refused — bytes NEVER captured, even when the
# operator lists it by error (hash/mtime row still written) (acceptance b)
# P5 secret-SHAPED content is refused — refusal, not redaction (acceptance b)
# P6 deletion of a preimage file is a tracked CHANGE (ABSENT), not an error
# P7 no-change is idempotent (no row growth, no enqueue)
# P8 unresolvable adapter command -> FAIL LOUD, never "no change" (D2/#955 class)
# P9 corrupt ledger -> REFUSE to compare/re-baseline (loud, no append)
# P10 oversized file -> bytes refused, hash still recorded (no data hoovering)
# P11 reconcile pre-step: a preimage change surfaces as a first-class line
# BEFORE the UNACCOUNTED enumerations it explains (acceptance c)
# P12 detector: preimage infra failure is LOUD (pass exits non-zero) but does
# NOT starve source observation
#
# Uses FAKE/STUB sources only (no live network). Isolated per test.
# shellcheck disable=SC2030,SC2031
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PRE="$SCRIPT_DIR/preimage.sh"
STORE="$SCRIPT_DIR/store.sh"
DET="$SCRIPT_DIR/detector.sh"
RECON="$SCRIPT_DIR/reconcile.sh"
command -v jq >/dev/null 2>&1 || {
echo "SKIP: jq not available" >&2
exit 0
}
TMP_ROOT="$(mktemp -d)"
trap 'rm -rf "$TMP_ROOT"' EXIT
FAILFILE="$TMP_ROOT/failures"
: >"$FAILFILE"
pass=0
fail_msg() {
echo " FAIL: $*" >&2
echo "x" >>"$FAILFILE"
}
ok() { pass=$((pass + 1)); }
fresh_state() {
local d="$TMP_ROOT/$1"
rm -rf "$d"
mkdir -p "$d"
printf '%s' "$d"
}
depth() { "$STORE" cursors | sed -n 's/pending_depth=//p'; }
state_dir() { printf '%s/default' "$WAKE_STATE_HOME"; }
ledger_rows() {
local f
f="$(state_dir)/preimage/preimage-ledger.jsonl"
[ -f "$f" ] && wc -l <"$f" | tr -d '[:space:]' || echo 0
}
# make_stub DIR — a source adapter reading $DIR/<kind>_<id> (same shape as the
# reconcile harness stub).
make_stub() {
local dir="$1"
cat >"$dir/adapter.sh" <<EOF
#!/usr/bin/env bash
set -u
kind="\$1"; id="\$2"; base="$dir/\${kind}_\${id}"; rc=0
[ -f "\$base.rc" ] && rc="\$(cat "\$base.rc")"
[ -f "\$base" ] && cat "\$base"
exit "\$rc"
EOF
chmod +x "$dir/adapter.sh"
}
make_wl() { # make_wl FILE — one repo source r1
cat >"$1" <<'EOF'
{ "schema_version": 1,
"repos": [ { "id": "r1" } ],
"watches": [ { "lane": "L", "sources": [ { "kind": "repo", "id": "r1" } ] } ] }
EOF
}
echo "== P1: first-seen -> SILENT baseline (rows, no enqueue) =="
(
WAKE_STATE_HOME="$(fresh_state p1)"
export WAKE_STATE_HOME
unset WAKE_AGENT
fx="$TMP_ROOT/p1fx"; mkdir -p "$fx"
make_stub "$fx"; make_wl "$fx/wl.json"
printf 'extra v1\n' >"$fx/extra.env"
export WAKE_DETECTOR_SOURCE_CMD="$fx/adapter.sh" WAKE_WATCH_LIST="$fx/wl.json" WAKE_PREIMAGE_EXTRA="$fx/extra.env"
out="$("$PRE" check --enqueue 2>&1)"; rc=$?
[ "$rc" -eq 0 ] || fail_msg "P1: baseline check must exit 0 (rc=$rc) [$out]"
[ "$(ledger_rows)" -eq 3 ] || fail_msg "P1: expected 3 baseline rows (adapter, watch-list, extra), got $(ledger_rows)"
[ "$(depth)" = "0" ] || fail_msg "P1: first-seen must NOT enqueue (depth=$(depth))"
) && ok
echo "== P2: change -> prior bytes + first-class hard-locator enqueue =="
(
WAKE_STATE_HOME="$(fresh_state p2)"
export WAKE_STATE_HOME
unset WAKE_AGENT
fx="$TMP_ROOT/p2fx"; mkdir -p "$fx"
make_stub "$fx"; make_wl "$fx/wl.json"
printf 'adapter preimage v1\n' >"$fx/extra.sh"
export WAKE_DETECTOR_SOURCE_CMD="$fx/adapter.sh" WAKE_WATCH_LIST="$fx/wl.json" WAKE_PREIMAGE_EXTRA="$fx/extra.sh"
"$PRE" check --enqueue >/dev/null 2>&1 || fail_msg "P2: baseline failed"
old_sha="$(sha256sum "$fx/extra.sh" | awk '{print $1}')"
printf 'adapter preimage v2 CHANGED\n' >"$fx/extra.sh"
new_sha="$(sha256sum "$fx/extra.sh" | awk '{print $1}')"
out="$("$PRE" check --enqueue 2>&1)"; rc=$?
[ "$rc" -eq 0 ] || fail_msg "P2: change check must exit 0 (rc=$rc) [$out]"
sd="$(state_dir)"
row="$(jq -c --arg p "$(realpath "$fx/extra.sh")" 'select(.path == $p)' "$sd/preimage/preimage-ledger.jsonl" | tail -n1)"
[ "$(jq -r '.prev // ""' <<<"$row")" = "$old_sha" ] || fail_msg "P2: new row must carry prev=<old sha> [$row]"
[ "$(jq -r '.sha256' <<<"$row")" = "$new_sha" ] || fail_msg "P2: new row sha mismatch [$row]"
# Acceptance (a): the PRIOR BYTES are retrievable from durable state alone.
[ -f "$sd/preimage/objects/$old_sha" ] || fail_msg "P2: prior bytes object missing"
[ "$(cat "$sd/preimage/objects/$old_sha")" = "adapter preimage v1" ] || fail_msg "P2: prior bytes not byte-exact"
[ -f "$sd/preimage/objects/$new_sha" ] || fail_msg "P2: current bytes object missing"
# Acceptance (c): ONE first-class actionable with a §2.1 hard locator (path).
[ "$(depth)" = "1" ] || fail_msg "P2: exactly one enqueue expected (depth=$(depth))"
ent="$(tail -n1 "$sd/pending.jsonl")"
[ "$(jq -r '.class' <<<"$ent")" = "actionable" ] || fail_msg "P2: entry class must be actionable [$ent]"
[ "$(jq -r '.locators.kind' <<<"$ent")" = "preimage" ] || fail_msg "P2: locator kind must be preimage [$ent]"
[ -n "$(jq -r '.locators.path // ""' <<<"$ent")" ] || fail_msg "P2: locator must carry path (hard locator) [$ent]"
[ "$(jq -r '.locators.observed_hash' <<<"$ent")" = "$new_sha" ] || fail_msg "P2: locator observed_hash mismatch [$ent]"
[ "$(jq -r '.locators.prev_hash' <<<"$ent")" = "$old_sha" ] || fail_msg "P2: locator prev_hash mismatch [$ent]"
) && ok
echo "== P3: detector orders the cause line BEFORE the delta it explains =="
(
WAKE_STATE_HOME="$(fresh_state p3)"
export WAKE_STATE_HOME
unset WAKE_AGENT
fx="$TMP_ROOT/p3fx"; mkdir -p "$fx"
make_stub "$fx"; make_wl "$fx/wl.json"
printf 'r1 state v1\n' >"$fx/repo_r1"
export WAKE_DETECTOR_SOURCE_CMD="$fx/adapter.sh" WAKE_WATCH_LIST="$fx/wl.json"
unset WAKE_PREIMAGE_EXTRA
"$DET" poll-once >/dev/null 2>&1 || fail_msg "P3: baseline poll failed"
[ "$(depth)" = "0" ] || fail_msg "P3: baseline poll must enqueue nothing (depth=$(depth))"
# Change the ADAPTER (preimage) and the source state in the same window.
printf '# comment: adapter changed\n' >>"$fx/adapter.sh"
printf 'r1 state v2\n' >"$fx/repo_r1"
"$DET" poll-once >/dev/null 2>&1 || fail_msg "P3: second poll failed"
sd="$(state_dir)"
pre_seq="$(jq -r 'select(.locators.kind == "preimage") | .observed_seq' "$sd/pending.jsonl" | head -n1)"
src_seq="$(jq -r 'select(.locators.kind == "repo") | .observed_seq' "$sd/pending.jsonl" | head -n1)"
[ -n "$pre_seq" ] || fail_msg "P3: no preimage cause entry enqueued"
[ -n "$src_seq" ] || fail_msg "P3: no source delta entry enqueued"
if [ -n "$pre_seq" ] && [ -n "$src_seq" ]; then
[ "$pre_seq" -lt "$src_seq" ] || fail_msg "P3: cause line must precede the delta (preimage seq=$pre_seq, source seq=$src_seq)"
fi
) && ok
echo "== P4: credential-store PATH refused — bytes never captured (acceptance b) =="
(
WAKE_STATE_HOME="$(fresh_state p4)"
export WAKE_STATE_HOME
unset WAKE_AGENT
fx="$TMP_ROOT/p4fx"; mkdir -p "$fx"
export MOSAIC_HOME="$fx/mosaic-home"
mkdir -p "$MOSAIC_HOME"
secret='cred-value-P4-do-not-capture'
printf '{"svc":{"token":"%s"}}\n' "$secret" >"$MOSAIC_HOME/credentials.json"
# Operator error: the credential store listed as a preimage extra.
export WAKE_PREIMAGE_EXTRA="$MOSAIC_HOME/credentials.json"
unset WAKE_DETECTOR_SOURCE_CMD WAKE_WATCH_LIST
out="$("$PRE" check --enqueue 2>&1)"; rc=$?
[ "$rc" -eq 0 ] || fail_msg "P4: refusal is not an infra failure (rc=$rc) [$out]"
sd="$(state_dir)"
row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")"
[ "$(jq -r '.captured' <<<"$row")" = "false" ] || fail_msg "P4: row must record captured=false [$row]"
jq -r '.refused // ""' <<<"$row" | grep -qi 'path' || fail_msg "P4: refusal must name the path deny [$row]"
# THE invariant: the secret bytes exist NOWHERE under the wake state dir.
if grep -rq "$secret" "$sd" 2>/dev/null; then
fail_msg "P4: credential bytes leaked into the wake state dir"
fi
) && ok
echo "== P5: secret-SHAPED content refused (refusal, not redaction) =="
(
WAKE_STATE_HOME="$(fresh_state p5)"
export WAKE_STATE_HOME
unset WAKE_AGENT
fx="$TMP_ROOT/p5fx"; mkdir -p "$fx"
tok='ghp_abcdefghijklmnop0123456789ABCDEF'
printf 'export MY_TOKEN=%s\n' "$tok" >"$fx/leaky.env"
export WAKE_PREIMAGE_EXTRA="$fx/leaky.env"
unset WAKE_DETECTOR_SOURCE_CMD WAKE_WATCH_LIST MOSAIC_HOME
out="$("$PRE" check --enqueue 2>&1)"; rc=$?
[ "$rc" -eq 0 ] || fail_msg "P5: refusal is not an infra failure (rc=$rc) [$out]"
sd="$(state_dir)"
row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")"
[ "$(jq -r '.captured' <<<"$row")" = "false" ] || fail_msg "P5: row must record captured=false [$row]"
jq -r '.refused // ""' <<<"$row" | grep -qi 'secret' || fail_msg "P5: refusal must name secret-shaped content [$row]"
if grep -rq "$tok" "$sd" 2>/dev/null; then
fail_msg "P5: secret-shaped bytes leaked into the wake state dir"
fi
) && ok
echo "== P6: deletion is a tracked CHANGE (ABSENT), not an error =="
(
WAKE_STATE_HOME="$(fresh_state p6)"
export WAKE_STATE_HOME
unset WAKE_AGENT
fx="$TMP_ROOT/p6fx"; mkdir -p "$fx"
printf 'to be deleted\n' >"$fx/gone.env"
export WAKE_PREIMAGE_EXTRA="$fx/gone.env"
unset WAKE_DETECTOR_SOURCE_CMD WAKE_WATCH_LIST MOSAIC_HOME
"$PRE" check --enqueue >/dev/null 2>&1 || fail_msg "P6: baseline failed"
rm -f "$fx/gone.env"
out="$("$PRE" check --enqueue 2>&1)"; rc=$?
[ "$rc" -eq 0 ] || fail_msg "P6: deletion check must exit 0 (rc=$rc) [$out]"
sd="$(state_dir)"
row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")"
[ "$(jq -r '.sha256' <<<"$row")" = "ABSENT" ] || fail_msg "P6: deletion must record sha256=ABSENT [$row]"
[ "$(depth)" = "1" ] || fail_msg "P6: deletion is a change and must enqueue (depth=$(depth))"
) && ok
echo "== P7: no-change is idempotent =="
(
WAKE_STATE_HOME="$(fresh_state p7)"
export WAKE_STATE_HOME
unset WAKE_AGENT
fx="$TMP_ROOT/p7fx"; mkdir -p "$fx"
printf 'stable\n' >"$fx/stable.env"
export WAKE_PREIMAGE_EXTRA="$fx/stable.env"
unset WAKE_DETECTOR_SOURCE_CMD WAKE_WATCH_LIST MOSAIC_HOME
"$PRE" check --enqueue >/dev/null 2>&1 || fail_msg "P7: baseline failed"
r1="$(ledger_rows)"
"$PRE" check --enqueue >/dev/null 2>&1 || fail_msg "P7: second check failed"
[ "$(ledger_rows)" = "$r1" ] || fail_msg "P7: no-change must not append rows ($r1 -> $(ledger_rows))"
[ "$(depth)" = "0" ] || fail_msg "P7: no-change must not enqueue (depth=$(depth))"
) && ok
echo "== P8: unresolvable adapter -> FAIL LOUD, never 'no change' (D2 class) =="
(
WAKE_STATE_HOME="$(fresh_state p8)"
export WAKE_STATE_HOME
unset WAKE_AGENT
export WAKE_DETECTOR_SOURCE_CMD="$TMP_ROOT/does-not-exist-adapter"
unset WAKE_WATCH_LIST WAKE_PREIMAGE_EXTRA MOSAIC_HOME
out="$("$PRE" check --enqueue 2>&1)"; rc=$?
[ "$rc" -ne 0 ] || fail_msg "P8: an unobservable preimage definition must FAIL LOUD (rc=0)"
echo "$out" | grep -qi 'does not resolve' || fail_msg "P8: the failure must name the unresolvable adapter [$out]"
) && ok
echo "== P9: corrupt ledger -> REFUSE to compare/re-baseline =="
(
WAKE_STATE_HOME="$(fresh_state p9)"
export WAKE_STATE_HOME
unset WAKE_AGENT
fx="$TMP_ROOT/p9fx"; mkdir -p "$fx"
printf 'v1\n' >"$fx/f.env"
export WAKE_PREIMAGE_EXTRA="$fx/f.env"
unset WAKE_DETECTOR_SOURCE_CMD WAKE_WATCH_LIST MOSAIC_HOME
"$PRE" check --enqueue >/dev/null 2>&1 || fail_msg "P9: baseline failed"
sd="$(state_dir)"
printf 'NOT-JSON-GARBAGE{{{\n' >>"$sd/preimage/preimage-ledger.jsonl"
r1="$(wc -l <"$sd/preimage/preimage-ledger.jsonl")"
printf 'v2 changed\n' >"$fx/f.env"
out="$("$PRE" check --enqueue 2>&1)"; rc=$?
[ "$rc" -ne 0 ] || fail_msg "P9: a corrupt ledger must FAIL LOUD (rc=0)"
echo "$out" | grep -qi 'unparseable' || fail_msg "P9: the failure must name the corrupt ledger [$out]"
[ "$(wc -l <"$sd/preimage/preimage-ledger.jsonl")" = "$r1" ] || fail_msg "P9: nothing may be appended over corrupt history"
) && ok
echo "== P10: oversized file -> bytes refused, hash still recorded =="
(
WAKE_STATE_HOME="$(fresh_state p10)"
export WAKE_STATE_HOME
unset WAKE_AGENT
fx="$TMP_ROOT/p10fx"; mkdir -p "$fx"
head -c 200 /dev/zero | tr '\0' 'A' >"$fx/big.bin"
export WAKE_PREIMAGE_EXTRA="$fx/big.bin" WAKE_PREIMAGE_MAX_BYTES=64
unset WAKE_DETECTOR_SOURCE_CMD WAKE_WATCH_LIST MOSAIC_HOME
out="$("$PRE" check --enqueue 2>&1)"; rc=$?
[ "$rc" -eq 0 ] || fail_msg "P10: size refusal is not an infra failure (rc=$rc) [$out]"
sd="$(state_dir)"
row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")"
[ "$(jq -r '.captured' <<<"$row")" = "false" ] || fail_msg "P10: row must record captured=false [$row]"
jq -r '.refused // ""' <<<"$row" | grep -qi 'MAX_BYTES' || fail_msg "P10: refusal must name the size cap [$row]"
sha="$(jq -r '.sha256' <<<"$row")"
[ "$sha" = "$(sha256sum "$fx/big.bin" | awk '{print $1}')" ] || fail_msg "P10: hash must still be recorded [$row]"
[ ! -f "$sd/preimage/objects/$sha" ] || fail_msg "P10: oversized bytes must NOT be stored"
) && ok
echo "== P11: reconcile surfaces the cause line before its enumerations =="
(
WAKE_STATE_HOME="$(fresh_state p11)"
export WAKE_STATE_HOME
unset WAKE_AGENT
fx="$TMP_ROOT/p11fx"; mkdir -p "$fx"
make_stub "$fx"; make_wl "$fx/wl.json"
printf 'r1 state\n' >"$fx/repo_r1"
export WAKE_DETECTOR_SOURCE_CMD="$fx/adapter.sh" WAKE_WATCH_LIST="$fx/wl.json"
unset WAKE_PREIMAGE_EXTRA MOSAIC_HOME
# Baseline the preimage set, then change the adapter while "the detector is
# down" — the reconcile path must still produce the first-class cause line.
"$PRE" check >/dev/null 2>&1 || fail_msg "P11: preimage baseline failed"
printf '# adapter changed while detector down\n' >>"$fx/adapter.sh"
"$RECON" reconcile >/dev/null 2>&1 # rc 1 expected (unaccounted enumerated)
sd="$(state_dir)"
pre_seq="$(jq -r 'select(.locators.kind == "preimage") | .observed_seq' "$sd/pending.jsonl" | head -n1)"
enum_seq="$(jq -r 'select(.locators.reconciled == true) | .observed_seq' "$sd/pending.jsonl" | head -n1)"
[ -n "$pre_seq" ] || fail_msg "P11: reconcile must enqueue the preimage cause line"
[ -n "$enum_seq" ] || fail_msg "P11: reconcile must still enumerate the unaccounted source"
if [ -n "$pre_seq" ] && [ -n "$enum_seq" ]; then
[ "$pre_seq" -lt "$enum_seq" ] || fail_msg "P11: cause line must precede the enumeration (preimage seq=$pre_seq, enum seq=$enum_seq)"
fi
) && ok
echo "== P12: detector — preimage infra failure is LOUD but does not starve observation =="
(
WAKE_STATE_HOME="$(fresh_state p12)"
export WAKE_STATE_HOME
unset WAKE_AGENT
fx="$TMP_ROOT/p12fx"; mkdir -p "$fx"
make_stub "$fx"; make_wl "$fx/wl.json"
printf 'r1 state\n' >"$fx/repo_r1"
export WAKE_DETECTOR_SOURCE_CMD="$fx/adapter.sh" WAKE_WATCH_LIST="$fx/wl.json"
unset WAKE_PREIMAGE_EXTRA MOSAIC_HOME
# Corrupt the ledger BEFORE the first poll: the preimage check fails loud,
# but the poll must still observe + baseline the source.
sd="$(state_dir)"
mkdir -p "$sd/preimage"
printf 'NOT-JSON-GARBAGE{{{\n' >"$sd/preimage/preimage-ledger.jsonl"
out="$("$DET" poll-once 2>&1)"; rc=$?
[ "$rc" -ne 0 ] || fail_msg "P12: the pass must exit non-zero on preimage infra failure"
echo "$out" | grep -qi 'preimage' || fail_msg "P12: the failure must name the preimage check [$out]"
n="$(find "$sd/detector" -name 'watch-*.hash' 2>/dev/null | wc -l | tr -d '[:space:]')"
[ "$n" -ge 1 ] || fail_msg "P12: source observation must still proceed (no watch hash baselined)"
) && ok
echo
total=$((pass + $(wc -l <"$FAILFILE")))
echo "== test-wake-preimage: $pass/$total passed =="
[ -s "$FAILFILE" ] && exit 1
exit 0