Files
stack/packages/mosaic/framework/tools/wake/test-wake-preimage.sh
T
mos-dt-0andMos 8d1d6e5e76 feat(wake): #958 A11 preimage.sh — durable provenance for the operator-side preimage definition (#964)
Closes #958.

The preimage definition (source-adapter.sh) is the single most consequential file in the wake pipeline — every observed_hash is a sha256 of what it emits — and it was UNVERSIONED: no git history, no backup. When it was edited at 07:27 on 2026-07-30, attribution was recoverable only because an agent transcript happened to still be on disk. A11 gives that file durable provenance (option (2) of #958: recorded content-addressed, not in-band).

DESIGN, per the pre-registration:
- Provenance is OUT-OF-BAND (never on the adapter's stdout) — an in-band record would advance observed_hash for every source at once and manufacture the re-baseline it exists to explain (B3).
- The obligation never depends on the provenance path: a missing/corrupt store cannot halt the detector or swallow a wake (#940 advisory-fields precedent; B4).
- DESC_FMT=d1 is NOT the provenance record — the tag versions the descriptor FORMAT; a behaviour change that keeps descriptor shape re-baselines every hash and leaves the tag unchanged (B6).

CREDENTIAL HARD GATE (rebuilt after the first verdict FAILED it): byte capture is now RECORD-ONLY BY DEFAULT (extras opt in via WAKE_PREIMAGE_CAPTURE), not allow-by-default-refuse-on-shape — because a shape list can only refuse the secrets someone already enumerated, and the tool's own usage text recommended adding detector.env (where HMAC material lives). Deny is evaluated on BOTH raw and resolved path forms with resolved anchors, ordered before allow — closing the realpath-before-deny ordering defect that let a renamed symlink target through.

VERIFICATION (reviewer, mos-dt, independent of the author's claims):
- Seven decoy cases by planted-marker-then-grep-whole-state-dir: known cred path / same-name symlink / RENAMED-target symlink / prefixed secret / prefixless secret / opted-in-symlink-to-DENIED-target all REFUSED; opted-in-symlink-to-ALLOWED-target CAPTURED (positive control that the harness can capture at all, and that C was not closed by breaking every symlink).
- Polarity-completeness self-test RE-RUN with the shape list stubbed always-allow AND both deny lists stubbed — case D still safe: the flip is complete, the shape list is not load-bearing. Each stub proven live first (a stub that silently fails to apply reports the dangerous state as safe).
- B11: rm-then-change fails LOUD (rc=1), refuses to re-baseline, leaves the ledger absent; absent-with-emptied-objects still first-installs cleanly (absent-is-not-corrupt not paid for by breaking first install).
- RED-first reproduced exactly P13-P16 pre-fix; each refusal corroborated three ways (loud stderr, ledger row captured:false WITH a hash so attribution survives refusal, objects/ holding only the adapter).

KNOWN RESIDUAL (filed #969, non-gating): the deny check is both-forms but the suite needles only the resolved form — a deny reduced to resolved-only survives 17/17 green and would leak a renamed-symlink case. No reachable leak at this head (shipped code correct on all seven decoys); it constrains a FUTURE edit. Doctrine: a both-forms fix needs a needle per form; a fixture that satisfies its assertion through a DIFFERENT rule is testing the rule it did not mean to test.

Authored by pepper (sb-it-1-dt); independently reviewed by mos-dt (sb-it-1-dt) under diff-blind pre-registration (7242688b1, predating first read) — NOT CLEAR on the first verdict (B2/B11 failed by decoy), CLEAR at 8aff7d8 after the polarity rebuild. Manifest version 0.7.0.

Co-authored-by: mos-dt-0 <[email protected]>
2026-07-30 21:48:06 +00:00

535 lines
26 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
#
# Regression needles from the #964 NOT-CLEAR review (decoy method: plant the
# bytes, grep the WHOLE state dir — the decoy does not know what the code
# believes):
# P13 case C: symlinked/renamed credential store (mosaic-home itself behind
# a symlink; a renamed target outside every path rule) — refused on BOTH
# path forms / by content, bytes grep-absent (acceptance b)
# P14 case D: prefix-less high-entropy key (detector.env class) — safe
# WITHOUT opt-in by POLARITY alone (must hold with the shape list
# deleted), and refused WITH opt-in by the named-assignment shape
# P15 polarity: extras are RECORD-ONLY by default (change row + enqueue, no
# bytes); byte capture requires explicit WAKE_PREIMAGE_CAPTURE opt-in
# P16 case B11: ledger deleted while objects/ survives — LOUD refusal to
# re-baseline; the v2->v3 change is NOT absorbed as first-seen
# P17 allowlist symmetry: an allowlisted symlink whose TARGET is denied
# refuses (deny runs first, on both forms — precedence is not path
# agreement); a symlink to an ALLOWED target still captures (the fix
# must not close case C by breaking every symlinked path)
#
# 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: adapter 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"
# Acceptance (a) names the OPERATOR ADAPTER — a core member. Core members
# capture by default (they ARE the preimage definition); extras do not (P15).
export WAKE_DETECTOR_SOURCE_CMD="$fx/adapter.sh" WAKE_WATCH_LIST="$fx/wl.json"
unset WAKE_PREIMAGE_EXTRA WAKE_PREIMAGE_CAPTURE MOSAIC_HOME
"$PRE" check --enqueue >/dev/null 2>&1 || fail_msg "P2: baseline failed"
cp "$fx/adapter.sh" "$fx/adapter.v1"
old_sha="$(sha256sum "$fx/adapter.sh" | awk '{print $1}')"
printf '# adapter v2 CHANGED\n' >>"$fx/adapter.sh"
new_sha="$(sha256sum "$fx/adapter.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/adapter.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.
cmp -s "$sd/preimage/objects/$old_sha" "$fx/adapter.v1" || fail_msg "P2: prior bytes missing or not byte-exact"
cmp -s "$sd/preimage/objects/$new_sha" "$fx/adapter.sh" || fail_msg "P2: current bytes object missing or mismatched"
# 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 — AND
# explicitly opted in to capture. Deny must win over the opt-in (#964: a
# path both denied and allowlisted must refuse).
export WAKE_PREIMAGE_EXTRA="$MOSAIC_HOME/credentials.json"
export WAKE_PREIMAGE_CAPTURE="$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"
# Opted in, so the CONTENT gate (not record-only polarity) is what refuses.
export WAKE_PREIMAGE_EXTRA="$fx/leaky.env" WAKE_PREIMAGE_CAPTURE="$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"
# Opted in, so the SIZE gate (not record-only polarity) is what refuses.
export WAKE_PREIMAGE_EXTRA="$fx/big.bin" WAKE_PREIMAGE_MAX_BYTES=64
export WAKE_PREIMAGE_CAPTURE="$fx/big.bin"
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 "== P13: symlinked/renamed credential store refused on BOTH path forms (#964 C) =="
(
WAKE_STATE_HOME="$(fresh_state p13)"
export WAKE_STATE_HOME
unset WAKE_AGENT
fx="$TMP_ROOT/p13fx"; mkdir -p "$fx"
# The mosaic home is ITSELF behind a symlink: a deny list written in
# unresolved paths cannot match a candidate that was realpath'd before the
# deny saw it — unless the anchors are resolved too.
mkdir -p "$fx/real-store/credentials"
ln -s "$fx/real-store" "$fx/mh"
export MOSAIC_HOME="$fx/mh"
s1='decoy-P13a-renamed-target-under-store'
printf 'x_token=%s\n' "$s1" >"$fx/mh/credentials/brain-creds.json"
# And a renamed target OUTSIDE every known store location, reached via a
# benign-looking symlink: no path rule can name it — the content gate must
# hold on the resolved file.
s2='0123456789abcdef0123456789abcdefP13b'
printf 'service_hmac_key=%s\n' "$s2" >"$fx/renamed-anywhere.cfg"
ln -s "$fx/renamed-anywhere.cfg" "$fx/link-b.env"
export WAKE_PREIMAGE_EXTRA="$fx/mh/credentials/brain-creds.json:$fx/link-b.env"
# Explicit opt-in for BOTH: deny must win over the allowlist.
export WAKE_PREIMAGE_CAPTURE="$fx/mh/credentials/brain-creds.json:$fx/link-b.env"
unset WAKE_DETECTOR_SOURCE_CMD WAKE_WATCH_LIST
out="$("$PRE" check --enqueue 2>&1)"; rc=$?
[ "$rc" -eq 0 ] || fail_msg "P13: refusal is not an infra failure (rc=$rc) [$out]"
sd="$(state_dir)"
n="$(jq -r 'select(.captured == false) | .path' "$sd/preimage/preimage-ledger.jsonl" | wc -l | tr -d '[:space:]')"
[ "$n" = "2" ] || fail_msg "P13: both decoys must record captured=false (got $n)"
# THE invariant (decoy method): the planted bytes exist NOWHERE in state.
if grep -rq "$s1" "$sd" 2>/dev/null; then
fail_msg "P13: renamed-target store bytes leaked into the wake state dir"
fi
if grep -rq "$s2" "$sd" 2>/dev/null; then
fail_msg "P13: prefix-less key bytes leaked into the wake state dir"
fi
) && ok
echo "== P14: detector.env-class key — safe WITHOUT opt-in by polarity, refused WITH opt-in by shape (#964 D) =="
(
WAKE_STATE_HOME="$(fresh_state p14)"
export WAKE_STATE_HOME
unset WAKE_AGENT
fx="$TMP_ROOT/p14fx"; mkdir -p "$fx"
hm='9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c'
printf 'WAKE_HMAC_KEY=%s\n' "$hm" >"$fx/detector.env"
export WAKE_PREIMAGE_EXTRA="$fx/detector.env"
unset WAKE_DETECTOR_SOURCE_CMD WAKE_WATCH_LIST MOSAIC_HOME WAKE_PREIMAGE_CAPTURE
# (a) NO opt-in: polarity alone keeps the bytes out. This leg must hold even
# with the content shape list deleted — the shape is defense-in-depth for
# the opt-in path, never the thing that makes case D read safe.
out="$("$PRE" check --enqueue 2>&1)"; rc=$?
[ "$rc" -eq 0 ] || fail_msg "P14a: record-only tracking must succeed (rc=$rc) [$out]"
sd="$(state_dir)"
row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")"
[ "$(jq -r '.captured' <<<"$row")" = "false" ] || fail_msg "P14a: extra must be record-only without opt-in [$row]"
if grep -rq "$hm" "$sd" 2>/dev/null; then
fail_msg "P14a: key bytes leaked without any opt-in"
fi
# (b) The operator opts the env file in (the exact error the old usage text
# invited): the named-assignment shape must still refuse the bytes.
export WAKE_PREIMAGE_CAPTURE="$fx/detector.env"
printf 'WAKE_HMAC_KEY=%s\nrotated=1\n' "$hm" >"$fx/detector.env"
out="$("$PRE" check --enqueue 2>&1)"; rc=$?
[ "$rc" -eq 0 ] || fail_msg "P14b: refusal is not an infra failure (rc=$rc) [$out]"
row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")"
[ "$(jq -r '.captured' <<<"$row")" = "false" ] || fail_msg "P14b: opted-in key material must still refuse [$row]"
jq -r '.refused // ""' <<<"$row" | grep -qi 'secret' || fail_msg "P14b: refusal must name secret-shaped content [$row]"
if grep -rq "$hm" "$sd" 2>/dev/null; then
fail_msg "P14b: key bytes leaked despite refusal"
fi
) && ok
echo "== P15: extras are RECORD-ONLY by default; capture requires explicit opt-in =="
(
WAKE_STATE_HOME="$(fresh_state p15)"
export WAKE_STATE_HOME
unset WAKE_AGENT
fx="$TMP_ROOT/p15fx"; mkdir -p "$fx"
printf 'plain v1\n' >"$fx/notes.cfg"
export WAKE_PREIMAGE_EXTRA="$fx/notes.cfg"
unset WAKE_DETECTOR_SOURCE_CMD WAKE_WATCH_LIST MOSAIC_HOME WAKE_PREIMAGE_CAPTURE
"$PRE" check --enqueue >/dev/null 2>&1 || fail_msg "P15: baseline failed"
printf 'plain v2\n' >"$fx/notes.cfg"
sha2="$(sha256sum "$fx/notes.cfg" | awk '{print $1}')"
"$PRE" check --enqueue >/dev/null 2>&1 || fail_msg "P15: change check failed"
sd="$(state_dir)"
row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")"
[ "$(jq -r '.captured' <<<"$row")" = "false" ] || fail_msg "P15: default must be record-only [$row]"
[ ! -f "$sd/preimage/objects/$sha2" ] || fail_msg "P15: bytes captured without opt-in"
[ "$(depth)" = "1" ] || fail_msg "P15: record-only must still track the change (depth=$(depth))"
# Opt in -> the next change captures.
export WAKE_PREIMAGE_CAPTURE="$fx/notes.cfg"
printf 'plain v3\n' >"$fx/notes.cfg"
sha3="$(sha256sum "$fx/notes.cfg" | awk '{print $1}')"
"$PRE" check --enqueue >/dev/null 2>&1 || fail_msg "P15: opted-in change check failed"
row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")"
[ "$(jq -r '.captured' <<<"$row")" = "true" ] || fail_msg "P15: opt-in must capture [$row]"
cmp -s "$sd/preimage/objects/$sha3" "$fx/notes.cfg" || fail_msg "P15: opted-in bytes missing or mismatched"
) && ok
echo "== P16: deleted ledger over surviving objects/ — REFUSE to re-baseline (#964 B11) =="
(
WAKE_STATE_HOME="$(fresh_state p16)"
export WAKE_STATE_HOME
unset WAKE_AGENT
fx="$TMP_ROOT/p16fx"; mkdir -p "$fx"
make_stub "$fx"
export WAKE_DETECTOR_SOURCE_CMD="$fx/adapter.sh"
unset WAKE_WATCH_LIST WAKE_PREIMAGE_EXTRA WAKE_PREIMAGE_CAPTURE MOSAIC_HOME
"$PRE" check >/dev/null 2>&1 || fail_msg "P16: v1 baseline failed"
printf '# v2\n' >>"$fx/adapter.sh"
"$PRE" check >/dev/null 2>&1 || fail_msg "P16: v2 change failed"
sd="$(state_dir)"
nobj="$(ls "$sd/preimage/objects" | wc -l | tr -d '[:space:]')"
# Delete the ledger; objects/ survives. ABSENT is not CORRUPT — and it is
# not a first install either: that asymmetry is locally detectable.
rm -f "$sd/preimage/preimage-ledger.jsonl"
printf '# v3\n' >>"$fx/adapter.sh"
out="$("$PRE" check --enqueue 2>&1)"; rc=$?
[ "$rc" -ne 0 ] || fail_msg "P16: absent ledger over prior objects must FAIL LOUD (rc=0) [$out]"
echo "$out" | grep -qi 'REFUSING to re-baseline' || fail_msg "P16: the failure must name the refusal [$out]"
[ ! -e "$sd/preimage/preimage-ledger.jsonl" ] || fail_msg "P16: the ledger must NOT be silently recreated over deleted history"
[ "$(ls "$sd/preimage/objects" | wc -l | tr -d '[:space:]')" = "$nobj" ] || fail_msg "P16: prior objects must remain untouched"
[ "$(depth)" = "0" ] || fail_msg "P16: the v2->v3 change must NOT be absorbed or enqueued from an unverifiable baseline (depth=$(depth))"
) && ok
echo "== P17: allowlist symmetry — symlink to a DENIED target refuses; symlink to an allowed target captures =="
(
WAKE_STATE_HOME="$(fresh_state p17)"
export WAKE_STATE_HOME
unset WAKE_AGENT
fx="$TMP_ROOT/p17fx"; mkdir -p "$fx"
export MOSAIC_HOME="$fx/mh"
mkdir -p "$MOSAIC_HOME"
s='decoy-P17-cred-behind-benign-alias'
printf '{"svc":{"token":"%s"}}\n' "$s" >"$MOSAIC_HOME/credentials.json"
# A benign-looking alias whose TARGET is denied: the allowlist matches the
# alias string, but deny runs FIRST and on BOTH forms — "deny wins over
# opt-in" is a precedence rule, path agreement is what makes it hold.
ln -s "$MOSAIC_HOME/credentials.json" "$fx/settings.json"
# And a symlink to an ALLOWED target: the fix must not close case C by
# breaking every symlinked path outright.
printf 'benign payload v1\n' >"$fx/target.cfg"
ln -s "$fx/target.cfg" "$fx/alias.cfg"
export WAKE_PREIMAGE_EXTRA="$fx/settings.json:$fx/alias.cfg"
export WAKE_PREIMAGE_CAPTURE="$fx/settings.json:$fx/alias.cfg"
unset WAKE_DETECTOR_SOURCE_CMD WAKE_WATCH_LIST
out="$("$PRE" check --enqueue 2>&1)"; rc=$?
[ "$rc" -eq 0 ] || fail_msg "P17: check must exit 0 (rc=$rc) [$out]"
sd="$(state_dir)"
crow="$(jq -c --arg p "$(realpath "$fx/settings.json")" 'select(.path == $p)' "$sd/preimage/preimage-ledger.jsonl" | tail -n1)"
[ "$(jq -r '.captured' <<<"$crow")" = "false" ] || fail_msg "P17: allowlisted symlink to a denied target must refuse [$crow]"
if grep -rq "$s" "$sd" 2>/dev/null; then
fail_msg "P17: credential bytes leaked via an allowlisted alias"
fi
brow="$(jq -c --arg p "$(realpath "$fx/alias.cfg")" 'select(.path == $p)' "$sd/preimage/preimage-ledger.jsonl" | tail -n1)"
[ "$(jq -r '.captured' <<<"$brow")" = "true" ] || fail_msg "P17: symlink to an allowed target must still capture [$brow]"
) && ok
echo
total=$((pass + $(wc -l <"$FAILFILE")))
echo "== test-wake-preimage: $pass/$total passed =="
[ -s "$FAILFILE" ] && exit 1
exit 0