From 67f7cb6df4afe137c5e90d1d3257399318c4fb09 Mon Sep 17 00:00:00 2001 From: mosaic-coder Date: Sat, 25 Jul 2026 19:44:56 -0500 Subject: [PATCH] fix(wake): digest hard-locator gate covers top-level .claim entries The W3 fail-loud hard-locator validator (digest.sh) treated an entry as actionable via class=="actionable" OR .locators.claim, but the RENDER tier also honors a top-level .claim (it renders via `jq -r '.claim // .locators.claim'`). A hand-crafted entry like {"class":"reaction","claim":"mergeable=true","locators":{}} fed via --stdin/--from-file rendered as a CLAIM@seq with an EMPTY locator, escaping the exit-4 hard-locator fail-loud gate. store.sh never emits a top-level .claim (non-canonical shape), but a non-canonical caller could bypass the gate. Align the validator to also treat a top-level .claim as actionable, matching the render tier's precedence, so any entry that WILL render as a claim must pass the hard -locator gate or FAIL LOUD (exit 4, nothing emitted). Closes #905 Part of #892 --- .../mosaic/framework/tools/wake/digest.sh | 7 ++++- .../tools/wake/test-wake-digest-hmac.sh | 27 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/mosaic/framework/tools/wake/digest.sh b/packages/mosaic/framework/tools/wake/digest.sh index 9013ae88..df02ff0d 100755 --- a/packages/mosaic/framework/tools/wake/digest.sh +++ b/packages/mosaic/framework/tools/wake/digest.sh @@ -229,7 +229,9 @@ cmd_render() { # --- FAIL-LOUD PASS FIRST (ยง2.1): validate every actionable claim carries a # hard locator BEFORE emitting a single byte. A malformed digest must never be # partially sent. Actionable-tier = class "actionable" OR any entry whose - # locators carry a `claim` (a consequential fact). ------------------------ + # locators carry a `claim` OR a top-level `claim` (a consequential fact) โ€” + # matching the render tier's `.claim // .locators.claim` precedence, so no + # entry can render as a CLAIM@seq without passing this gate. ------------- local line class loc is_actionable while IFS= read -r line; do [ -n "$line" ] || continue @@ -241,6 +243,9 @@ cmd_render() { if jq -e 'has("claim") and ((.claim // "") != "")' >/dev/null 2>&1 <<<"$loc"; then is_actionable=1 fi + if jq -e 'has("claim") and ((.claim // "") != "")' >/dev/null 2>&1 <<<"$line"; then + is_actionable=1 + fi if [ "$is_actionable" = "1" ] && ! _has_hard_locator "$loc"; then local seq seq="$(jq -r '.observed_seq // "?"' <<<"$line")" diff --git a/packages/mosaic/framework/tools/wake/test-wake-digest-hmac.sh b/packages/mosaic/framework/tools/wake/test-wake-digest-hmac.sh index 776fd0d2..e3eb4592 100755 --- a/packages/mosaic/framework/tools/wake/test-wake-digest-hmac.sh +++ b/packages/mosaic/framework/tools/wake/test-wake-digest-hmac.sh @@ -117,6 +117,33 @@ echo "== D2: HARD-LOCATOR enforcement โ€” actionable claim with no locator FAILS export WAKE_STATE_HOME "$STORE" enqueue --seq 1 --class actionable --locators "$(jq -cn --arg s "$SHA40" '{claim:"ci=green",sha:$s}')" >/dev/null "$DIGEST" render >/dev/null 2>&1 || fail_msg "D2: a well-located actionable claim must render" + # --- #905: a NON-CANONICAL entry with a TOP-LEVEL `.claim` (store.sh never + # emits this shape; only .locators.claim is canonical) and EMPTY .locators, + # fed via --stdin / --from-file, must ALSO fail loud. The render tier honors + # `.claim // .locators.claim` (renders CLAIM@seq either way), so the + # fail-loud gate must cover top-level .claim too, else a hand-crafted / + # non-canonical caller could bypass the hard-locator gate entirely. + WAKE_STATE_HOME="$(fresh_state d2d)" + export WAKE_STATE_HOME + toplevel_claim_entry='{"class":"reaction","claim":"mergeable=true","locators":{}}' + out="" + if out="$(printf '%s\n' "$toplevel_claim_entry" | "$DIGEST" render --stdin 2>/dev/null)"; then + fail_msg "D2(#905): top-level .claim + empty locators must FAIL via --stdin, but render succeeded" + fi + [ -z "${out:-}" ] || fail_msg "D2(#905): --stdin top-level-.claim bypass emitted body instead of failing loud [$out]" + rc=0 + printf '%s\n' "$toplevel_claim_entry" | "$DIGEST" render --stdin >/dev/null 2>&1 || rc=$? + [ "$rc" -eq 4 ] || fail_msg "D2(#905): expected fail-loud exit 4 for top-level .claim via --stdin, got $rc" + ff="$TMP_ROOT/d2d-entry.jsonl" + printf '%s\n' "$toplevel_claim_entry" >"$ff" + out2="" + if out2="$("$DIGEST" render --from-file "$ff" 2>/dev/null)"; then + fail_msg "D2(#905): top-level .claim + empty locators must FAIL via --from-file, but render succeeded" + fi + [ -z "${out2:-}" ] || fail_msg "D2(#905): --from-file top-level-.claim bypass emitted body instead of failing loud [$out2]" + rc=0 + "$DIGEST" render --from-file "$ff" >/dev/null 2>&1 || rc=$? + [ "$rc" -eq 4 ] || fail_msg "D2(#905): expected fail-loud exit 4 for top-level .claim via --from-file, got $rc" ) && ok echo "== D3: TWO-TIER โ€” orientation no-op with ZERO tool calls; actionable = claim-to-verify =="