forked from mosaicstack/stack
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
667402457e | ||
|
|
313f3087e6 |
@@ -54,16 +54,6 @@ Commands:
|
||||
Local-write only; a background sync
|
||||
ships it (never blocks on network).
|
||||
--no-sync suppresses the background ship.
|
||||
--force-past-quarantine passes the #946
|
||||
force flag through to store.sh consume:
|
||||
the ONLY way to advance past a
|
||||
QUARANTINED (dead-lettered, never
|
||||
delivered) seq. The store's per-seq
|
||||
step-over diagnostics are re-emitted on
|
||||
stderr; no consumed-hash witness is
|
||||
recorded for the quarantined entry.
|
||||
Without the flag, a consume that would
|
||||
cross a quarantined seq is REFUSED.
|
||||
embed --upto N [--wake-id ID] [--agent A]
|
||||
Print the copy-run ack line to EMBED in
|
||||
a digest (does not perform the ack).
|
||||
@@ -179,7 +169,7 @@ cmd_received() {
|
||||
|
||||
cmd_consumed() {
|
||||
_need_jq
|
||||
local upto='' wake_id='' do_sync="1" force="0"
|
||||
local upto='' wake_id='' do_sync="1"
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--upto)
|
||||
@@ -194,10 +184,6 @@ cmd_consumed() {
|
||||
do_sync="0"
|
||||
shift
|
||||
;;
|
||||
--force-past-quarantine)
|
||||
force="1"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "ack.sh consumed: unknown option '$1'" >&2
|
||||
exit 2
|
||||
@@ -214,25 +200,11 @@ cmd_consumed() {
|
||||
# Advance consumed_seq via the store. The store enforces the CONTIGUOUS
|
||||
# gapless-prefix rule and rejects a gap (cannot ack N while N-1 unconsumed).
|
||||
# This is a LOCAL-WRITE cursor advance — no network.
|
||||
local new_cursor store_args
|
||||
store_args=(consume --upto "$upto")
|
||||
if [ "$force" = "1" ]; then
|
||||
store_args+=(--force-past-quarantine)
|
||||
fi
|
||||
if ! new_cursor="$("$STORE_SH" "${store_args[@]}" 2>&1)"; then
|
||||
local new_cursor
|
||||
if ! new_cursor="$("$STORE_SH" consume --upto "$upto" 2>&1)"; then
|
||||
echo "ack.sh consumed: refused — $new_cursor" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$force" = "1" ]; then
|
||||
# #946: on the forced path the store's LOUD per-seq step-over diagnostics
|
||||
# were captured together with the cursor line (2>&1 above). Re-emit them on
|
||||
# OUR stderr — the loudness must survive the wrapper — and keep only the
|
||||
# final line (the cursor) for the CONSUMED report below.
|
||||
local cursor_line
|
||||
cursor_line="$(printf '%s\n' "$new_cursor" | tail -n1)"
|
||||
printf '%s\n' "$new_cursor" | sed '$d' | grep -v '^[[:space:]]*$' >&2 || true
|
||||
new_cursor="$cursor_line"
|
||||
fi
|
||||
|
||||
# Record the CONSUMED ack in the local ledger (still no network).
|
||||
local record
|
||||
|
||||
@@ -295,24 +295,16 @@ _poll_source() {
|
||||
# is interpolated into $((...)) under set -u, so a non-numeric value
|
||||
# ('300s', '5m', 'abc') would be FATAL to the poll — the one thing this
|
||||
# block must never be. Resolve it ONCE, validate, fall back loudly.
|
||||
local now_s slack slack_ok
|
||||
local now_s slack
|
||||
slack="${WAKE_SNAPSHOT_TS_FUTURE_SLACK:-300}"
|
||||
# NOT grep: grep is LINE-oriented, so ^...$ anchors bind per line and a
|
||||
# multi-line value ($'300\n8') passes the regex yet is FATAL in $((...)).
|
||||
# The case pattern matches the WHOLE string, newlines included. (snap_ts
|
||||
# is immune: jq's number type-check above cannot emit an embedded newline.)
|
||||
case "$slack" in
|
||||
'' | *[!0-9]*) slack_ok=1 ;;
|
||||
*) [ "${#slack}" -le 9 ] && slack_ok=0 || slack_ok=1 ;;
|
||||
esac
|
||||
if [ "$slack_ok" -ne 0 ]; then
|
||||
echo "detector.sh: WAKE_SNAPSHOT_TS_FUTURE_SLACK='$slack' is not a plain non-negative integer of at most 9 digits (seconds) — falling back to 300, poll continues (#940)." >&2
|
||||
if ! printf '%s' "$slack" | grep -Eq '^[0-9]{1,9}$'; then
|
||||
echo "detector.sh: WAKE_SNAPSHOT_TS_FUTURE_SLACK='$slack' is not a plain non-negative integer (seconds) — falling back to 300, poll continues (#940)." >&2
|
||||
slack=300
|
||||
fi
|
||||
# Shape validation is not radix validation: bash reads a leading zero as
|
||||
# OCTAL, so '08'/'09' pass the shape check yet are FATAL in $((...)), and
|
||||
# OCTAL, so '08'/'09' pass the regex yet are FATAL in $((...)), and
|
||||
# '0300' silently means 192. Force base-10 so the knob means what the
|
||||
# operator wrote (safe: the case pattern above guarantees pure digits).
|
||||
# operator wrote (safe: the regex above guarantees pure digits).
|
||||
slack=$((10#$slack))
|
||||
now_s="$(date +%s)"
|
||||
if [ "$snap_ts" -gt $((now_s + slack)) ]; then
|
||||
@@ -414,22 +406,6 @@ cmd_poll_once() {
|
||||
_wake_clean_stale_tmp "$STATE_DIR"
|
||||
mkdir -p "$DET_DIR"
|
||||
|
||||
local failed=0
|
||||
|
||||
# #958 preimage provenance: check the OPERATOR-SIDE preimage definition (the
|
||||
# source adapter file, the watch-list, operator-declared extras) BEFORE
|
||||
# observing any source. A changed preimage re-baselines EVERY source at once;
|
||||
# running the check first means its first-class cause line is enqueued at a
|
||||
# LOWER observed_seq than the N per-source deltas it explains, so the digest
|
||||
# shows the cause, not just the flood. An infrastructure failure of the check
|
||||
# is LOUD and marks this pass failed (G2a discipline — never read as "no
|
||||
# change"), but source observation still proceeds: provenance must not be
|
||||
# able to starve wake delivery.
|
||||
if ! "$SCRIPT_DIR/preimage.sh" check --enqueue; then
|
||||
echo "detector.sh: FAIL LOUD — preimage provenance check failed (see preimage.sh above); source observation continues but this pass exits non-zero." >&2
|
||||
failed=1
|
||||
fi
|
||||
|
||||
# Iterate the DECLARED source-coverage inventory (§4/G3): only sources listed
|
||||
# in watches[].sources[] are polled. An omitted source is not observed (and so
|
||||
# cannot make anything pass vacuously); a referenced-but-undefined source is a
|
||||
@@ -443,7 +419,7 @@ cmd_poll_once() {
|
||||
exit 2
|
||||
fi
|
||||
|
||||
local kind id def class
|
||||
local failed=0 kind id def class
|
||||
while IFS=$'\t' read -r kind id; do
|
||||
[ -n "$kind" ] || continue
|
||||
# Resolve the source definition from its top-level collection by id.
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
# sufficiency NEVER exempts a consequential action from its live gate.
|
||||
#
|
||||
# HARD LOCATORS (§2.1): every actionable claim MUST carry a precise locator
|
||||
# (repo + issue#, a 40-char SHA, file:anchor, or path — #944) so re-verification
|
||||
# is ONE targeted call. A missing locator = malformed ACTIONABLE entry = FAIL-LOUD.
|
||||
# (repo + issue#, a 40-char SHA, or file:anchor) so re-verification is ONE
|
||||
# targeted call. A missing locator = malformed ACTIONABLE entry = FAIL-LOUD.
|
||||
#
|
||||
# #920 (per-entry quarantine — fail-loud WITHOUT head-of-line blocking): a
|
||||
# render-refused entry (actionable-tier, no hard locator) is QUARANTINED — durably
|
||||
@@ -112,14 +112,6 @@ Exit codes:
|
||||
diagnostic (#924/G2a) — it never wedges the whole render either.
|
||||
Reconciler enumerations (locators.reconciled==true) render ORIENTATION-tier,
|
||||
gate-exempt.
|
||||
#946: quarantined entries are DISCLOSED in a QUARANTINED section (by
|
||||
seq/class only — content stays excluded) and the embedded ack copy-run
|
||||
line is CLAMPED below the lowest quarantined seq (the digest never
|
||||
instructs the consumer to record a delivery that never happened; the
|
||||
clamp is announced as an ACK CLAMPED note). A store-mode render also
|
||||
REPLACES the store's quarantined.set (store.sh quarantine-sync) so the
|
||||
consume path enforces the same clamp; --from-file/--stdin renders never
|
||||
touch the set.
|
||||
2 usage error.
|
||||
3 jq is required but missing.
|
||||
|
||||
@@ -217,28 +209,13 @@ _scrub_free() {
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# _has_hard_locator LOCATORS_JSON — true iff the locators object carries at least
|
||||
# one PRECISE locator sufficient for one-call re-verification. BOTH locator
|
||||
# vocabularies in live use are accepted (the same two _locator_line documents
|
||||
# under #914b):
|
||||
# forge shape (§2.1): repo + issue (issue#) | 40-hex sha | file (file:anchor)
|
||||
# detector shape (#944): path
|
||||
# The `path` arm was added by #944: detector.sh (A1) builds board_file locators
|
||||
# as kind/id/observed_hash + path (+ snapshot_sha when adapter-attested, #940)
|
||||
# — none of which the gate tested — so a class=actionable board_file entry
|
||||
# could NEVER pass and every heartbeat-planning delta dead-lettered (live seqs
|
||||
# 63/68). `path` mirrors `file` exactly (same one-call "re-read X" hint below;
|
||||
# with an attested snapshot_sha the hint upgrades to one-call
|
||||
# `git show <snapshot_sha>:<path>`). DELIBERATELY NOT ARMS: `observed_hash`
|
||||
# (a content hash, not an address) and bare `snapshot_sha` without `path`
|
||||
# (a path-less 40-hex would widen the gate past the board_file vocabulary —
|
||||
# review-adopted criterion on #944; snapshot_sha's precision is only reachable
|
||||
# THROUGH a path, so path is the address and snapshot_sha stays a refinement).
|
||||
# one PRECISE locator sufficient for one-call re-verification:
|
||||
# repo + issue (issue#) | 40-hex sha | file (file:anchor).
|
||||
_has_hard_locator() {
|
||||
jq -e '
|
||||
((.repo // "") != "" and ((.issue // "") | tostring) != "")
|
||||
or (((.sha // "") | test("^[0-9a-f]{40}$")))
|
||||
or ((.file // "") != "")
|
||||
or ((.path // "") != "")
|
||||
' >/dev/null 2>&1 <<<"$1"
|
||||
}
|
||||
|
||||
@@ -246,18 +223,16 @@ _has_hard_locator() {
|
||||
# one-targeted-call re-verify hint, where available.
|
||||
#
|
||||
# #914b: covers BOTH locator vocabularies actually in use:
|
||||
# - the HARD-locator shape (§2.1, gated by _has_hard_locator):
|
||||
# - the HARD-locator shape (§2.1, gated by _has_hard_locator, unchanged):
|
||||
# repo+issue | 40-hex sha | file(:anchor).
|
||||
# - the shape detector.sh (A1) actually builds for a `digest`-class entry
|
||||
# (see detector.sh's enqueue-locators jq filter): kind/id/observed_hash +
|
||||
# whichever of repo/path/anchor/remote/branches the source def declares.
|
||||
# None of kind/id/observed_hash/remote/path/branches were recognized here
|
||||
# before, so a real digest-class pointer rendered a bare empty "locator:"
|
||||
# line despite the entry carrying real (soft, non-hard) locator data.
|
||||
# (At #914b this was display-only; #944 later promoted `path` — and ONLY
|
||||
# `path` — into _has_hard_locator, since it carries the same one-call
|
||||
# re-verify precision as `file`. kind/id/observed_hash/remote/branches
|
||||
# and bare snapshot_sha remain soft/display-only.)
|
||||
# line despite the entry carrying real (soft, non-hard) locator data. This
|
||||
# is display-only: it does NOT feed _has_hard_locator, so the
|
||||
# ACTIONABLE-tier hard-locator FAIL-LOUD gate is untouched.
|
||||
_locator_line() {
|
||||
local loc="$1" repo issue sha file anchor head parts='' reverify=''
|
||||
local remote path kind id ohash branches snap_sha snap_ts
|
||||
@@ -502,7 +477,7 @@ _quarantine_entry() {
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
echo "digest.sh: FAIL-LOUD QUARANTINE (#920) — malformed ACTIONABLE entry at observed_seq=$seq has no §2.1 hard locator (repo+issue# / 40-hex sha / file:anchor / path). DEAD-LETTERED to $dlq and EXCLUDED from this digest; the rest of the cumulative set still renders (no head-of-line block). Re-feed the source with a valid hard locator." >&2
|
||||
echo "digest.sh: FAIL-LOUD QUARANTINE (#920) — malformed ACTIONABLE entry at observed_seq=$seq has no §2.1 hard locator (repo+issue#/40-char SHA/file:anchor). DEAD-LETTERED to $dlq and EXCLUDED from this digest; the rest of the cumulative set still renders (no head-of-line block). Re-feed the source with a valid hard locator." >&2
|
||||
# #924 (G2a): route the SAME per-entry alarm off-host too, deduped by
|
||||
# observed_seq so a still-dead-lettered entry re-drained every tick is
|
||||
# alarmed off-host EXACTLY ONCE (never once per re-render).
|
||||
@@ -552,7 +527,7 @@ cmd_render() {
|
||||
# #920 head-of-line-blocking defect that exit-4'd the entire cumulative-state
|
||||
# drain). Reconciler enumerations (locators.reconciled==true) are ORIENTATION-
|
||||
# tier and gate-exempt, so they pass straight through to the deliverable set.
|
||||
local line loc seq pending_ok='' quarantined=0 q_seqs='' q_disclose=''
|
||||
local line loc seq pending_ok='' quarantined=0
|
||||
while IFS= read -r line; do
|
||||
[ -n "$line" ] || continue
|
||||
printf '%s' "$line" | jq -e . >/dev/null 2>&1 || continue
|
||||
@@ -562,17 +537,6 @@ cmd_render() {
|
||||
seq="$(jq -r '.observed_seq // "?"' <<<"$line")"
|
||||
_quarantine_entry "$line" "$seq"
|
||||
quarantined=$((quarantined + 1))
|
||||
# #946: collect the quarantined identity for DISCLOSURE + the ack
|
||||
# CLAMP. Disclosure is by durable identity (observed_seq) + class ONLY:
|
||||
# this entry failed the locator gate, so its content is exactly what
|
||||
# this digest refuses to re-inject (the exclusion property Q1/Q4
|
||||
# assert) — the consumer re-verifies via the dead-letter ledger, never
|
||||
# via this line.
|
||||
case "$seq" in
|
||||
'' | *[!0-9]*) : ;; # an unnumbered entry cannot clamp the numeric cursor
|
||||
*) q_seqs="$q_seqs$seq"$'\n' ;;
|
||||
esac
|
||||
q_disclose="$q_disclose * seq $seq [$(_scrub_inline "$(jq -r '.class // "actionable"' <<<"$line")")] HELD — dead-lettered (no §2.1 hard locator); content withheld, NOT delivered."$'\n'
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
@@ -582,33 +546,6 @@ cmd_render() {
|
||||
pending="$(printf '%s' "$pending_ok" | grep -v '^[[:space:]]*$' || true)"
|
||||
depth="$(printf '%s\n' "$pending" | grep -c . || true)"
|
||||
|
||||
# --- #946: quarantine truth-sync + ack clamp ------------------------------
|
||||
# (1) SYNC: an AUTHORITATIVE full-set render (src=store) REPLACES the store's
|
||||
# quarantined.set with THIS render's quarantined seqs (possibly none — an
|
||||
# empty replace IS the #944 recovery: once the gate is fixed and everything
|
||||
# renders, the stale set clears and the store-side clamp self-heals). A
|
||||
# foreign-data render (--from-file/--stdin) must NEVER rewrite lane truth.
|
||||
if [ "$src" = "store" ]; then
|
||||
if ! printf '%s' "$q_seqs" | "$STORE_SH" quarantine-sync; then
|
||||
echo "digest.sh: WARN (#946) — store.sh quarantine-sync FAILED; the store-side consume clamp may be stale for this lane (the clamped ack line rendered below is still correct)." >&2
|
||||
fi
|
||||
fi
|
||||
# (2) CLAMP: the embedded ack may advance AT MOST to just below the LOWEST
|
||||
# quarantined seq — consume requires a contiguous prefix, so one held seq
|
||||
# caps everything above it. With nothing quarantined this is the observed
|
||||
# cursor unchanged. Render-local on purpose: it protects the copy-run line in
|
||||
# EVERY mode, including hermetic --from-file renders.
|
||||
local ack_upto="$observed" min_q='' qs q_list=''
|
||||
while IFS= read -r qs; do
|
||||
[ -n "$qs" ] || continue
|
||||
if [ -z "$min_q" ] || [ "$qs" -lt "$min_q" ]; then min_q="$qs"; fi
|
||||
done <<<"$q_seqs"
|
||||
if [ -n "$min_q" ] && [ "$((min_q - 1))" -lt "$ack_upto" ]; then
|
||||
ack_upto=$((min_q - 1))
|
||||
fi
|
||||
[ "$ack_upto" -ge 0 ] || ack_upto=0
|
||||
q_list="$(printf '%s' "$q_seqs" | tr '\n' ' ' | sed -e 's/[[:space:]]*$//')"
|
||||
|
||||
# --- render (all validated) ----------------------------------------------
|
||||
local n_actionable=0
|
||||
{
|
||||
@@ -702,16 +639,6 @@ cmd_render() {
|
||||
printf '%s\n' "$hbody"
|
||||
fi
|
||||
|
||||
# #946: QUARANTINED disclosure — a held entry must be VISIBLE in the digest
|
||||
# it was held from (five successive live digests each silently stepped the
|
||||
# consumer past buried seq 68). Disclosure is by seq/class ONLY; the
|
||||
# entry's content already failed the locator gate and stays EXCLUDED.
|
||||
if [ -n "$q_disclose" ]; then
|
||||
printf '\n-- QUARANTINED (dead-lettered; HELD — NOT delivered; the ack below does NOT cover these) --\n'
|
||||
printf '%s' "$q_disclose"
|
||||
printf ' disposition: see %s/dead-letter.jsonl — fix the source locator (re-delivery is automatic once the entry passes the gate), or step past EXPLICITLY with ack.sh consumed --force-past-quarantine.\n' "$STATE_DIR"
|
||||
fi
|
||||
|
||||
# Embedded ack copy-run line (W2). CONSUMED is a consumer act; this is the
|
||||
# exact local-write line the consumer runs after durable capture.
|
||||
#
|
||||
@@ -726,19 +653,12 @@ cmd_render() {
|
||||
# itself was env-less (agent=="default"), baking "default" is no worse than
|
||||
# today — the fix wins the common case where WAKE_AGENT was set at render.
|
||||
printf '\n-- ACK (copy-run; local-write only, never blocks on network) --\n'
|
||||
# #946: the embedded --upto is the CLAMPED cursor (ack_upto), never the raw
|
||||
# observed cursor while a quarantined seq sits inside (consumed, observed] —
|
||||
# the copy-run line itself must not instruct the consumer to record
|
||||
# deliveries that never happened. The clamp is disclosed loudly.
|
||||
if [ "$ack_upto" -ne "$observed" ]; then
|
||||
printf '# ACK CLAMPED (#946): embedding --upto %s, not observed_seq %s — quarantined seq(s) %s were dead-lettered and NEVER delivered; an ordinary ack cannot step past them. Only ack.sh consumed ... --force-past-quarantine (loud) can.\n' "$ack_upto" "$observed" "$q_list"
|
||||
fi
|
||||
local ack_line agent_scrubbed
|
||||
agent_scrubbed="$(_scrub_inline "$agent")"
|
||||
if [ -n "$wake_id" ]; then
|
||||
ack_line="$("$ACK_SH" embed --upto "$ack_upto" --agent "$agent_scrubbed" --wake-id "$wake_id" 2>/dev/null || true)"
|
||||
ack_line="$("$ACK_SH" embed --upto "$observed" --agent "$agent_scrubbed" --wake-id "$wake_id" 2>/dev/null || true)"
|
||||
else
|
||||
ack_line="$("$ACK_SH" embed --upto "$ack_upto" --agent "$agent_scrubbed" 2>/dev/null || true)"
|
||||
ack_line="$("$ACK_SH" embed --upto "$observed" --agent "$agent_scrubbed" 2>/dev/null || true)"
|
||||
fi
|
||||
printf '%s\n' "${ack_line:-# ack unavailable}"
|
||||
} | _redact_secrets
|
||||
|
||||
@@ -276,21 +276,15 @@
|
||||
# yields a NEGATIVE age — stale-reads-fresher-than-fresh, the
|
||||
# exact failure class #940 fixes. The SLACK knob itself is
|
||||
# operator input interpolated into arithmetic under set -u, so it
|
||||
# gets the same discipline (#941 §2 round 2): shape-validated as
|
||||
# a plain non-negative integer of at most 9 digits, else LOUD
|
||||
# fallback to 300 — a malformed knob
|
||||
# gets the same discipline (#941 §2 round 2): validated
|
||||
# ^[0-9]{1,9}$, else LOUD fallback to 300 — a malformed knob
|
||||
# ('300s', '5m', 'abc') must never kill the poll, and a negative
|
||||
# one must never invert the guard into deny-all. Shape validation
|
||||
# is NOT radix validation (#942 review): bash reads leading zeros
|
||||
# as OCTAL, so '08'/'09' pass the shape check yet are fatal in
|
||||
# $((...)) and '0300' silently means 192 — the knob is therefore
|
||||
# forced base-10 (10#) after validation, so it means what the
|
||||
# operator wrote. The validator and the consumer must also agree
|
||||
# on STRING EXTENT (#942 follow-up): grep's ^...$ anchors bind
|
||||
# per LINE, so a multi-line value ($'300\n8') passed the regex
|
||||
# whole yet was fatal in $((...)) — validation is a whole-string
|
||||
# case pattern, not grep, so an embedded newline rejects.
|
||||
# SKEW GUARANTEE
|
||||
# as OCTAL, so '08'/'09' pass the regex yet are fatal in $((...))
|
||||
# and '0300' silently means 192 — the knob is therefore forced
|
||||
# base-10 (10#) after validation, so it means what the operator
|
||||
# wrote. SKEW GUARANTEE
|
||||
# (stated, not implied): the future-skew check runs against the
|
||||
# DETECTOR's clock; consumer-side age arithmetic runs on the
|
||||
# consumer's. A surviving snapshot_ts is therefore attested only
|
||||
@@ -306,148 +300,8 @@
|
||||
# absence of these fields.
|
||||
# Changed: detector.sh, digest.sh (+ test-wake-detector.sh
|
||||
# D10/D11/D12/D13, test-wake-digest-quarantine.sh Q10).
|
||||
# 0.6.13 #942/#943 SLACK-knob validation hardening, split from 0.6.12 because
|
||||
# version= is the component's SOLE self-identity claim (no per-file
|
||||
# hashes here) and two detector-changing merges after the 0.6.12
|
||||
# stamp had left three materially different detectors under one
|
||||
# version string (#943 review §2). #942: the knob is resolved once,
|
||||
# shape-validated, LOUD fallback 300, and forced base-10 (10#) so
|
||||
# zero-padded values mean what the operator wrote instead of octal.
|
||||
# #943: validation is a whole-string case pattern, not grep, so an
|
||||
# embedded newline ($'300\n8' — accepted per-line by grep's ^...$
|
||||
# anchors, fatal in $((...))) rejects. Full rationale in the knob
|
||||
# paragraph of the 0.6.12 entry above.
|
||||
# Changed: detector.sh (+ test-wake-detector.sh D13).
|
||||
# 0.6.14 #944 the §2.1 hard-locator gate was UNSATISFIABLE for detector-built
|
||||
# actionable board_file entries: _has_hard_locator tested only
|
||||
# repo+issue / 40-hex sha / file, while detector.sh (A1) builds
|
||||
# kind/id/observed_hash + path (+ snapshot_sha/_ts when attested,
|
||||
# #940) — no key in common, so every class=actionable board_file
|
||||
# delta was structurally guaranteed to dead-letter (live: mos-dt
|
||||
# seqs 63/68, 2026-07-30; the only tier with a 30m SLO delivered
|
||||
# nothing on its only actionable source). Fix: `path` becomes a
|
||||
# hard-locator arm — and ONLY path: it mirrors `file`'s one-call
|
||||
# "re-read X" precision, upgrading to one-call
|
||||
# `git show <snapshot_sha>:<path>` when a snapshot is attested.
|
||||
# observed_hash (content hash, not an address) and bare path-less
|
||||
# snapshot_sha (would widen the gate past the board_file
|
||||
# vocabulary — review-adopted criterion) remain NON-arms.
|
||||
# Quarantine is a RENDER-TIME filter (entries never leave
|
||||
# pending), so existing UNCONSUMED dead-letters re-deliver
|
||||
# automatically on the first post-upgrade drain; consumed-past
|
||||
# dead-letters are not requeued.
|
||||
# Changed: digest.sh (+ test-wake-digest-quarantine.sh: Q11
|
||||
# positive control — the live seq-68 entry verbatim must RENDER
|
||||
# as CLAIM@seq — and Q1/Q6-Q9 fixtures moved off the now-valid
|
||||
# path-bearing shape onto genuinely address-free shapes,
|
||||
# amending the #920-era ruling that had pinned the live pilot's
|
||||
# own locator shape as the malformed example). Doc follow-up:
|
||||
# #948 amends CONVERGED-DESIGN.md §2.1 to add `path` to the
|
||||
# hard-locator enumeration and to state the operative test as
|
||||
# "one targeted call, never a search" (NOT "pins the observed
|
||||
# state") — sequenced AFTER the reseed so the edit itself is a
|
||||
# live delivery test of the fixed gate.
|
||||
# 0.6.15 #946 the digest's embedded ack watermark covered quarantined
|
||||
# entries: quarantine is a render-time filter (0.6.14), so the
|
||||
# suggested `ack.sh consumed --upto <observed_seq>` stepped the
|
||||
# cursor PAST dead-lettered seqs and _record_last_consumed then
|
||||
# wrote consumed-hash witness rows for deliveries that never
|
||||
# happened (live: mos-dt seq 68 buried under five digests;
|
||||
# Finding A: a false 9d0f639f…@63 witness row). Fix — disclose
|
||||
# AND clamp: (1) the rendered digest gains a QUARANTINED section
|
||||
# (seq + class + HELD only; ids/locators stay withheld,
|
||||
# preserving the exclusion property) and the embedded ack is
|
||||
# clamped to min(observed_seq, min quarantined seq − 1);
|
||||
# (2) store.sh consume REFUSES to cross an unconsumed
|
||||
# quarantined seq — `--force-past-quarantine` (plumbed through
|
||||
# ack.sh consumed) is the ONLY way past, loud per-seq on stderr,
|
||||
# and even the forced path never writes a consumed-hash witness
|
||||
# for a quarantined seq; (3) render --from-store syncs the
|
||||
# store-owned quarantined.set via new `store.sh quarantine-sync`
|
||||
# (full-replace, so a gate fix self-heals stale quarantine;
|
||||
# --from-file/--stdin never touch the set); (4) new
|
||||
# `store.sh quarantine-audit [--repair]` sweeps consumed-hashes
|
||||
# for rows provably contradicted by the dead-letter ledger
|
||||
# (report exits 1; --repair removes only provably-false rows;
|
||||
# the ledger itself is history and is never modified; rows whose
|
||||
# dead-letter evidence was pruned are unprovable and untouched).
|
||||
# Changed: store.sh, digest.sh, ack.sh
|
||||
# (+ test-wake-store-ack.sh T13-T16,
|
||||
# test-wake-digest-quarantine.sh Q12-Q16).
|
||||
# 0.7.0 #958 A11 preimage.sh — durable provenance for the OPERATOR-SIDE
|
||||
# preimage definition (wake-pilot finding: source-adapter.sh was
|
||||
# unversioned, so the operator-owned bytes every observed_hash is
|
||||
# computed FROM had thinner provenance than any hash they feed;
|
||||
# attribution required an agent transcript). NEW tool + two
|
||||
# pre-step integrations, ADDITIVE: (1) preimage.sh derives the
|
||||
# preimage set from the RUNTIME env (the resolved
|
||||
# WAKE_DETECTOR_SOURCE_CMD file, WAKE_WATCH_LIST, optional
|
||||
# WAKE_PREIMAGE_EXTRA paths) and, per file, records
|
||||
# {ts,path,sha256,size,mtime,prev} to an append-only ledger +
|
||||
# captures the bytes CONTENT-ADDRESSED under
|
||||
# $STATE_DIR/preimage/objects/<sha256> — prior bytes + change
|
||||
# time are answerable from durable state alone, no transcript
|
||||
# (acceptance a). A git-repo-in-operator-dir design was REJECTED:
|
||||
# its who/why claim is false under shared-author fleets, and
|
||||
# .gitignore is pattern-based where acceptance (b) demands
|
||||
# fail-closed. (2) CREDENTIAL HARD GATE (acceptance b), FOUR
|
||||
# LAYERS (#964 re-verdict: B2 cases C+D): (i) POLARITY — extras
|
||||
# (WAKE_PREIMAGE_EXTRA) are RECORD-ONLY by default (ledger row +
|
||||
# cause line, NO bytes); byte capture for an extra requires the
|
||||
# path listed in WAKE_PREIMAGE_CAPTURE (colon-separated opt-in);
|
||||
# only the core set (adapter, watch-list) is capture-eligible by
|
||||
# default — a shape list can only refuse the secrets someone
|
||||
# already enumerated, so safety may not rest on one (#964-D).
|
||||
# (ii) PATH DENY evaluated on BOTH the raw and realpath-resolved
|
||||
# candidate forms against BOTH unresolved and resolved
|
||||
# mosaic-home anchors (credentials.json,
|
||||
# tools/_lib/credentials.json, credentials/**, any basename
|
||||
# credentials.json) — a deny list written in unresolved paths
|
||||
# cannot match a path resolved before it arrived (#964-C), and a
|
||||
# symlinked opt-in entry cannot smuggle a denied target (deny
|
||||
# runs FIRST, independent of what the opt-in matched). (iii)
|
||||
# CONTENT DENY on the opt-in path only, defense-in-depth NOT the
|
||||
# safety mechanism: digest.sh's six scrub shapes + a
|
||||
# named-assignment probe (key/token/secret/passw/hmac/credential/
|
||||
# bearer = unbroken value >=16 chars); probe ERROR fails TOWARD
|
||||
# refusal (an error exit is not a negative result); false
|
||||
# positives are acceptable — a false match only withholds byte
|
||||
# capture, never tracking. (iv) size cap WAKE_PREIMAGE_MAX_BYTES
|
||||
# (default 1 MiB). Deny ALWAYS wins over the opt-in. A refused
|
||||
# file still gets its hash/size/mtime row (captured:false +
|
||||
# refused reason) so change TIME survives even when content must
|
||||
# not. (3) FIRST-CLASS CAUSE LINE
|
||||
# (acceptance c): on a change (or deletion — ABSENT is a state,
|
||||
# not an error), one class=actionable entry is enqueued via the
|
||||
# store allocator with locators {kind:preimage, path (§2.1 hard
|
||||
# locator — never quarantined), observed_hash, prev_hash,
|
||||
# preimage:true, reason:preimage-definition-changed}. Both
|
||||
# detector.sh cmd_poll_once and reconcile.sh cmd_reconcile run
|
||||
# the check as a PRE-step, so the cause line lands at a LOWER
|
||||
# observed_seq than the N per-source deltas/enumerations it
|
||||
# explains — "preimage definition changed" reads first, not N
|
||||
# UNACCOUNTED lines. (4) FAIL-LOUD discipline (D2/#955 class):
|
||||
# an unresolvable adapter, unreadable watch-list, corrupt ledger
|
||||
# (REFUSES to compare or re-baseline over corrupt history), failed
|
||||
# object/ledger write, or failed enqueue is a loud non-zero —
|
||||
# never read as "no change"; in both integrations the pass exits
|
||||
# non-zero but source observation still proceeds (no starvation).
|
||||
# An ABSENT/EMPTY ledger with objects/ NON-empty is a loud
|
||||
# non-zero REFUSAL to re-baseline (#964-B11: objects with no
|
||||
# ledger cannot be a first install — history was deleted; the
|
||||
# first-install path must not silently absorb it). First-seen on
|
||||
# a genuinely clean state dir is a SILENT baseline (detector
|
||||
# first-poll idiom).
|
||||
# The installer is UNCHANGED — Gate A auto-enumerates the new
|
||||
# file from the filesystem; the recording site is the runtime
|
||||
# tick, which is where the env-derived preimage set exists.
|
||||
# store.sh/digest.sh/beacon.sh UNCHANGED; watch-list schema
|
||||
# UNTOUCHED ([1,1]). Changed: preimage.sh (new), detector.sh,
|
||||
# reconcile.sh (+ test-wake-preimage.sh P1-P17; P13-P17 are the
|
||||
# #964 re-verdict regression needles: symlinked store, live
|
||||
# secret value, polarity, ledger deletion, allowlist-symlink).
|
||||
component=wake
|
||||
version=0.7.0
|
||||
version=0.6.12
|
||||
|
||||
# Watch-list schema this component consumes, and the INCLUSIVE range of
|
||||
# schema_version values it supports. A wake-watch-list.json whose schema_version
|
||||
@@ -518,19 +372,6 @@ schema_max=1
|
||||
# seed) instead of a bare source error, and LINKS the unit into
|
||||
# the user systemd search path + post-install-validates it
|
||||
# resolves (#913). (W7, #913)
|
||||
# preimage.sh A11 — operator-side PREIMAGE-DEFINITION provenance: derives the
|
||||
# preimage set from the runtime env (resolved adapter file,
|
||||
# watch-list, WAKE_PREIMAGE_EXTRA), appends
|
||||
# {ts,path,sha256,size,mtime,prev} rows to an append-only
|
||||
# ledger, captures bytes content-addressed under
|
||||
# preimage/objects/<sha256>, and enqueues a FIRST-CLASS
|
||||
# "preimage-definition-changed" actionable (path = §2.1 hard
|
||||
# locator) BEFORE the deltas it explains (detector +
|
||||
# reconcile pre-step). Credential HARD GATE: capture is
|
||||
# REFUSED (hash/mtime still recorded) for credential-store
|
||||
# paths, secret-shaped content, and oversized files —
|
||||
# refusal, never redaction. Fail-loud on any infra failure;
|
||||
# a corrupt ledger refuses re-baseline. (#958)
|
||||
# Companion (framework subtree, not under tools/wake/): systemd/user/mosaic-wake.service
|
||||
# — the long-lived detector daemon unit (per-class SLO lives in
|
||||
# the daemon, NOT a systemd interval). (W7)
|
||||
|
||||
@@ -1,571 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# preimage.sh — A11 of the wake canon (#958): durable PROVENANCE for the
|
||||
# OPERATOR-SIDE preimage definition.
|
||||
#
|
||||
# THE GAP THIS CLOSES (#958): every observed_hash in the lane store is
|
||||
# sha256(adapter stdout) — the operator's source adapter (plus the watch-list
|
||||
# and any operator-declared preimage files) IS the preimage definition for
|
||||
# every hash the pipeline ever records. Those files live OUTSIDE the versioned
|
||||
# wake component, so a byte change to them was attributable only through agent
|
||||
# transcripts: provenance thinner than any hash it feeds. A changed preimage
|
||||
# also re-baselines EVERY source at once, which previously surfaced only as N
|
||||
# per-source deltas / UNACCOUNTED lines with no first-class cause.
|
||||
#
|
||||
# WHAT THIS TOOL DOES:
|
||||
# - Derives the PREIMAGE SET generically (no operator paths baked in):
|
||||
# * the resolved file behind WAKE_DETECTOR_SOURCE_CMD (first word),
|
||||
# * the WAKE_WATCH_LIST file,
|
||||
# * operator-declared extras via WAKE_PREIMAGE_EXTRA (colon-separated —
|
||||
# e.g. sink/feed scripts). Extras are RECORD-ONLY by default (see the
|
||||
# credential hard gate below); never list key-material files.
|
||||
# - Records each file's (sha256, size, mtime, ts) as an APPEND-ONLY ledger
|
||||
# row in <state>/preimage/preimage-ledger.jsonl and stores the full bytes
|
||||
# CONTENT-ADDRESSED at <state>/preimage/objects/<sha256> — so a change is
|
||||
# attributable (prior bytes + change time) from durable state alone, with
|
||||
# no transcript required. Acceptance (a) of #958.
|
||||
# - On a CHANGE (not first-seen), `check --enqueue` enqueues ONE first-class
|
||||
# class=actionable entry per changed file via the store's single allocator
|
||||
# (#908), carrying a §2.1 hard locator (`path`), BEFORE the per-source
|
||||
# deltas it explains are observed — so the digest shows the cause line,
|
||||
# not just N re-baselined sources. Acceptance (c).
|
||||
#
|
||||
# CREDENTIAL HARD GATE — acceptance (b): the mechanism must be UNABLE to
|
||||
# capture credential material even by operator error. Layered, every layer
|
||||
# fail-closed toward NOT capturing bytes (the hash/size/mtime row is still
|
||||
# written — change-time attribution survives, content capture does not;
|
||||
# a sha256 discloses nothing about the bytes):
|
||||
# 1. POLARITY (#964 review, case D): byte capture is DENY-BY-DEFAULT for
|
||||
# operator extras. WAKE_PREIMAGE_EXTRA paths are RECORD-ONLY (rows +
|
||||
# first-class change entries, no bytes) unless explicitly listed in
|
||||
# WAKE_PREIMAGE_CAPTURE — a shape list can only refuse the secrets
|
||||
# someone already enumerated, so arbitrary operator-pointed files must
|
||||
# not default to capture. The CORE set (the resolved adapter file, the
|
||||
# watch-list) is capture-eligible: it IS the preimage definition this
|
||||
# tool exists to snapshot, and the deny layers below still apply to it.
|
||||
# 2. PATH deny: the mosaic credential store locations
|
||||
# (<mosaic-home>/credentials.json, <mosaic-home>/tools/_lib/
|
||||
# credentials.json, anything under <mosaic-home>/credentials/, and any
|
||||
# file literally named credentials.json) are refused. Evaluated on BOTH
|
||||
# the operator-supplied form AND the symlink-resolved form, against BOTH
|
||||
# the unresolved and resolved forms of the mosaic-home anchor — a deny
|
||||
# list written only in unresolved paths cannot match a path that was
|
||||
# resolved before it arrived (#964 review, case C).
|
||||
# 3. CONTENT deny: a candidate whose bytes match the well-known secret-token
|
||||
# shapes (same conservative set digest.sh redacts: PAT/Slack/AKIA/JWT/PEM)
|
||||
# OR a named-assignment secret shape (key/token/secret/passw/hmac/
|
||||
# credential/bearer = long unbroken value — catches prefix-less
|
||||
# high-entropy keys, e.g. an HMAC key in an env file) is refused —
|
||||
# refusal, not redaction: a redacted preimage would be a false witness,
|
||||
# and secret bytes must never land in the object store. Deliberately
|
||||
# conservative toward REFUSAL: a false match only withholds byte capture,
|
||||
# never tracking.
|
||||
# Plus a size cap (WAKE_PREIMAGE_MAX_BYTES, default 1 MiB) so a stray extra
|
||||
# pointing at a large binary cannot turn provenance into data hoovering.
|
||||
# Deny ALWAYS wins over the WAKE_PREIMAGE_CAPTURE opt-in.
|
||||
#
|
||||
# FAIL-LOUD DISCIPLINE (the D2/#955 class, both layers): an infrastructure
|
||||
# failure of THIS tool (unresolvable adapter, unreadable/corrupt ledger,
|
||||
# failed durable write, failed enqueue) exits NON-ZERO and is never read as
|
||||
# "no change". A corrupt ledger REFUSES to compare (and to re-baseline):
|
||||
# silently restarting history would erase the very attribution this exists
|
||||
# to provide. Likewise (#964 review, B11) an ABSENT/EMPTY ledger while
|
||||
# objects/ still holds prior captures is DELETED HISTORY, not a first
|
||||
# install — it refuses loudly instead of re-baselining, because a silent
|
||||
# restart would absorb the next real change as first-seen.
|
||||
#
|
||||
# Operator-agnostic (framework firewall): all state via XDG/env; the deny
|
||||
# list names only framework-defined credential locations, no operator hosts/
|
||||
# names/secrets. This tool never prints file CONTENT to any stream.
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./_wake-common.sh disable=SC1091
|
||||
. "$SCRIPT_DIR/_wake-common.sh"
|
||||
|
||||
STATE_DIR="$(wake_state_dir)"
|
||||
STORE_SH="$SCRIPT_DIR/store.sh"
|
||||
PRE_DIR="$STATE_DIR/preimage"
|
||||
LEDGER="$PRE_DIR/preimage-ledger.jsonl"
|
||||
OBJECTS="$PRE_DIR/objects"
|
||||
|
||||
_need_jq() {
|
||||
command -v jq >/dev/null 2>&1 || {
|
||||
echo "preimage.sh: jq is required" >&2
|
||||
exit 3
|
||||
}
|
||||
}
|
||||
|
||||
# _hash_stdin — sha256 of stdin, first field only (portable, same fallback
|
||||
# chain as detector.sh).
|
||||
_hash_stdin() {
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum | awk '{print $1}'
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
shasum -a 256 | awk '{print $1}'
|
||||
elif command -v openssl >/dev/null 2>&1; then
|
||||
openssl dgst -sha256 | awk '{print $NF}'
|
||||
else
|
||||
echo "preimage.sh: no sha256 tool (sha256sum/shasum/openssl) available" >&2
|
||||
exit 3
|
||||
fi
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
Usage: preimage.sh <command>
|
||||
|
||||
Commands:
|
||||
check [--enqueue] Compare every preimage-set file against the ledger head.
|
||||
A changed file gets a new ledger row (+ captured bytes
|
||||
when capture-eligible and not denied — see Environment);
|
||||
with --enqueue each change (not first-seen) also enqueues
|
||||
ONE first-class class=actionable store entry (hard
|
||||
locator: path). First-seen files baseline SILENTLY (row,
|
||||
no enqueue — same idiom as the detector's first-seen).
|
||||
Exit 0 whether or not changes were found; non-zero ONLY
|
||||
on infrastructure failure (never read as "no change").
|
||||
record Alias of `check` without enqueue (baseline/refresh).
|
||||
set Print the derived preimage set, one resolved path per
|
||||
line (diagnostic).
|
||||
status Print the ledger head row for every tracked path.
|
||||
|
||||
Environment:
|
||||
WAKE_DETECTOR_SOURCE_CMD Adapter command; its resolved file joins the set.
|
||||
WAKE_WATCH_LIST Watch-list path; joins the set when set.
|
||||
WAKE_PREIMAGE_EXTRA Colon-separated additional operator preimage files
|
||||
(e.g. sink/feed scripts). Extras are RECORD-ONLY
|
||||
by default: changes get a hash/size/mtime row and
|
||||
a first-class change entry, but their BYTES are
|
||||
never captured unless the path is also listed in
|
||||
WAKE_PREIMAGE_CAPTURE. A listed path that does not
|
||||
exist is tracked as ABSENT (deletion of a preimage
|
||||
file is a change, not an error).
|
||||
WAKE_PREIMAGE_CAPTURE Colon-separated allowlist of extras whose bytes
|
||||
MAY be captured. The credential deny rules ALWAYS
|
||||
win over this list. NEVER list files that can hold
|
||||
key material (env files with keys/HMACs, credential
|
||||
stores): the deny gate is a backstop, not a
|
||||
license.
|
||||
WAKE_PREIMAGE_MAX_BYTES Byte-capture cap (default 1048576). Larger files:
|
||||
hash/size/mtime recorded, bytes refused.
|
||||
WAKE_STATE_HOME/WAKE_AGENT store namespace (see store.sh).
|
||||
EOF
|
||||
}
|
||||
|
||||
# --- preimage-set derivation (generic; no operator paths baked in) ----------
|
||||
|
||||
# _realpath_or_self PATH — resolved form when resolvable, the input otherwise.
|
||||
_realpath_or_self() {
|
||||
local p="$1"
|
||||
if command -v realpath >/dev/null 2>&1; then
|
||||
realpath -- "$p" 2>/dev/null || printf '%s' "$p"
|
||||
else
|
||||
printf '%s' "$p"
|
||||
fi
|
||||
}
|
||||
|
||||
# _resolve_cmd_file CMD — resolve the FIRST WORD of an adapter command string
|
||||
# to a real file; prints `raw<TAB>resolved`. Non-zero (loud) if it cannot be
|
||||
# resolved: an adapter the detector will invoke but provenance cannot see is
|
||||
# an infrastructure failure, not a smaller set. BOTH forms are kept: the deny
|
||||
# gate must see the pre-resolution form too (#964 review, case C).
|
||||
_resolve_cmd_file() {
|
||||
local cmd="$1" word path
|
||||
# shellcheck disable=SC2086
|
||||
set -- $cmd
|
||||
word="${1:-}"
|
||||
[ -n "$word" ] || return 1
|
||||
if [ -f "$word" ]; then
|
||||
path="$word"
|
||||
else
|
||||
path="$(command -v -- "$word" 2>/dev/null)" || return 1
|
||||
[ -f "$path" ] || return 1
|
||||
fi
|
||||
# realpath so the ledger keys on the actual file, not a symlink alias.
|
||||
printf '%s\t%s' "$path" "$(_realpath_or_self "$path")"
|
||||
}
|
||||
|
||||
# _preimage_set — print the derived set, one member per line as
|
||||
# `origin<TAB>raw<TAB>resolved` (origin: core|extra). BOTH path forms travel
|
||||
# with every member so the deny gate and the capture allowlist are evaluated
|
||||
# on the SAME candidate in BOTH its forms — resolving before denying is the
|
||||
# #964 case-C ordering defect. Paths from WAKE_PREIMAGE_EXTRA are printed
|
||||
# EVEN IF ABSENT (deletion is a tracked state); the adapter and watch-list
|
||||
# must resolve (loud failure otherwise — see _resolve_cmd_file rationale).
|
||||
_preimage_set() {
|
||||
local failed=0
|
||||
if [ -n "${WAKE_DETECTOR_SOURCE_CMD:-}" ]; then
|
||||
local af
|
||||
if af="$(_resolve_cmd_file "$WAKE_DETECTOR_SOURCE_CMD")"; then
|
||||
printf 'core\t%s\n' "$af"
|
||||
else
|
||||
echo "preimage.sh: FAIL LOUD — WAKE_DETECTOR_SOURCE_CMD ('$WAKE_DETECTOR_SOURCE_CMD') does not resolve to a file; the preimage definition cannot be observed (NOT treated as 'no change')." >&2
|
||||
failed=1
|
||||
fi
|
||||
fi
|
||||
if [ -n "${WAKE_WATCH_LIST:-}" ]; then
|
||||
if [ -f "$WAKE_WATCH_LIST" ]; then
|
||||
printf 'core\t%s\t%s\n' "$WAKE_WATCH_LIST" "$(_realpath_or_self "$WAKE_WATCH_LIST")"
|
||||
else
|
||||
echo "preimage.sh: FAIL LOUD — WAKE_WATCH_LIST ('$WAKE_WATCH_LIST') is not a file; the preimage definition cannot be observed." >&2
|
||||
failed=1
|
||||
fi
|
||||
fi
|
||||
if [ -n "${WAKE_PREIMAGE_EXTRA:-}" ]; then
|
||||
local IFS=':' p
|
||||
for p in $WAKE_PREIMAGE_EXTRA; do
|
||||
[ -n "$p" ] || continue
|
||||
# Extras are tracked even when absent (ABSENT is a state). Resolve the
|
||||
# realpath only when the file exists; otherwise track the literal path.
|
||||
if [ -e "$p" ]; then
|
||||
printf 'extra\t%s\t%s\n' "$p" "$(_realpath_or_self "$p")"
|
||||
else
|
||||
printf 'extra\t%s\t%s\n' "$p" "$p"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
return "$failed"
|
||||
}
|
||||
|
||||
# --- credential hard gate (acceptance (b)) ----------------------------------
|
||||
|
||||
# _deny_path RAW RESOLVED — 0 (deny) iff EITHER form of the candidate names a
|
||||
# known credential-store location. Framework-defined locations only
|
||||
# (firewall): the mosaic credential store, the tools/_lib credential file the
|
||||
# framework-manifest itself carves out of tools/**, the credentials/ operator
|
||||
# subtree, and any file literally named credentials.json.
|
||||
#
|
||||
# Evaluated on BOTH the operator-supplied (raw) form and the symlink-resolved
|
||||
# form, against BOTH the unresolved and resolved forms of the mosaic-home
|
||||
# anchor: a deny list written only in unresolved paths cannot match a path
|
||||
# that was resolved before it arrived (#964 review, case C — a symlink whose
|
||||
# target was renamed slipped every path rule because the candidate had
|
||||
# already been realpath'd but the anchors never were).
|
||||
_deny_path() {
|
||||
local raw="$1" resolved="$2"
|
||||
local home="${MOSAIC_HOME:-$HOME/.config/mosaic}" rhome p a
|
||||
rhome="$(_realpath_or_self "$home")"
|
||||
for p in "$raw" "$resolved"; do
|
||||
[ -n "$p" ] || continue
|
||||
for a in "$home" "$rhome"; do
|
||||
case "$p" in
|
||||
"$a/credentials.json") return 0 ;;
|
||||
"$a/tools/_lib/credentials.json") return 0 ;;
|
||||
"$a/credentials/"*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
case "$(basename -- "$p")" in
|
||||
credentials.json) return 0 ;;
|
||||
esac
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# _deny_content FILE — 0 (deny) iff FILE's bytes look secret-shaped.
|
||||
# Two probes: (a) the well-known vendor-token shapes digest.sh redacts
|
||||
# (PAT / Slack / AKIA / JWT / PEM — conservative: a 40-hex git sha never
|
||||
# matches); (b) a named-assignment shape (key/token/secret/passw/hmac/
|
||||
# credential/bearer = long unbroken value) that catches prefix-less
|
||||
# high-entropy keys, e.g. an HMAC key in an env file (#964 review, case D).
|
||||
#
|
||||
# (b) is defense-in-depth for the OPT-IN path only, NOT the thing that keeps
|
||||
# case D safe — polarity does that (extras are record-only unless allowlisted;
|
||||
# a shape list can only refuse the secrets someone already enumerated). It is
|
||||
# deliberately conservative toward REFUSAL: a false match (a checksum in a
|
||||
# config, a path that happens to be long and unbroken) only withholds byte
|
||||
# capture — the hash/size/mtime row and change entry still land. A variable
|
||||
# REFERENCE (token="$GITEA_TOKEN") never matches: `$` is not in the value
|
||||
# class — only literal secret values do.
|
||||
#
|
||||
# FAILS TOWARD REFUSAL: if a probe itself errors (unreadable file, grep
|
||||
# failure — rc >= 2), the answer is DENY, not capture. An error exit is not a
|
||||
# negative result.
|
||||
_deny_content() {
|
||||
local rc
|
||||
LC_ALL=C grep -Eq \
|
||||
-e 'gh[pousr]_[A-Za-z0-9]{16,}' \
|
||||
-e 'github_pat_[A-Za-z0-9_]{16,}' \
|
||||
-e 'xox[baprs]-[A-Za-z0-9-]{10,}' \
|
||||
-e 'AKIA[0-9A-Z]{16}' \
|
||||
-e 'eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}' \
|
||||
-e '-----BEGIN[A-Z ]*PRIVATE KEY-----' \
|
||||
-- "$1" 2>/dev/null
|
||||
rc=$?
|
||||
[ "$rc" -eq 1 ] || return 0
|
||||
LC_ALL=C grep -Eiq \
|
||||
-e "[A-Za-z0-9_]*(key|token|secret|passw|hmac|credential|bearer)[A-Za-z0-9_]*[[:space:]]*[=:][[:space:]]*[\"']?[A-Za-z0-9+/=_-]{16,}" \
|
||||
-- "$1" 2>/dev/null
|
||||
rc=$?
|
||||
[ "$rc" -eq 1 ] || return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
# _capture_allowed ORIGIN RAW RESOLVED — 0 iff byte capture may even be
|
||||
# ATTEMPTED for this member (the deny gate still runs after, and wins).
|
||||
# POLARITY (#964 review, case D): core members (the adapter file, the
|
||||
# watch-list) are capture-eligible — they ARE the preimage definition this
|
||||
# tool exists to snapshot (acceptance (a)). Extras are RECORD-ONLY unless
|
||||
# listed in WAKE_PREIMAGE_CAPTURE.
|
||||
#
|
||||
# The allowlist is matched with the SAME both-forms discipline as the deny
|
||||
# list (entry raw/resolved vs candidate raw/resolved): two lists keyed on
|
||||
# different strings would let a symlink alias slip between them. "Deny wins
|
||||
# over opt-in" is a precedence rule, not a guarantee both lists see the same
|
||||
# path — the guarantee comes from _check_one running _deny_path on both forms
|
||||
# FIRST, independent of anything matched here.
|
||||
_capture_allowed() {
|
||||
local origin="$1" raw="$2" resolved="$3"
|
||||
[ "$origin" = "core" ] && return 0
|
||||
[ -n "${WAKE_PREIMAGE_CAPTURE:-}" ] || return 1
|
||||
local IFS=':' e er
|
||||
for e in $WAKE_PREIMAGE_CAPTURE; do
|
||||
[ -n "$e" ] || continue
|
||||
er="$e"
|
||||
[ -e "$e" ] && er="$(_realpath_or_self "$e")"
|
||||
if [ "$e" = "$raw" ] || [ "$e" = "$resolved" ] ||
|
||||
[ "$er" = "$raw" ] || [ "$er" = "$resolved" ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# --- ledger primitives ------------------------------------------------------
|
||||
|
||||
# _ledger_head PATH — print the LAST ledger row for PATH (empty if none).
|
||||
# Non-zero (loud) if the ledger exists but is unparseable: a corrupt ledger
|
||||
# must REFUSE to compare, never silently re-baseline (that would erase the
|
||||
# attribution history this tool exists to keep).
|
||||
_ledger_head() {
|
||||
local path="$1"
|
||||
[ -f "$LEDGER" ] || return 0
|
||||
if [ -s "$LEDGER" ] && ! jq -cn 'inputs' <"$LEDGER" >/dev/null 2>&1; then
|
||||
echo "preimage.sh: FAIL LOUD — preimage ledger $LEDGER is unparseable; REFUSING to compare or re-baseline over corrupt history. Investigate/restore the ledger." >&2
|
||||
return 1
|
||||
fi
|
||||
jq -c --arg p "$path" 'select(.path == $p)' "$LEDGER" 2>/dev/null | tail -n 1
|
||||
}
|
||||
|
||||
# _ledger_append ROW_JSON — append one row atomically (read + append + rename;
|
||||
# rows are small and the writer is serialized by the preimage lock).
|
||||
_ledger_append() {
|
||||
local row="$1"
|
||||
{
|
||||
[ -f "$LEDGER" ] && cat "$LEDGER"
|
||||
printf '%s\n' "$row"
|
||||
} | _atomic_write "$LEDGER"
|
||||
}
|
||||
|
||||
# --- the check itself -------------------------------------------------------
|
||||
|
||||
# _check_one ORIGIN RAW PATH ENQUEUE — compare PATH (the resolved form; the
|
||||
# ledger keys on it) to its ledger head; on change record (+bytes only if
|
||||
# capture-eligible and not denied) and optionally enqueue. Prints nothing on
|
||||
# no-change. Returns: 0 ok (changed or not), 1 infrastructure failure.
|
||||
_check_one() {
|
||||
local origin="$1" raw="$2" path="$3" enqueue="$4"
|
||||
local cur_sha size mtime denied="" refused=""
|
||||
|
||||
if [ -f "$path" ]; then
|
||||
cur_sha="$(_hash_stdin <"$path")" || return 1
|
||||
[ -n "$cur_sha" ] || {
|
||||
echo "preimage.sh: FAIL LOUD — could not hash $path" >&2
|
||||
return 1
|
||||
}
|
||||
size="$(wc -c <"$path" | tr -d '[:space:]')"
|
||||
# Portable mtime (GNU stat -c / BSD stat -f).
|
||||
mtime="$(stat -c %Y -- "$path" 2>/dev/null || stat -f %m -- "$path" 2>/dev/null || echo 0)"
|
||||
local cap="${WAKE_PREIMAGE_MAX_BYTES:-1048576}"
|
||||
case "$cap" in '' | *[!0-9]*) cap=1048576 ;; esac
|
||||
# Gate order matters: path deny FIRST, on BOTH forms, INDEPENDENT of the
|
||||
# capture allowlist — so an allowlisted symlink whose target is denied
|
||||
# refuses no matter what string the allowlist matched (a known credential
|
||||
# store is refused without a content probe; the hash read above is
|
||||
# deliberate: a sha256 discloses nothing about the bytes). Then polarity
|
||||
# (record-only extras never reach the content probe — case D must be safe
|
||||
# WITHOUT the shape list), then size cap (never content-grep an oversized
|
||||
# file), content shape last.
|
||||
if _deny_path "$raw" "$path"; then
|
||||
denied="credential-store path (deny list)"
|
||||
elif ! _capture_allowed "$origin" "$raw" "$path"; then
|
||||
denied="extra is record-only by default (byte capture requires WAKE_PREIMAGE_CAPTURE; deny rules still win)"
|
||||
elif [ "$size" -gt "$cap" ]; then
|
||||
denied="exceeds WAKE_PREIMAGE_MAX_BYTES ($size > $cap)"
|
||||
elif _deny_content "$path"; then
|
||||
denied="secret-shaped content"
|
||||
fi
|
||||
else
|
||||
# ABSENT is a state (deletion of a preimage file is a change to record).
|
||||
cur_sha="ABSENT"
|
||||
size=0
|
||||
mtime=0
|
||||
fi
|
||||
|
||||
local head prev_sha=""
|
||||
head="$(_ledger_head "$path")" || return 1
|
||||
[ -n "$head" ] && prev_sha="$(jq -r '.sha256 // ""' <<<"$head")"
|
||||
|
||||
if [ "$cur_sha" = "$prev_sha" ]; then
|
||||
return 0 # unchanged — no row, no enqueue (idempotent)
|
||||
fi
|
||||
|
||||
# --- capture bytes (content-addressed) unless the hard gate refuses -------
|
||||
local captured=true
|
||||
if [ "$cur_sha" != "ABSENT" ]; then
|
||||
if [ -n "$denied" ]; then
|
||||
captured=false
|
||||
refused="$denied"
|
||||
echo "preimage.sh: byte capture WITHHELD for $path ($denied) — hash/size/mtime recorded, content NOT stored (#958)." >&2
|
||||
else
|
||||
mkdir -p "$OBJECTS" || return 1
|
||||
if [ ! -f "$OBJECTS/$cur_sha" ]; then
|
||||
if ! _atomic_write "$OBJECTS/$cur_sha" <"$path"; then
|
||||
echo "preimage.sh: FAIL LOUD — durable object write failed for $path ($OBJECTS/$cur_sha); NO ledger row recorded (a row whose bytes were never durably kept would be a false witness)." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
local ts row
|
||||
ts="$(date +%s)"
|
||||
row="$(jq -cn \
|
||||
--arg path "$path" \
|
||||
--arg sha "$cur_sha" \
|
||||
--arg prev "$prev_sha" \
|
||||
--argjson size "$size" \
|
||||
--argjson mtime "$mtime" \
|
||||
--argjson ts "$ts" \
|
||||
--argjson captured "$captured" \
|
||||
--arg refused "$refused" \
|
||||
'{ts:$ts, path:$path, sha256:$sha, size:$size, mtime:$mtime, captured:$captured}
|
||||
+ (if $prev != "" then {prev:$prev} else {} end)
|
||||
+ (if $refused != "" then {refused:$refused} else {} end)')" || return 1
|
||||
if ! _ledger_append "$row"; then
|
||||
echo "preimage.sh: FAIL LOUD — durable ledger append failed ($LEDGER)" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$head" ]; then
|
||||
# First-seen: baseline SILENTLY (row only, no enqueue) — the same idiom as
|
||||
# the detector's first-seen source baseline.
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "preimage.sh: preimage definition CHANGED — $path ($prev_sha -> $cur_sha); prior bytes: $OBJECTS/$prev_sha" >&2
|
||||
|
||||
if [ "$enqueue" = "1" ]; then
|
||||
# First-class cause line (acceptance (c)): ONE class=actionable entry per
|
||||
# changed preimage file, allocated by the store's single allocator (#908).
|
||||
# `path` is a §2.1 hard locator, so the digest renders it — never
|
||||
# quarantined. Callers run this BEFORE polling sources, so this seq is
|
||||
# LOWER than the per-source deltas the change explains.
|
||||
local locators
|
||||
locators="$(jq -cn \
|
||||
--arg path "$path" \
|
||||
--arg sha "$cur_sha" \
|
||||
--arg prev "$prev_sha" \
|
||||
'{kind:"preimage", id:$path, path:$path, observed_hash:$sha, prev_hash:$prev,
|
||||
preimage:true, reason:"preimage-definition-changed"}')"
|
||||
if ! "$STORE_SH" enqueue --class actionable --locators "$locators" --emit-ts "$(date +%s)" >/dev/null; then
|
||||
echo "preimage.sh: FAIL LOUD — store enqueue of the preimage-change entry FAILED for $path (the change IS recorded in the ledger; the first-class wake line is NOT — treat as infrastructure failure)." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
cmd_check() {
|
||||
local enqueue=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--enqueue)
|
||||
enqueue=1
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "preimage.sh check: unknown option '$1'" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
_need_jq
|
||||
|
||||
local set_list
|
||||
if ! set_list="$(_preimage_set)"; then
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$set_list" ]; then
|
||||
# Nothing declared (no adapter/watch-list/extras in env) — an empty set is
|
||||
# a no-op, not an error: standalone invocations outside a wake runtime
|
||||
# must not fail loud for lacking one.
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$PRE_DIR" || exit 1
|
||||
# Serialize concurrent checks (detector tick vs reconcile vs manual) — the
|
||||
# ledger append is read+rewrite, so two writers must not interleave.
|
||||
if ! _wake_lock_acquire "$PRE_DIR/preimage.lock"; then
|
||||
echo "preimage.sh: FAIL LOUD — cannot acquire preimage lock" >&2
|
||||
exit 1
|
||||
fi
|
||||
# #964 review (B11): an ABSENT/EMPTY ledger while objects/ still holds
|
||||
# prior captures is DELETED HISTORY, not a first install — the asymmetry is
|
||||
# locally detectable and is the whole detection. Re-baselining here would
|
||||
# silently absorb the next real change as first-seen (the exact erasure
|
||||
# this tool exists to prevent). ABSENT is not CORRUPT: it must not take the
|
||||
# first-install path either.
|
||||
if [ ! -s "$LEDGER" ] && [ -d "$OBJECTS" ] && [ -n "$(ls -A -- "$OBJECTS" 2>/dev/null)" ]; then
|
||||
echo "preimage.sh: FAIL LOUD — preimage ledger $LEDGER is ABSENT/EMPTY but $OBJECTS still holds prior objects: history was deleted; REFUSING to re-baseline over it (a silent restart would absorb the next real change as first-seen). Restore the ledger, or move objects/ aside explicitly after investigation." >&2
|
||||
_wake_lock_release
|
||||
exit 1
|
||||
fi
|
||||
local failed=0 origin raw resolved
|
||||
while IFS=$'\t' read -r origin raw resolved; do
|
||||
[ -n "$resolved" ] || continue
|
||||
_check_one "$origin" "$raw" "$resolved" "$enqueue" || failed=1
|
||||
done <<EOF
|
||||
$set_list
|
||||
EOF
|
||||
_wake_lock_release
|
||||
[ "$failed" -eq 0 ] || exit 1
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
_need_jq
|
||||
[ -f "$LEDGER" ] || {
|
||||
echo "preimage.sh: no ledger yet ($LEDGER)" >&2
|
||||
return 0
|
||||
}
|
||||
# Head row per path (last wins).
|
||||
jq -cs 'group_by(.path) | map(last) | .[]' "$LEDGER"
|
||||
}
|
||||
|
||||
cmd_set() {
|
||||
local s
|
||||
s="$(_preimage_set)" || exit 1
|
||||
[ -n "$s" ] && printf '%s\n' "$s" | cut -f3
|
||||
}
|
||||
|
||||
main() {
|
||||
[ $# -ge 1 ] || {
|
||||
usage
|
||||
exit 2
|
||||
}
|
||||
local cmd="$1"
|
||||
shift
|
||||
case "$cmd" in
|
||||
check) cmd_check "$@" ;;
|
||||
record) cmd_check ;;
|
||||
set) cmd_set "$@" ;;
|
||||
status) cmd_status "$@" ;;
|
||||
-h | --help | help) usage ;;
|
||||
*)
|
||||
echo "preimage.sh: unknown command '$cmd'" >&2
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -440,17 +440,6 @@ cmd_reconcile() {
|
||||
: "$allow_enumerate"
|
||||
|
||||
local pairs kind id failed=0 unaccounted=0 enumerated=0
|
||||
|
||||
# #958 preimage provenance: same pre-step as the detector's poll tick, for
|
||||
# the path where the preimage changed while the detector was down — without
|
||||
# it, a changed adapter surfaces here only as N UNACCOUNTED enumerations
|
||||
# with no first-class cause line. Run BEFORE observing any source so the
|
||||
# cause entry's observed_seq precedes the enumerations it explains. Loud
|
||||
# infrastructure failure marks the reconcile failed but does not stop it.
|
||||
if ! "$SCRIPT_DIR/preimage.sh" check --enqueue; then
|
||||
echo "reconcile.sh: FAIL LOUD — preimage provenance check failed (see preimage.sh above); reconcile continues but exits non-zero." >&2
|
||||
failed=1
|
||||
fi
|
||||
pairs="$(printf '%s' "$json" | jq -r '[ .watches[].sources[] | "\(.kind)\t\(.id)" ] | unique | .[]')"
|
||||
if [ -z "$pairs" ]; then
|
||||
echo "reconcile.sh: watch-list declares no sources under watches[].sources[]" >&2
|
||||
|
||||
@@ -77,42 +77,11 @@ Commands:
|
||||
the actual paste is out of scope.
|
||||
If --require-idle-cmd is given and it
|
||||
exits non-zero, emit nothing (not idle).
|
||||
consume --upto N [--force-past-quarantine]
|
||||
Advance consumed_seq over the contiguous
|
||||
consume --upto N Advance consumed_seq over the contiguous
|
||||
gapless prefix <=N; drop consumed
|
||||
entries. Rejects a gap (cannot ack N
|
||||
while N-1 is unconsumed). Cumulative &
|
||||
idempotent. REFUSES to advance past a
|
||||
QUARANTINED seq (#946): a quarantined
|
||||
entry was dead-lettered at render and
|
||||
never delivered in any digest, so an
|
||||
ordinary ack may not record it consumed.
|
||||
--force-past-quarantine is the ONLY way
|
||||
past — loud per stepped-over seq, and
|
||||
even then NO consumed-hash witness is
|
||||
recorded for the quarantined entry.
|
||||
quarantine-sync REPLACE the store-owned quarantined.set
|
||||
with the observed_seqs read from stdin
|
||||
(one per line; empty input CLEARS).
|
||||
Called by digest.sh after each
|
||||
authoritative store render — the set is
|
||||
re-DERIVED per render, never accumulated,
|
||||
so a fixed locator gate self-heals the
|
||||
consume clamp (#944 recovery).
|
||||
quarantine-audit [--repair] Report consumed-hashes rows that are
|
||||
PROVABLY FALSE: the row matches a
|
||||
dead-letter ledger entry on
|
||||
(kind,id,observed_seq,observed_hash) at
|
||||
or below consumed_seq — i.e. the recorded
|
||||
consumption was of a quarantined,
|
||||
never-delivered entry (#946 Finding A).
|
||||
Report-only by default (exit 1 when any
|
||||
found); --repair removes exactly those
|
||||
rows (atomic, loud). The dead-letter
|
||||
ledger itself is NEVER modified (it is
|
||||
history). Rows whose dead-letter evidence
|
||||
was pruned are NOT provable and are
|
||||
never touched.
|
||||
idempotent.
|
||||
cursors Print observed_seq / consumed_seq / depth.
|
||||
|
||||
Environment:
|
||||
@@ -385,20 +354,10 @@ cmd_drain() {
|
||||
# (the reconciler re-enumerates the consumed state once — no lost obligation),
|
||||
# never a failed consume.
|
||||
_record_last_consumed() {
|
||||
local upto="$1" existing new_records merged qjson
|
||||
local upto="$1" existing new_records merged
|
||||
[ -f "$STATE_DIR/pending.jsonl" ] || return 0
|
||||
# #946: seqs in quarantined.set are EXCLUDED from the record — the trust
|
||||
# boundary above says "existence implies durably enqueued+CONSUMED", but a
|
||||
# quarantined entry was dead-lettered at render and NEVER delivered, so a row
|
||||
# for it would witness a delivery that never happened. This holds even on the
|
||||
# FORCED step-over path: the reconciler re-enumerating the state once is
|
||||
# safe-but-noisy; a false witness silences it forever.
|
||||
qjson="$(jq -nR -c '[inputs | select(length > 0) | tonumber? // empty]' "$STATE_DIR/quarantined.set" 2>/dev/null || true)"
|
||||
[ -n "$qjson" ] || qjson='[]'
|
||||
new_records="$(jq -c --argjson upto "$upto" --argjson quarantined "$qjson" '
|
||||
(.observed_seq // -1) as $seq
|
||||
| select($seq <= $upto)
|
||||
| select(($quarantined | index($seq)) == null)
|
||||
new_records="$(jq -c --argjson upto "$upto" '
|
||||
select((.observed_seq // -1) <= $upto)
|
||||
| select((.locators.kind // "") != "" and (.locators.id // "") != "" and (.locators.observed_hash // "") != "")
|
||||
| {kind:.locators.kind, id:.locators.id, observed_hash:.locators.observed_hash, observed_seq:.observed_seq}
|
||||
' "$STATE_DIR/pending.jsonl" 2>/dev/null || true)"
|
||||
@@ -413,17 +372,13 @@ _record_last_consumed() {
|
||||
}
|
||||
|
||||
cmd_consume() {
|
||||
local upto='' force=0
|
||||
local upto=''
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--upto)
|
||||
upto="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--force-past-quarantine)
|
||||
force=1
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "store.sh consume: unknown option '$1'" >&2
|
||||
exit 2
|
||||
@@ -471,33 +426,6 @@ cmd_consume() {
|
||||
k=$((k + 1))
|
||||
done
|
||||
|
||||
# --- #946 quarantine CLAMP -------------------------------------------------
|
||||
# A quarantined seq was DEAD-LETTERED at render (no §2.1 hard locator): it was
|
||||
# never delivered in any digest, so advancing consumed_seq past it would record
|
||||
# consumption of an entry the consumer has never seen. The ordinary path
|
||||
# REFUSES; --force-past-quarantine is the ONLY way past, and it is loud per
|
||||
# stepped-over seq. digest.sh re-derives the set at each authoritative store
|
||||
# render (quarantine-sync REPLACE), so a fixed locator gate self-heals this
|
||||
# clamp without operator action.
|
||||
local qfile="$STATE_DIR/quarantined.set" blocked='' q
|
||||
if [ -s "$qfile" ]; then
|
||||
while IFS= read -r q; do
|
||||
case "$q" in '' | *[!0-9]*) continue ;; esac
|
||||
if [ "$q" -gt "$consumed" ] && [ "$q" -le "$upto" ]; then
|
||||
blocked="$blocked $q"
|
||||
fi
|
||||
done <"$qfile"
|
||||
fi
|
||||
if [ -n "$blocked" ]; then
|
||||
if [ "$force" -eq 0 ]; then
|
||||
echo "store.sh consume: REFUSED (#946 quarantine clamp) — quarantined seq(s):$blocked inside (consumed_seq=$consumed, upto=$upto] were dead-lettered at render and NEVER delivered in any digest. Advancing past them would record consumption of entries the consumer has never seen. Disposition them (see $STATE_DIR/dead-letter.jsonl) or step over EXPLICITLY with --force-past-quarantine." >&2
|
||||
exit 1
|
||||
fi
|
||||
for q in $blocked; do
|
||||
echo "store.sh consume: FORCED PAST QUARANTINE (#946) — stepping consumed_seq over quarantined seq $q (dead-lettered, NEVER delivered). No consumed-hash witness is recorded for it; the obligation stays visible ONLY in $STATE_DIR/dead-letter.jsonl." >&2
|
||||
done
|
||||
fi
|
||||
|
||||
# #932: record the last-consumed observed_hash per (kind,id) BEFORE the pending
|
||||
# prefix is dropped (this reads the entries about to be truncated), so the
|
||||
# reconciler can recognise an already-consumed state as ACCOUNTED instead of
|
||||
@@ -511,137 +439,9 @@ cmd_consume() {
|
||||
awk -v c="$upto" 'NF && $1+0 > c' "$STATE_DIR/observed.set" 2>/dev/null |
|
||||
_atomic_write "$STATE_DIR/observed.set" || true
|
||||
printf '%s' "$upto" | _atomic_write "$STATE_DIR/consumed_seq"
|
||||
# #946: on a forced step-over, PRUNE the stepped-over seqs from quarantined.set
|
||||
# (they are inside the consumed prefix now; a stale entry would re-refuse the
|
||||
# next consume forever). Mirrors the observed.set prune idiom above.
|
||||
if [ -n "$blocked" ]; then
|
||||
awk -v c="$upto" 'NF && $1+0 > c' "$qfile" 2>/dev/null |
|
||||
_atomic_write "$qfile" || true
|
||||
fi
|
||||
echo "$upto"
|
||||
}
|
||||
|
||||
# cmd_quarantine_sync — #946: REPLACE the store-owned quarantined.set with the
|
||||
# observed_seqs read from stdin (one per line). Called by digest.sh after each
|
||||
# AUTHORITATIVE full-set render (src=store): the set is re-DERIVED per render,
|
||||
# never accumulated, so a fixed locator gate self-heals the consume clamp (the
|
||||
# #944 recovery case — a cumulative-forever set would keep refusing acks on
|
||||
# entries that now render). Empty input CLEARS the set. Foreign-data renders
|
||||
# (--from-file/--stdin) never call this. Atomic write; invalid input is refused
|
||||
# loudly with the set left untouched.
|
||||
cmd_quarantine_sync() {
|
||||
[ $# -eq 0 ] || {
|
||||
echo "store.sh quarantine-sync: takes no options (observed_seqs on stdin, one per line)" >&2
|
||||
exit 2
|
||||
}
|
||||
local seq list=''
|
||||
while IFS= read -r seq; do
|
||||
[ -n "$seq" ] || continue
|
||||
case "$seq" in
|
||||
*[!0-9]*)
|
||||
echo "store.sh quarantine-sync: invalid observed_seq '$seq' — one non-negative integer per line; set left untouched" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
list="$list$seq"$'\n'
|
||||
done
|
||||
_wake_init_dir "$STATE_DIR"
|
||||
if ! { printf '%s' "$list" | grep -v '^[[:space:]]*$' || true; } | sort -n | uniq | _atomic_write "$STATE_DIR/quarantined.set"; then
|
||||
echo "store.sh quarantine-sync: quarantined.set write FAILED — the consume clamp may be stale for this lane" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# cmd_quarantine_audit — #946 Finding A: the pre-#946 consume recorded
|
||||
# consumed-hash rows for QUARANTINED (never-delivered) entries — false
|
||||
# witnesses that silence the reconciler for keys whose only "consumption" was a
|
||||
# dead-lettered entry (live: safe by population only where the key re-emitted
|
||||
# and a later seq won the per-key max_by merge; keys that never recurred keep
|
||||
# the false row forever).
|
||||
#
|
||||
# A row is PROVABLY FALSE iff the dead-letter ledger contains an entry matching
|
||||
# it on (kind, id, observed_seq, observed_hash) AND row.observed_seq <=
|
||||
# consumed_seq: the per-key max_by merge means the surviving row's provenance IS
|
||||
# that quarantined entry (a healed row differs in seq/hash and never matches).
|
||||
# PROVABILITY BOUND: a row whose dead-letter evidence was pruned/rotated away is
|
||||
# NOT provable and is never touched — this audit only ever removes what the
|
||||
# ledger can convict. The dead-letter ledger itself is history and is NEVER
|
||||
# modified here.
|
||||
cmd_quarantine_audit() {
|
||||
local repair=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--repair)
|
||||
repair=1
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "store.sh quarantine-audit: unknown option '$1'" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
_need_jq
|
||||
_wake_init_dir "$STATE_DIR"
|
||||
local rec="$STATE_DIR/consumed-hashes.jsonl" dlf="$STATE_DIR/dead-letter.jsonl"
|
||||
if [ ! -s "$rec" ]; then
|
||||
echo "store.sh quarantine-audit: OK — no consumed-hashes record to audit"
|
||||
return 0
|
||||
fi
|
||||
local dl
|
||||
dl="$(jq -s -c '[.[] | {kind: (.locators.kind // ""), id: ((.locators.id // "") | tostring), observed_seq: (.observed_seq // -1), observed_hash: (.locators.observed_hash // "")}]' "$dlf" 2>/dev/null || true)"
|
||||
[ -n "$dl" ] || dl='[]'
|
||||
local consumed
|
||||
consumed="$(_wake_read_int "$STATE_DIR/consumed_seq" 0)"
|
||||
local false_rows
|
||||
false_rows="$(jq -c --argjson dl "$dl" --argjson consumed "$consumed" '
|
||||
. as $row
|
||||
| select(($row.observed_seq // -1) <= $consumed)
|
||||
| select(($dl | map(select(
|
||||
.kind == ($row.kind // "")
|
||||
and .id == (($row.id // "") | tostring)
|
||||
and .observed_seq == ($row.observed_seq // -1)
|
||||
and .observed_hash == ($row.observed_hash // "")
|
||||
)) | length) > 0)
|
||||
' "$rec" 2>/dev/null || true)"
|
||||
if [ -z "$false_rows" ]; then
|
||||
echo "store.sh quarantine-audit: OK — no provably-false consumed-hash rows (rows without surviving dead-letter evidence are not provable and were not judged)"
|
||||
return 0
|
||||
fi
|
||||
local n row
|
||||
n="$(printf '%s\n' "$false_rows" | grep -c . || true)"
|
||||
while IFS= read -r row; do
|
||||
[ -n "$row" ] || continue
|
||||
if [ "$repair" -eq 1 ]; then
|
||||
echo "store.sh quarantine-audit: REPAIR — removing FALSE WITNESS row $row (matches a dead-lettered, never-delivered entry at/below consumed_seq=$consumed)" >&2
|
||||
else
|
||||
echo "FALSE WITNESS — consumed-hashes row $row matches a dead-lettered, never-delivered entry at/below consumed_seq=$consumed (the recorded consumption never happened)"
|
||||
fi
|
||||
done <<<"$false_rows"
|
||||
if [ "$repair" -eq 0 ]; then
|
||||
echo "store.sh quarantine-audit: $n provably-false row(s) found — run with --repair to remove exactly these rows"
|
||||
return 1
|
||||
fi
|
||||
local kept
|
||||
kept="$(jq -c --argjson dl "$dl" --argjson consumed "$consumed" '
|
||||
. as $row
|
||||
| select(
|
||||
(($row.observed_seq // -1) > $consumed)
|
||||
or (($dl | map(select(
|
||||
.kind == ($row.kind // "")
|
||||
and .id == (($row.id // "") | tostring)
|
||||
and .observed_seq == ($row.observed_seq // -1)
|
||||
and .observed_hash == ($row.observed_hash // "")
|
||||
)) | length) == 0)
|
||||
)
|
||||
' "$rec" 2>/dev/null || true)"
|
||||
if ! { printf '%s\n' "$kept" | grep -v '^[[:space:]]*$' || true; } | _atomic_write "$rec"; then
|
||||
echo "store.sh quarantine-audit: consumed-hashes rewrite FAILED — record left untouched" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "store.sh quarantine-audit: repaired — removed $n provably-false row(s); dead-letter ledger untouched (history)"
|
||||
}
|
||||
|
||||
cmd_cursors() {
|
||||
_wake_init_dir "$STATE_DIR"
|
||||
local observed consumed depth
|
||||
@@ -663,8 +463,6 @@ main() {
|
||||
enqueue) cmd_enqueue "$@" ;;
|
||||
drain) cmd_drain "$@" ;;
|
||||
consume) cmd_consume "$@" ;;
|
||||
quarantine-sync) cmd_quarantine_sync "$@" ;;
|
||||
quarantine-audit) cmd_quarantine_audit "$@" ;;
|
||||
cursors) cmd_cursors "$@" ;;
|
||||
-h | --help | help) usage ;;
|
||||
*)
|
||||
|
||||
@@ -644,26 +644,6 @@ EOF
|
||||
entry="$("$STORE" drain | tail -1)"
|
||||
echo "$entry" | jq -e --arg s "$goodsha" 'select(.locators.snapshot_sha==$s) | .locators | has("snapshot_ts")' >/dev/null 2>&1 \
|
||||
|| fail_msg "D13: negative slack must NOT invert the guard into deny-all [$entry]"
|
||||
# (c2) MULTI-LINE knobs — grep's ^...$ anchors bind PER LINE, so a value with
|
||||
# an embedded newline ($'300\n8') passed the old regex whole yet is FATAL in
|
||||
# \$((...)) ('error token is "8"'; $'300\nabc' dies as unbound variable). The
|
||||
# case pattern matches the WHOLE string: both must fall back loudly, keep the ts.
|
||||
printf '{"snapshot_sha":"%s","snapshot_ts":%s}\n' "$goodsha" "$(date +%s)" >"$stub/repo_r1.meta"
|
||||
printf 'SHA-CC2\n' >"$stub/repo_r1"
|
||||
err="$(WAKE_SNAPSHOT_TS_FUTURE_SLACK=$'300\n8' "$DET" poll-once 2>&1 >/dev/null)"; rc=$?
|
||||
[ "$rc" -eq 0 ] || fail_msg "D13: multi-line SLACK (300\\n8) must NEVER fail the poll (rc=$rc) [$err]"
|
||||
echo "$err" | grep -qi 'falling back to 300' || fail_msg "D13: multi-line slack must be LOUD on stderr [$err]"
|
||||
entry="$("$STORE" drain | tail -1)"
|
||||
echo "$entry" | jq -e --arg s "$goodsha" 'select(.locators.snapshot_sha==$s) | .locators | has("snapshot_ts")' >/dev/null 2>&1 \
|
||||
|| fail_msg "D13: valid metadata must SURVIVE a multi-line knob [$entry]"
|
||||
printf '{"snapshot_sha":"%s","snapshot_ts":%s}\n' "$goodsha" "$(date +%s)" >"$stub/repo_r1.meta"
|
||||
printf 'SHA-CC3\n' >"$stub/repo_r1"
|
||||
err="$(WAKE_SNAPSHOT_TS_FUTURE_SLACK=$'300\nabc' "$DET" poll-once 2>&1 >/dev/null)"; rc=$?
|
||||
[ "$rc" -eq 0 ] || fail_msg "D13: multi-line SLACK (300\\nabc) must NEVER fail the poll (rc=$rc) [$err]"
|
||||
echo "$err" | grep -qi 'falling back to 300' || fail_msg "D13: multi-line non-numeric slack must be LOUD on stderr [$err]"
|
||||
entry="$("$STORE" drain | tail -1)"
|
||||
echo "$entry" | jq -e --arg s "$goodsha" 'select(.locators.snapshot_sha==$s) | .locators | has("snapshot_ts")' >/dev/null 2>&1 \
|
||||
|| fail_msg "D13: valid metadata must SURVIVE a multi-line non-numeric knob [$entry]"
|
||||
# (d2-pre) ZERO-PADDED knobs — shape-valid, radix-hostile. '08' passes the
|
||||
# regex but is fatal octal in \$((...)) without the 10# normalization; '0300'
|
||||
# silently means 192 (octal), so a ts +250s ahead would be WRONGLY dropped.
|
||||
@@ -691,7 +671,7 @@ EOF
|
||||
entry="$("$STORE" drain | tail -1)"
|
||||
echo "$entry" | jq -e --arg s "$goodsha" 'select(.locators.snapshot_sha==$s) | .locators | has("snapshot_ts") | not' >/dev/null 2>&1 \
|
||||
|| fail_msg "D13: valid SLACK=0 must drop the future ts but keep the sha [$entry]"
|
||||
[ "$(det_seq)" = "8" ] || fail_msg "D13: all eight real deltas must still have enqueued, got seq $(det_seq)"
|
||||
[ "$(det_seq)" = "6" ] || fail_msg "D13: all six real deltas must still have enqueued, got seq $(det_seq)"
|
||||
) && ok
|
||||
|
||||
echo
|
||||
|
||||
@@ -4,20 +4,12 @@
|
||||
# blocking) + reconciler ENUMERATIONS render ORIENTATION-tier.
|
||||
#
|
||||
# Each test asserts ONE invariant and goes RED against the pre-#920 digest.sh:
|
||||
# Q1 (a) QUARANTINE + REST-DELIVERS: a malformed `actionable` carrying NO hard
|
||||
# locator ({kind,id,observed_hash} — a content hash but no ADDRESS, no
|
||||
# reconciled marker) is DEAD-LETTERED + alarmed AND EXCLUDED, while a
|
||||
# clean valid sibling in the SAME drain still renders (exit 0).
|
||||
# Q1 (a) QUARANTINE + REST-DELIVERS: a malformed `actionable` carrying the live
|
||||
# pilot's EXACT locator shape {kind,id,path,observed_hash} (no hard
|
||||
# locator, no reconciled marker) is DEAD-LETTERED + alarmed AND EXCLUDED,
|
||||
# while a clean valid sibling in the SAME drain still renders (exit 0).
|
||||
# RED baseline: the old whole-digest exit-4 delivered NOTHING (the live
|
||||
# head-of-line-blocking wedge). (#920 FIX1)
|
||||
# [#944 AMENDMENT: at #920 this fixture pinned the live pilot's shape
|
||||
# {kind,id,path,observed_hash} as the malformed case — ratifying a gate
|
||||
# the detector's own locators could never satisfy (every actionable
|
||||
# heartbeat-planning delta dead-lettered; live seqs 63/68). #944 amends
|
||||
# that ruling: `path` is now a hard-locator arm — and ONLY path;
|
||||
# bare snapshot_sha is deliberately NOT an arm, Q11(d) asserts it
|
||||
# still quarantines — so the quarantine fixtures here and in Q6-Q9
|
||||
# use genuinely ADDRESS-FREE shapes instead.]
|
||||
# Q2 (b) ENUM-AS-ORIENTATION: a reconciler enumeration (locators.reconciled==
|
||||
# true) renders as an ORIENTATION-tier pointer and does NOT exit-4 / is
|
||||
# NOT quarantined. RED baseline: reconciled `actionable` + soft locators
|
||||
@@ -61,55 +53,8 @@
|
||||
# (Q6/Q7/Q8 all see 0 captured alarms) and no "FAIL LOUD ... alarm sink"
|
||||
# diagnostic exists to fire (Q9).
|
||||
#
|
||||
# #944 (unsatisfiable-gate fix): _has_hard_locator gains the `path` arm — and
|
||||
# ONLY that arm — covering the detector-built board_file vocabulary.
|
||||
# Q11 POSITIVE CONTROL + RETAINED NEGATIVES, one drain: (a) the live
|
||||
# seq-68 entry VERBATIM (the exact production entry that dead-lettered
|
||||
# under the unsatisfiable gate: kind/id/observed_hash + path +
|
||||
# snapshot_sha + snapshot_ts, class=actionable, as detector.sh
|
||||
# actually emits it — NOT a hand-built dict) must RENDER as a
|
||||
# CLAIM@seq with the one-call `git show <snapshot_sha>:<path>`
|
||||
# re-verify hint — the assertion is the rendered claim, not merely
|
||||
# the predicate returning true; (b) a path-only sibling (no snapshot
|
||||
# attestation — a pre-#940 adapter or a dropped attestation) must
|
||||
# ALSO render, with the "re-read <path>" hint; (c) an address-free
|
||||
# sibling ({kind,id,observed_hash}) must STILL quarantine + route its
|
||||
# own alarm — observed_hash is a content hash, not an address; (d)
|
||||
# bare path-less snapshot_sha siblings must ALSO still quarantine —
|
||||
# the widened gate must not widen PAST the board_file vocabulary
|
||||
# (review-adopted criterion) — asserted at THREE lengths (7-char
|
||||
# abbreviation, 40-hex, 64-char sha-256) spanning the detector's
|
||||
# actual attestation validation ^[0-9a-f]{7,64}$ (detector.sh), so
|
||||
# the assertion distinguishes "no snapshot_sha arm" from "an arm
|
||||
# present but length-gated". RED baseline: pre-#944
|
||||
# digest.sh dead-letters (a) and (b) — an assertion nobody has seen
|
||||
# succeed is as unproven as one nobody has seen fail.
|
||||
#
|
||||
# #946 (ack watermark passes quarantined entries): the rendered digest embedded
|
||||
# `ack.sh consumed --upto <observed>` even when entries in (consumed, observed]
|
||||
# were quarantined — the copy-run line itself instructed the consumer to record
|
||||
# deliveries that never happened (live: five successive digests each stepping
|
||||
# the consumer past buried seq 68). Fix = DISCLOSE + CLAMP + force-only-past:
|
||||
# Q12 disclosure (by seq — content stays EXCLUDED per Q1/Q4) + the
|
||||
# embedded ack CLAMPED below the lowest quarantined seq, hermetic
|
||||
# --from-file; a foreign-data render must NOT write the store's
|
||||
# quarantined.set.
|
||||
# Q13 nothing quarantined -> unclamped ack at the observed cursor; no
|
||||
# disclosure section, no clamp note.
|
||||
# Q14 store-mode render SYNCS quarantined.set (REPLACE) -> the store's
|
||||
# ordinary consume path refuses past the held seq END-TO-END.
|
||||
# Q15 gate-fix RECOVERY: a stale quarantined.set is REPLACED (cleared) by
|
||||
# a clean store-mode render — the clamp self-heals (#944 recovery
|
||||
# invariant; a cumulative-forever set would keep blocking acks on
|
||||
# entries a fixed gate now renders).
|
||||
# Q16 (guard, green-by-design) Q2's ENUM-B fixture must STAY address-free
|
||||
# so the reconciled exemption remains load-bearing at the gate
|
||||
# (#944 F1); goes RED only if the fixture regresses.
|
||||
#
|
||||
# Hermetic: feeds controlled JSONL via `digest.sh render --from-file` — NO store,
|
||||
# NO network, NO openssl (so it runs identically under the CI openssl-mask).
|
||||
# (Q14/Q15 are the intentional exception: the #946 store sync is store-mode-only
|
||||
# behavior, so they drive store.sh enqueue/consume against a temp state home.)
|
||||
#
|
||||
# Each test runs in its own (..) subshell for env isolation; the per-subshell
|
||||
# WAKE_STATE_HOME export is intentional (mirrors test-wake-reconcile.sh).
|
||||
@@ -160,16 +105,15 @@ fresh_home() {
|
||||
# dlq HOME — the dead-letter path for the default agent under HOME.
|
||||
dlq() { printf '%s/default/dead-letter.jsonl' "$1"; }
|
||||
|
||||
echo "== Q1 (a): malformed address-free {kind,id,observed_hash} QUARANTINES; clean sibling STILL delivers =="
|
||||
echo "== Q1 (a): malformed {kind,id,path,observed_hash} QUARANTINES; clean sibling STILL delivers =="
|
||||
(
|
||||
home="$(fresh_home q1)"
|
||||
export WAKE_STATE_HOME="$home"
|
||||
unset WAKE_AGENT
|
||||
f="$TMP_ROOT/q1.jsonl"
|
||||
# An ADDRESS-FREE malformed shape (no reconciled marker) + a clean sibling.
|
||||
# [#944: the original fixture carried `path`, which is now a hard-locator arm.]
|
||||
# The live pilot's EXACT malformed shape (no reconciled marker) + a clean sibling.
|
||||
{
|
||||
printf '%s\n' '{"observed_seq":1,"class":"actionable","locators":{"kind":"repo","id":"MALFORMED-Q","observed_hash":"deadbeef"},"emit_ts":1}'
|
||||
printf '%s\n' '{"observed_seq":1,"class":"actionable","locators":{"kind":"repo","id":"MALFORMED-Q","path":"docs/x.md","observed_hash":"deadbeef"},"emit_ts":1}'
|
||||
printf '{"observed_seq":2,"class":"actionable","locators":{"sha":"%s","file":"src/a.ts"},"emit_ts":1}\n' "$SHA40"
|
||||
} >"$f"
|
||||
err="$TMP_ROOT/q1.err"
|
||||
@@ -191,11 +135,7 @@ echo "== Q2 (b): reconciler enumeration (reconciled:true) renders ORIENTATION-ti
|
||||
unset WAKE_AGENT
|
||||
f="$TMP_ROOT/q2.jsonl"
|
||||
# A reconciler enumeration: store class actionable (unchanged) + reconciled marker.
|
||||
# ADDRESS-FREE on purpose (#944 F1): no path/file/sha/repo+issue — the reconciled
|
||||
# exemption must be the ONLY thing keeping this entry out of quarantine, so the
|
||||
# exemption is proven load-bearing AT THE GATE (mutation-killable), not merely at
|
||||
# the tier label. (Q3's ENUM-C* stay path-bearing: reconciled + valid-locator mix.)
|
||||
printf '%s\n' '{"observed_seq":5,"class":"actionable","locators":{"kind":"repo","id":"ENUM-B","observed_hash":"cafe1234","reconciled":true},"emit_ts":1}' >"$f"
|
||||
printf '%s\n' '{"observed_seq":5,"class":"actionable","locators":{"kind":"repo","id":"ENUM-B","path":"BOARD.md","observed_hash":"cafe1234","reconciled":true},"emit_ts":1}' >"$f"
|
||||
err="$TMP_ROOT/q2.err"
|
||||
out="$("$DIGEST" render --from-file "$f" --agent default 2>"$err")"
|
||||
rc=$?
|
||||
@@ -269,7 +209,7 @@ echo "== Q6 (a): dead-lettered entry routes EXACTLY ONE off-host alarm (payload
|
||||
export WAKE_STATE_HOME="$home"
|
||||
unset WAKE_AGENT
|
||||
f="$TMP_ROOT/q6.jsonl"
|
||||
printf '%s\n' '{"observed_seq":21,"class":"actionable","locators":{"kind":"repo","id":"DLQ-Q6","observed_hash":"aaaa"},"emit_ts":1}' >"$f"
|
||||
printf '%s\n' '{"observed_seq":21,"class":"actionable","locators":{"kind":"repo","id":"DLQ-Q6","path":"x.md","observed_hash":"aaaa"},"emit_ts":1}' >"$f"
|
||||
ALARM_OUT="$TMP_ROOT/q6.alarm.jsonl"
|
||||
export ALARM_OUT
|
||||
: >"$ALARM_OUT"
|
||||
@@ -292,7 +232,7 @@ echo "== Q7 (b): re-draining the SAME still-dead-lettered entry N times routes Z
|
||||
export WAKE_STATE_HOME="$home"
|
||||
unset WAKE_AGENT
|
||||
f="$TMP_ROOT/q7.jsonl"
|
||||
printf '%s\n' '{"observed_seq":22,"class":"actionable","locators":{"kind":"repo","id":"DLQ-Q7","observed_hash":"bbbb"},"emit_ts":1}' >"$f"
|
||||
printf '%s\n' '{"observed_seq":22,"class":"actionable","locators":{"kind":"repo","id":"DLQ-Q7","path":"y.md","observed_hash":"bbbb"},"emit_ts":1}' >"$f"
|
||||
ALARM_OUT="$TMP_ROOT/q7.alarm.jsonl"
|
||||
export ALARM_OUT
|
||||
: >"$ALARM_OUT"
|
||||
@@ -312,8 +252,8 @@ echo "== Q8 (c): a NEW distinct dead-lettered entry routes its OWN one alarm (de
|
||||
unset WAKE_AGENT
|
||||
f1="$TMP_ROOT/q8a.jsonl"
|
||||
f2="$TMP_ROOT/q8b.jsonl"
|
||||
printf '%s\n' '{"observed_seq":31,"class":"actionable","locators":{"kind":"repo","id":"DLQ-Q8A","observed_hash":"c1"},"emit_ts":1}' >"$f1"
|
||||
printf '%s\n' '{"observed_seq":32,"class":"actionable","locators":{"kind":"repo","id":"DLQ-Q8B","observed_hash":"c2"},"emit_ts":1}' >"$f2"
|
||||
printf '%s\n' '{"observed_seq":31,"class":"actionable","locators":{"kind":"repo","id":"DLQ-Q8A","path":"a.md","observed_hash":"c1"},"emit_ts":1}' >"$f1"
|
||||
printf '%s\n' '{"observed_seq":32,"class":"actionable","locators":{"kind":"repo","id":"DLQ-Q8B","path":"b.md","observed_hash":"c2"},"emit_ts":1}' >"$f2"
|
||||
ALARM_OUT="$TMP_ROOT/q8.alarm.jsonl"
|
||||
export ALARM_OUT
|
||||
: >"$ALARM_OUT"
|
||||
@@ -333,7 +273,7 @@ echo "== Q9 (d): WAKE_ALARM_SINK_CMD unconfigured OR unreachable -> FAIL LOUD (n
|
||||
export WAKE_STATE_HOME="$home"
|
||||
unset WAKE_AGENT
|
||||
f="$TMP_ROOT/q9.jsonl"
|
||||
printf '%s\n' '{"observed_seq":41,"class":"actionable","locators":{"kind":"repo","id":"DLQ-Q9","observed_hash":"dddd"},"emit_ts":1}' >"$f"
|
||||
printf '%s\n' '{"observed_seq":41,"class":"actionable","locators":{"kind":"repo","id":"DLQ-Q9","path":"z.md","observed_hash":"dddd"},"emit_ts":1}' >"$f"
|
||||
# (a) UNCONFIGURED alarm sink.
|
||||
unset WAKE_ALARM_SINK_CMD
|
||||
err_a="$TMP_ROOT/q9a.err"
|
||||
@@ -397,185 +337,6 @@ echo "== Q10: snapshot metadata (#940) — snapshot_sha/snapshot_ts render on th
|
||||
true
|
||||
) && ok
|
||||
|
||||
echo "== Q11 (#944): REAL detector-shape actionable RENDERS as CLAIM@seq; address-free + bare-snapshot_sha siblings STILL quarantine =="
|
||||
(
|
||||
home="$(fresh_home q11)"
|
||||
export WAKE_STATE_HOME="$home"
|
||||
unset WAKE_AGENT
|
||||
f="$TMP_ROOT/q11.jsonl"
|
||||
{
|
||||
# (a) the LIVE seq-68 entry VERBATIM — the exact production entry that
|
||||
# dead-lettered on the mos-dt lane under the unsatisfiable gate
|
||||
# (dragon-lin dead-letter.jsonl, 2026-07-30). Detector-emitted shape,
|
||||
# not a hand-built dict.
|
||||
printf '%s\n' '{"observed_seq":68,"locators":{"kind":"board_file","id":"heartbeat-planning","observed_hash":"2e85f2474001961e920976a91981eca5bb86a5ff0df044e082a1e1f2dc7493b5","snapshot_sha":"55d4909569d2b5fbccfec49ec9ca83db5049f3ce","snapshot_ts":1785414523,"path":"docs/scratchpads/heartbeat-planning"},"class":"actionable","emit_ts":1785415057,"hmac":""}'
|
||||
# (b) same vocabulary WITHOUT snapshot attestation (pre-#940 adapter or
|
||||
# dropped-as-malformed attestation) — must pass via the path arm alone.
|
||||
printf '%s\n' '{"observed_seq":69,"class":"actionable","locators":{"kind":"board_file","id":"PILOT-LOCAL","observed_hash":"abcd1234","path":"BOARD.md"},"emit_ts":2}'
|
||||
# (c) ADDRESS-FREE — the retained negative: a content hash is not an address.
|
||||
printf '%s\n' '{"observed_seq":70,"class":"actionable","locators":{"kind":"board_file","id":"ADDR-FREE","observed_hash":"ffff0000"},"emit_ts":2}'
|
||||
# (d) bare path-less snapshot_sha — must NOT pass: the widened gate must
|
||||
# not widen past the board_file vocabulary. Asserted at all three
|
||||
# lengths the detector's attestation validation ^[0-9a-f]{7,64}$
|
||||
# admits: a 40-hex sha-1, a 7-char abbreviation, a 64-char sha-256.
|
||||
# One length alone cannot distinguish "no arm" from "arm present but
|
||||
# length-gated" (enumeration finding E1).
|
||||
printf '%s\n' '{"observed_seq":71,"class":"actionable","locators":{"kind":"board_file","id":"SNAP-ONLY-40","observed_hash":"eeee1111","snapshot_sha":"55d4909569d2b5fbccfec49ec9ca83db5049f3ce"},"emit_ts":2}'
|
||||
printf '%s\n' '{"observed_seq":72,"class":"actionable","locators":{"kind":"board_file","id":"SNAP-ONLY-7","observed_hash":"eeee2222","snapshot_sha":"55d4909"},"emit_ts":2}'
|
||||
printf '%s\n' '{"observed_seq":73,"class":"actionable","locators":{"kind":"board_file","id":"SNAP-ONLY-64","observed_hash":"eeee3333","snapshot_sha":"55d4909569d2b5fbccfec49ec9ca83db5049f3ce55d4909569d2b5fbccfec49e"},"emit_ts":2}'
|
||||
} >"$f"
|
||||
ALARM_OUT="$TMP_ROOT/q11.alarm.jsonl"
|
||||
export ALARM_OUT
|
||||
: >"$ALARM_OUT"
|
||||
export WAKE_ALARM_SINK_CMD="$CAPTURE_ALARM"
|
||||
err="$TMP_ROOT/q11.err"
|
||||
out="$("$DIGEST" render --from-file "$f" --agent default 2>"$err")"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] || fail_msg "Q11: render must exit 0, got rc=$rc"
|
||||
# (a) POSITIVE: the live entry RENDERS as an actionable claim (not merely
|
||||
# passes the predicate) with the one-call git-show re-verify hint.
|
||||
printf '%s' "$out" | grep -q 'seq 68 — CLAIM@seq' || fail_msg "Q11a: the live seq-68 detector-shape entry must RENDER as CLAIM@seq (positive control)"
|
||||
printf '%s' "$out" | grep -q 'git show 55d4909569d2b5fbccfec49ec9ca83db5049f3ce:docs/scratchpads/heartbeat-planning' \
|
||||
|| fail_msg "Q11a: the rendered claim must carry the one-call re-verify hint git show <snapshot_sha>:<path>"
|
||||
# (b) POSITIVE: path arm alone suffices; hint degrades to one-call re-read.
|
||||
printf '%s' "$out" | grep -q 'seq 69 — CLAIM@seq' || fail_msg "Q11b: a path-only detector-shape entry must RENDER as CLAIM@seq (path arm)"
|
||||
printf '%s' "$out" | grep -q 're-read BOARD.md' || fail_msg "Q11b: the path-only claim must carry the one-call re-read <path> hint"
|
||||
n_claims="$(printf '%s\n' "$out" | grep -c 'CLAIM@seq' || true)"
|
||||
[ "$n_claims" = "2" ] || fail_msg "Q11: EXACTLY the two valid entries must render as CLAIM@seq (got $n_claims)"
|
||||
# (c)+(d) NEGATIVES retained: both quarantine, each with its OWN alarm.
|
||||
printf '%s' "$out" | grep -q 'ADDR-FREE' && fail_msg "Q11c: the address-free entry must be EXCLUDED from the digest"
|
||||
printf '%s' "$out" | grep -q 'SNAP-ONLY' && fail_msg "Q11d: no bare path-less snapshot_sha entry may appear in the digest (gate must not widen past board_file)"
|
||||
grep -q 'ADDR-FREE' "$(dlq "$home")" 2>/dev/null || fail_msg "Q11c: the address-free entry must be DEAD-LETTERED"
|
||||
for snap_id in SNAP-ONLY-40 SNAP-ONLY-7 SNAP-ONLY-64; do
|
||||
grep -q "$snap_id" "$(dlq "$home")" 2>/dev/null || fail_msg "Q11d: the bare snapshot_sha entry ($snap_id) must be DEAD-LETTERED"
|
||||
done
|
||||
grep -q 'heartbeat-planning' "$(dlq "$home")" 2>/dev/null && fail_msg "Q11a: the valid live entry must NOT be dead-lettered"
|
||||
grep -q 'PILOT-LOCAL' "$(dlq "$home")" 2>/dev/null && fail_msg "Q11b: the valid path-only entry must NOT be dead-lettered"
|
||||
n_alarms="$(grep -c . "$ALARM_OUT" 2>/dev/null || true)"
|
||||
[ "$n_alarms" = "4" ] || fail_msg "Q11: exactly the four invalid entries must alarm (got $n_alarms) [$(cat "$ALARM_OUT" 2>/dev/null)]"
|
||||
grep -q '"observed_seq":70' "$ALARM_OUT" 2>/dev/null || fail_msg "Q11c: seq 70's own alarm must be present"
|
||||
for snap_seq in 71 72 73; do
|
||||
grep -q "\"observed_seq\":$snap_seq" "$ALARM_OUT" 2>/dev/null || fail_msg "Q11d: seq $snap_seq's own alarm must be present"
|
||||
done
|
||||
true
|
||||
) && ok
|
||||
|
||||
echo "== Q12 (#946): quarantined entries are DISCLOSED (by seq, content withheld) and the embedded ack is CLAMPED below them =="
|
||||
(
|
||||
home="$(fresh_home q12)"
|
||||
export WAKE_STATE_HOME="$home"
|
||||
unset WAKE_AGENT
|
||||
mkdir -p "$home/default"
|
||||
printf '2' >"$home/default/observed_seq"
|
||||
f="$TMP_ROOT/q12.jsonl"
|
||||
{
|
||||
printf '{"observed_seq":1,"class":"actionable","locators":{"sha":"%s","file":"src/a.ts"},"emit_ts":1}\n' "$SHA40"
|
||||
printf '%s\n' '{"observed_seq":2,"class":"actionable","locators":{"kind":"board_file","id":"ADDR-Q12","observed_hash":"qq12"},"emit_ts":1}'
|
||||
} >"$f"
|
||||
out="$("$DIGEST" render --from-file "$f" --agent default 2>/dev/null)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] || fail_msg "Q12: render must exit 0, got rc=$rc"
|
||||
# DISCLOSURE: a held entry must be VISIBLE in the digest it was held from —
|
||||
# a silent hold is how five successive digests each stepped past seq 68...
|
||||
printf '%s' "$out" | grep -q 'QUARANTINED' || fail_msg "Q12: the digest must carry a QUARANTINED disclosure section (no silent hold)"
|
||||
printf '%s' "$out" | grep -q 'seq 2 .*HELD' || fail_msg "Q12: the disclosure must name the held seq (2) as HELD"
|
||||
# ...but WITHOUT re-injecting the refused content: disclosure is by seq only;
|
||||
# the Q1/Q4/Q5/Q6/Q9/Q11 exclusion property stands.
|
||||
printf '%s' "$out" | grep -q 'ADDR-Q12' && fail_msg "Q12: the quarantined entry's content/locators must stay EXCLUDED from the digest"
|
||||
# CLAMP: the embedded ack stops BELOW the quarantined seq, and says so loudly.
|
||||
printf '%s' "$out" | grep -Eq 'consumed --upto 1$' || fail_msg "Q12: the embedded ack must be CLAMPED to --upto 1 (below quarantined seq 2)"
|
||||
printf '%s' "$out" | grep -Eq 'consumed --upto 2( |$)' && fail_msg "Q12: the raw observed cursor (2) must NOT be embedded while seq 2 is quarantined"
|
||||
printf '%s' "$out" | grep -q 'ACK CLAMPED' || fail_msg "Q12: the clamp must be LOUDLY disclosed in the ACK section"
|
||||
# A foreign-data render must NOT rewrite the lane's quarantine truth.
|
||||
[ -e "$home/default/quarantined.set" ] && fail_msg "Q12: a --from-file render must NOT write the store's quarantined.set (lane truth is store-mode only)"
|
||||
true
|
||||
) && ok
|
||||
|
||||
echo "== Q13 (#946): nothing quarantined -> ack UNCLAMPED at the observed cursor; no disclosure section, no clamp note =="
|
||||
(
|
||||
home="$(fresh_home q13)"
|
||||
export WAKE_STATE_HOME="$home"
|
||||
unset WAKE_AGENT
|
||||
mkdir -p "$home/default"
|
||||
printf '1' >"$home/default/observed_seq"
|
||||
f="$TMP_ROOT/q13.jsonl"
|
||||
printf '{"observed_seq":1,"class":"actionable","locators":{"sha":"%s","file":"src/a.ts"},"emit_ts":1}\n' "$SHA40" >"$f"
|
||||
out="$("$DIGEST" render --from-file "$f" --agent default 2>/dev/null)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] || fail_msg "Q13: render must exit 0, got rc=$rc"
|
||||
printf '%s' "$out" | grep -Eq 'consumed --upto 1$' || fail_msg "Q13: with nothing quarantined the ack must embed the observed cursor (1) unchanged"
|
||||
printf '%s' "$out" | grep -q 'QUARANTINED' && fail_msg "Q13: no disclosure section when nothing is quarantined"
|
||||
printf '%s' "$out" | grep -q 'ACK CLAMPED' && fail_msg "Q13: no clamp note when nothing is quarantined"
|
||||
true
|
||||
) && ok
|
||||
|
||||
echo "== Q14 (#946): store-mode render SYNCS quarantine truth into the store — the clamp is enforced END-TO-END at consume =="
|
||||
(
|
||||
home="$(fresh_home q14)"
|
||||
export WAKE_STATE_HOME="$home"
|
||||
unset WAKE_AGENT
|
||||
STORE="$SCRIPT_DIR/store.sh"
|
||||
"$STORE" enqueue --class actionable --locators "{\"sha\":\"$SHA40\",\"file\":\"src/a.ts\"}" >/dev/null
|
||||
"$STORE" enqueue --class actionable --locators '{"kind":"board_file","id":"ADDR-Q14","observed_hash":"qq14"}' >/dev/null
|
||||
out="$("$DIGEST" render --from-store --agent default 2>/dev/null)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] || fail_msg "Q14: render must exit 0, got rc=$rc"
|
||||
printf '%s' "$out" | grep -q 'QUARANTINED' || fail_msg "Q14: the store-mode digest must disclose the held entry"
|
||||
printf '%s' "$out" | grep -Eq 'consumed --upto 1$' || fail_msg "Q14: the embedded ack must clamp to 1 (below quarantined seq 2)"
|
||||
qf="$home/default/quarantined.set"
|
||||
[ "$(cat "$qf" 2>/dev/null)" = "2" ] || fail_msg "Q14: a store-mode render must sync quarantined.set to exactly {2}, got [$(cat "$qf" 2>/dev/null)]"
|
||||
# END-TO-END: even a hand-typed upto past the held seq is refused at the
|
||||
# store — the copy-run defect (#946) cannot re-land via a different path.
|
||||
if "$STORE" consume --upto 2 >/dev/null 2>&1; then
|
||||
fail_msg "Q14: store consume --upto 2 must be REFUSED after the render synced the quarantine"
|
||||
fi
|
||||
"$STORE" consume --upto 1 >/dev/null 2>&1 || fail_msg "Q14: consume --upto 1 (the clamped value) must succeed"
|
||||
) && ok
|
||||
|
||||
echo "== Q15 (#946): gate-fix RECOVERY — a stale quarantined.set is REPLACED by a clean store-mode render; the clamp self-heals =="
|
||||
(
|
||||
home="$(fresh_home q15)"
|
||||
export WAKE_STATE_HOME="$home"
|
||||
unset WAKE_AGENT
|
||||
STORE="$SCRIPT_DIR/store.sh"
|
||||
"$STORE" enqueue --class actionable --locators "{\"sha\":\"$SHA40\",\"file\":\"src/a.ts\"}" >/dev/null
|
||||
"$STORE" enqueue --class actionable --locators '{"kind":"board_file","id":"OK-NOW","observed_hash":"h","path":"BOARD.md"}' >/dev/null
|
||||
# A stale set from a broken-gate era: both seqs wrongly quarantined.
|
||||
printf '1\n2\n' >"$home/default/quarantined.set"
|
||||
out="$("$DIGEST" render --from-store --agent default 2>/dev/null)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] || fail_msg "Q15: render must exit 0, got rc=$rc"
|
||||
printf '%s' "$out" | grep -q 'QUARANTINED' && fail_msg "Q15: nothing quarantines under the fixed gate — no disclosure section"
|
||||
printf '%s' "$out" | grep -Eq 'consumed --upto 2$' || fail_msg "Q15: the ack must embed the full observed cursor (2) once the gate is fixed"
|
||||
[ -s "$home/default/quarantined.set" ] && fail_msg "Q15: the clean render must REPLACE (clear) the stale quarantined.set — a cumulative-forever set would block acks on entries that now render, got [$(cat "$home/default/quarantined.set")]"
|
||||
"$STORE" consume --upto 2 >/dev/null 2>&1 || fail_msg "Q15: the ordinary consume must succeed after the clamp self-heals"
|
||||
) && ok
|
||||
|
||||
echo "== Q16 (guard): Q2's ENUM-B fixture must STAY address-free — the reconciled exemption must remain load-bearing at the gate (#944 F1) =="
|
||||
(
|
||||
self="$SCRIPT_DIR/test-wake-digest-quarantine.sh"
|
||||
# Token concatenated so THIS guard's own source lines never contain the
|
||||
# literal fixture id and cannot self-match.
|
||||
enum_id='ENUM''-B'
|
||||
fixture_line="$(grep -F "\"id\":\"$enum_id\"" "$self" | grep -F '"observed_seq":5' | head -n1)"
|
||||
[ -n "$fixture_line" ] || fail_msg "Q16: could not locate Q2's $enum_id fixture line (renamed/renumbered? update this guard)"
|
||||
fixture_json="$(printf '%s' "$fixture_line" | sed "s/.*'\({.*}\)'.*/\1/")"
|
||||
# Positive controls FIRST (blind-instrument rule): the extraction must yield
|
||||
# the real fixture, and the predicate must be able to detect a hard locator.
|
||||
printf '%s' "$fixture_json" | jq -e . >/dev/null 2>&1 || fail_msg "Q16: extracted fixture is not valid JSON [$fixture_json]"
|
||||
printf '%s' "$fixture_json" | jq -e '.locators.reconciled == true' >/dev/null 2>&1 || fail_msg "Q16: fixture must carry reconciled:true (wrong line extracted?) [$fixture_json]"
|
||||
hard_arms='.locators | ((.repo // "") != "" and (((.issue // "") | tostring) != "")) or (((.sha // "") | tostring) | test("^[0-9a-f]{40}$")) or ((.file // "") != "") or ((.path // "") != "")'
|
||||
printf '%s' '{"locators":{"path":"BOARD.md"}}' | jq -e "$hard_arms" >/dev/null 2>&1 || fail_msg "Q16: positive control failed — the inline hard-locator predicate did not detect a path arm (instrument broken; re-sync it with digest.sh _has_hard_locator)"
|
||||
# THE GUARD: the fixture must remain ADDRESS-FREE. If it ever gains a hard
|
||||
# locator, Q2 passes the gate for the wrong reason and the reconciled
|
||||
# exemption stops being exercised (#944 F1: an exemption nobody exercises is
|
||||
# as unproven as a gate nobody has seen refuse).
|
||||
if printf '%s' "$fixture_json" | jq -e "$hard_arms" >/dev/null 2>&1; then
|
||||
fail_msg "Q16: Q2's $enum_id fixture has grown a hard-locator arm — restore an address-free fixture so the reconciled exemption stays load-bearing [$fixture_json]"
|
||||
fi
|
||||
true
|
||||
) && ok
|
||||
|
||||
echo
|
||||
if [ -s "$FAILFILE" ]; then
|
||||
echo "wake digest-quarantine harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2
|
||||
|
||||
@@ -1,534 +0,0 @@
|
||||
#!/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
|
||||
@@ -19,15 +19,6 @@
|
||||
# T10 concurrency: two concurrent enqueues get DISTINCT seqs (lock) (#908)
|
||||
# T12 consume records the last-consumed observed_hash per (kind,id) into the
|
||||
# store-owned record (additive; monotonic last-seq wins; lazily created) (#932)
|
||||
# T13 #946 quarantine CLAMP: ordinary consume (store + ack wrapper) REFUSES to
|
||||
# advance past a quarantined seq; the refusal names the seq + the force flag
|
||||
# T14 #946 forced step-over: --force-past-quarantine advances LOUDLY, prunes the
|
||||
# set, and NEVER fabricates a consumed-hash row for the quarantined entry
|
||||
# T15 #946 quarantine-sync: full REPLACE semantics (sorted/deduped; empty input
|
||||
# CLEARS — the clamp self-heals once the gate is fixed; invalid input refused)
|
||||
# T16 #946 quarantine-audit: consumed-hashes rows provably false against the
|
||||
# dead-letter ledger are reported (exit 1) and removed only under --repair;
|
||||
# healed rows and the ledger itself are untouched
|
||||
#
|
||||
# Isolated: every test runs against a fresh WAKE_STATE_HOME temp dir.
|
||||
set -uo pipefail
|
||||
@@ -530,156 +521,6 @@ echo "== T12: #932 — consume records the last-consumed observed_hash per (kind
|
||||
"$STORE" cursors | grep -q 'consumed_seq=3' || fail_msg "T12: consumed_seq must be 3 after CONSUMED 3"
|
||||
) && ok
|
||||
|
||||
echo "== T13: #946 — ordinary consume REFUSES to advance past a quarantined seq (store + ack paths) =="
|
||||
(
|
||||
WAKE_STATE_HOME="$(fresh_state t13)"
|
||||
export WAKE_STATE_HOME
|
||||
unset WAKE_AGENT
|
||||
"$STORE" enqueue --class actionable --locators '{"kind":"repo","id":"a","observed_hash":"HA"}' >/dev/null
|
||||
"$STORE" enqueue --class actionable --locators '{"kind":"repo","id":"b","observed_hash":"HB"}' >/dev/null
|
||||
"$STORE" enqueue --class actionable --locators '{"kind":"repo","id":"c","observed_hash":"HC"}' >/dev/null
|
||||
printf '2\n' | "$STORE" quarantine-sync || fail_msg "T13: quarantine-sync must accept a valid seq list"
|
||||
# A quarantined seq was dead-lettered at render and NEVER delivered in any
|
||||
# digest; the ordinary path must REFUSE to record it consumed (#946: a force
|
||||
# flag the ordinary path can bypass is decoration).
|
||||
if "$STORE" consume --upto 3 >/dev/null 2>&1; then
|
||||
fail_msg "T13: ordinary consume --upto 3 must be REFUSED while seq 2 is quarantined"
|
||||
fi
|
||||
err="$("$STORE" consume --upto 3 2>&1 >/dev/null || true)"
|
||||
echo "$err" | grep -q 'quarantined seq(s): 2' || fail_msg "T13: the refusal must NAME the quarantined seq [$err]"
|
||||
echo "$err" | grep -q -- '--force-past-quarantine' || fail_msg "T13: the refusal must NAME the force flag [$err]"
|
||||
cur="$("$STORE" cursors)"
|
||||
echo "$cur" | grep -q 'consumed_seq=0' || fail_msg "T13: a refused consume must NOT advance the cursor [$cur]"
|
||||
# BELOW the quarantined seq the ordinary path is unaffected.
|
||||
"$STORE" consume --upto 1 >/dev/null 2>&1 || fail_msg "T13: consume --upto 1 (below the quarantined seq) must succeed"
|
||||
# The ack wrapper propagates the refusal — no ordinary-path bypass exists.
|
||||
if "$ACK" consumed --upto 3 --no-sync >/dev/null 2>&1; then
|
||||
fail_msg "T13: ack.sh consumed --upto 3 must be REFUSED while seq 2 is quarantined (ordinary-path bypass)"
|
||||
fi
|
||||
cur="$("$STORE" cursors)"
|
||||
echo "$cur" | grep -q 'consumed_seq=1' || fail_msg "T13: cursor must still be 1 after the refused ack [$cur]"
|
||||
) && ok
|
||||
|
||||
echo "== T14: #946 — FORCED step-over is LOUD, prunes the set, and NEVER fabricates a consumed-hash row for the quarantined entry =="
|
||||
(
|
||||
WAKE_STATE_HOME="$(fresh_state t14)"
|
||||
export WAKE_STATE_HOME
|
||||
unset WAKE_AGENT
|
||||
rec="$WAKE_STATE_HOME/default/consumed-hashes.jsonl"
|
||||
qf="$WAKE_STATE_HOME/default/quarantined.set"
|
||||
"$STORE" enqueue --class actionable --locators '{"kind":"repo","id":"a","observed_hash":"HA"}' >/dev/null
|
||||
"$STORE" enqueue --class actionable --locators '{"kind":"repo","id":"b","observed_hash":"HB"}' >/dev/null
|
||||
"$STORE" enqueue --class actionable --locators '{"kind":"repo","id":"c","observed_hash":"HC"}' >/dev/null
|
||||
printf '2\n' | "$STORE" quarantine-sync || fail_msg "T14: quarantine-sync failed"
|
||||
errf="$TMP_ROOT/t14.err"
|
||||
out="$("$STORE" consume --upto 3 --force-past-quarantine 2>"$errf")"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] || fail_msg "T14: forced consume must succeed (rc=$rc) [$(cat "$errf")]"
|
||||
[ "$out" = "3" ] || fail_msg "T14: forced consume must print the new cursor 3, got '$out'"
|
||||
grep -q 'FORCED PAST QUARANTINE' "$errf" || fail_msg "T14: the forced path must be LOUD on stderr [$(cat "$errf")]"
|
||||
grep -q 'seq 2' "$errf" || fail_msg "T14: the forced-path diagnostic must name the stepped-over seq 2 [$(cat "$errf")]"
|
||||
"$STORE" cursors | grep -q 'consumed_seq=3' || fail_msg "T14: forced consume must advance the cursor to 3"
|
||||
# NO FALSE WITNESS: the quarantined entry (repo/b) was NEVER delivered, so no
|
||||
# consumed-hash row may exist for it — even on the forced path (the reconciler
|
||||
# re-enumerating it once is safe-but-noisy; a false witness silences it
|
||||
# forever). Its delivered siblings' rows must exist.
|
||||
jq_any "$rec" '.kind=="repo" and .id=="a" and .observed_hash=="HA"' || fail_msg "T14: the delivered sibling repo/a must have its consumed-hash row"
|
||||
jq_any "$rec" '.kind=="repo" and .id=="c" and .observed_hash=="HC"' || fail_msg "T14: the delivered sibling repo/c must have its consumed-hash row"
|
||||
jq_any "$rec" '.kind=="repo" and .id=="b"' && fail_msg "T14: the quarantined entry repo/b must have NO consumed-hash row (a row would witness a delivery that never happened)"
|
||||
# The stepped-over seq is PRUNED from the set (it is consumed now; a stale
|
||||
# entry would re-refuse forever).
|
||||
grep -qxF '2' "$qf" 2>/dev/null && fail_msg "T14: seq 2 must be PRUNED from quarantined.set after the forced step-over"
|
||||
# The ack wrapper's force flag passes through, stays LOUD on stderr, and
|
||||
# still reports a CLEAN cursor line on stdout.
|
||||
"$STORE" enqueue --class actionable --locators '{"kind":"repo","id":"d","observed_hash":"HD"}' >/dev/null
|
||||
"$STORE" enqueue --class actionable --locators '{"kind":"repo","id":"e","observed_hash":"HE"}' >/dev/null
|
||||
printf '5\n' | "$STORE" quarantine-sync || fail_msg "T14: quarantine-sync (2nd) failed"
|
||||
errf2="$TMP_ROOT/t14b.err"
|
||||
out2="$("$ACK" consumed --upto 5 --no-sync --force-past-quarantine 2>"$errf2")"
|
||||
rc2=$?
|
||||
[ "$rc2" -eq 0 ] || fail_msg "T14: forced ack must succeed (rc=$rc2) [$(cat "$errf2")]"
|
||||
echo "$out2" | grep -q '^CONSUMED 5$' || fail_msg "T14: forced ack must report a CLEAN cursor line 'CONSUMED 5', got '$out2'"
|
||||
grep -q 'FORCED PAST QUARANTINE' "$errf2" || fail_msg "T14: the forced-path loudness must survive the ack wrapper (stderr) [$(cat "$errf2")]"
|
||||
jq_any "$rec" '.kind=="repo" and .id=="e"' && fail_msg "T14: the quarantined repo/e must have NO consumed-hash row via the forced ack path either"
|
||||
true
|
||||
) && ok
|
||||
|
||||
echo "== T15: #946 — quarantine-sync is a full REPLACE (sorted, deduped; empty input CLEARS; invalid input REFUSED) =="
|
||||
(
|
||||
WAKE_STATE_HOME="$(fresh_state t15)"
|
||||
export WAKE_STATE_HOME
|
||||
unset WAKE_AGENT
|
||||
qf="$WAKE_STATE_HOME/default/quarantined.set"
|
||||
printf '3\n1\n3\n' | "$STORE" quarantine-sync || fail_msg "T15: sync of a valid list must succeed"
|
||||
[ "$(cat "$qf" 2>/dev/null)" = "$(printf '1\n3')" ] || fail_msg "T15: set must be sorted+deduped {1,3}, got [$(cat "$qf" 2>/dev/null)]"
|
||||
printf '2\n' | "$STORE" quarantine-sync || fail_msg "T15: re-sync must succeed"
|
||||
[ "$(cat "$qf" 2>/dev/null)" = "2" ] || fail_msg "T15: sync must REPLACE, not merge — expected {2}, got [$(cat "$qf" 2>/dev/null)]"
|
||||
# Empty input CLEARS the set: the set is re-DERIVED per authoritative render,
|
||||
# never accumulated, so a fixed locator gate self-heals the clamp.
|
||||
: | "$STORE" quarantine-sync || fail_msg "T15: empty sync (clear) must succeed"
|
||||
[ ! -s "$qf" ] || fail_msg "T15: empty sync must CLEAR the set, got [$(cat "$qf")]"
|
||||
# Invalid input is refused loudly and must not corrupt the set.
|
||||
printf '1\n' | "$STORE" quarantine-sync || fail_msg "T15: re-seed failed"
|
||||
if printf 'abc\n' | "$STORE" quarantine-sync >/dev/null 2>&1; then
|
||||
fail_msg "T15: a non-integer line must be REFUSED"
|
||||
fi
|
||||
[ "$(cat "$qf" 2>/dev/null)" = "1" ] || fail_msg "T15: a refused sync must leave the set untouched, got [$(cat "$qf" 2>/dev/null)]"
|
||||
# End-to-end: a cleared set stops clamping (the #944 recovery case).
|
||||
"$STORE" enqueue --class actionable --locators '{"kind":"repo","id":"x","observed_hash":"H1"}' >/dev/null
|
||||
if "$STORE" consume --upto 1 >/dev/null 2>&1; then
|
||||
fail_msg "T15: consume --upto 1 must be refused while seq 1 is quarantined"
|
||||
fi
|
||||
: | "$STORE" quarantine-sync || fail_msg "T15: clear failed"
|
||||
"$STORE" consume --upto 1 >/dev/null 2>&1 || fail_msg "T15: after the set is cleared (gate fixed), the ordinary consume must succeed — the clamp must self-heal"
|
||||
) && ok
|
||||
|
||||
echo "== T16: #946 — quarantine-audit: a consumed-hash row matching a dead-letter entry on (kind,id,seq,hash) at/below consumed_seq is PROVABLY FALSE; --repair removes ONLY those rows =="
|
||||
(
|
||||
WAKE_STATE_HOME="$(fresh_state t16)"
|
||||
export WAKE_STATE_HOME
|
||||
unset WAKE_AGENT
|
||||
STATE_DIR="$WAKE_STATE_HOME/default"
|
||||
rec="$STATE_DIR/consumed-hashes.jsonl"
|
||||
dl="$STATE_DIR/dead-letter.jsonl"
|
||||
# Rebuild the historical false-witness state via the REAL flow the defect
|
||||
# used: X@1 was quarantined (dead-lettered) yet consumed under pre-#946 code;
|
||||
# Y@2 is clean; Z re-emitted (dead-lettered at seq 3, healed by seq 4 winning
|
||||
# the per-key max_by merge — Finding A's live-canary shape).
|
||||
"$STORE" enqueue --class actionable --locators '{"kind":"repo","id":"X","observed_hash":"HX"}' >/dev/null
|
||||
"$STORE" enqueue --class actionable --locators '{"kind":"repo","id":"Y","observed_hash":"HY"}' >/dev/null
|
||||
"$STORE" enqueue --class actionable --locators '{"kind":"repo","id":"Z","observed_hash":"HZ-OLD"}' >/dev/null
|
||||
"$STORE" enqueue --class actionable --locators '{"kind":"repo","id":"Z","observed_hash":"HZ-NEW"}' >/dev/null
|
||||
{
|
||||
printf '%s\n' '{"observed_seq":1,"locators":{"kind":"repo","id":"X","observed_hash":"HX"},"class":"actionable","emit_ts":1,"hmac":""}'
|
||||
printf '%s\n' '{"observed_seq":3,"locators":{"kind":"repo","id":"Z","observed_hash":"HZ-OLD"},"class":"actionable","emit_ts":1,"hmac":""}'
|
||||
} >"$dl"
|
||||
# Pre-#946-shaped consume: NO quarantined.set exists, so this consume writes
|
||||
# the false witness for X@1 exactly as the live defect did.
|
||||
"$STORE" consume --upto 4 >/dev/null 2>&1 || fail_msg "T16: baseline consume failed"
|
||||
jq_any "$rec" '.id=="X" and .observed_seq==1' || fail_msg "T16: fixture broken — the false X@1 row was not written"
|
||||
# REPORT: exactly the X row is provably false; non-zero exit signals findings.
|
||||
rep="$TMP_ROOT/t16.rep"
|
||||
if "$STORE" quarantine-audit >"$rep" 2>&1; then
|
||||
fail_msg "T16: report-mode audit must exit NON-ZERO when false rows exist"
|
||||
fi
|
||||
grep -q 'FALSE WITNESS' "$rep" || fail_msg "T16: the audit must name the false row loudly [$(cat "$rep")]"
|
||||
grep -q '"id":"X"' "$rep" || fail_msg "T16: the audit must identify the false row (repo/X@1) [$(cat "$rep")]"
|
||||
grep -q '"id":"Y"' "$rep" && fail_msg "T16: the clean row repo/Y must NOT be flagged"
|
||||
grep -q '"id":"Z"' "$rep" && fail_msg "T16: the HEALED row repo/Z@4 must NOT be flagged (its dead-letter evidence is seq 3 with a different hash)"
|
||||
# Report mode modifies nothing.
|
||||
jq_any "$rec" '.id=="X"' || fail_msg "T16: report mode must not modify the record"
|
||||
# REPAIR: exactly the false row is removed; the dead-letter LEDGER is history
|
||||
# and must never be modified.
|
||||
"$STORE" quarantine-audit --repair >"$TMP_ROOT/t16.fix" 2>&1 || fail_msg "T16: --repair must succeed [$(cat "$TMP_ROOT/t16.fix")]"
|
||||
jq_any "$rec" '.id=="X"' && fail_msg "T16: --repair must REMOVE the provably-false X row"
|
||||
jq_any "$rec" '.id=="Y" and .observed_hash=="HY"' || fail_msg "T16: --repair must keep the clean Y row"
|
||||
jq_any "$rec" '.id=="Z" and .observed_hash=="HZ-NEW" and .observed_seq==4' || fail_msg "T16: --repair must keep the healed Z@4 row"
|
||||
[ "$(grep -c . "$dl")" = "2" ] || fail_msg "T16: the dead-letter LEDGER must be untouched by --repair"
|
||||
# Clean re-audit: OK, exit 0.
|
||||
"$STORE" quarantine-audit >"$TMP_ROOT/t16.ok" 2>&1 || fail_msg "T16: a clean audit must exit 0 [$(cat "$TMP_ROOT/t16.ok")]"
|
||||
grep -qi 'OK' "$TMP_ROOT/t16.ok" || fail_msg "T16: a clean audit must say OK [$(cat "$TMP_ROOT/t16.ok")]"
|
||||
) && ok
|
||||
|
||||
echo
|
||||
if [ -s "$FAILFILE" ]; then
|
||||
echo "wake store/ack harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"lint": "eslint src",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell",
|
||||
"test:framework-shell": "python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh"
|
||||
"test:framework-shell": "python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-install.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mosaicstack/brain": "workspace:*",
|
||||
|
||||
Reference in New Issue
Block a user