#!/usr/bin/env bash # test-wake-digest-hmac.sh — RED-FIRST invariant harness for W3 (EPIC #892): # the cumulative-state digest renderer (digest.sh, A3) + the non-circular HMAC # signer (sign.sh, A5). # # Each test asserts ONE CONVERGED-DESIGN invariant and is designed to go RED if # that invariant regresses: # D1 CUMULATIVE-STATE: two still-pending changes both render (not just the # latest delta) — a delta would silently drop the older change. (§2.1) # D2 HARD-LOCATOR enforcement: an actionable claim with no precise locator is # QUARANTINED — dead-lettered + alarmed + excluded — never delivered as # valid (fail-loud PER-ENTRY, #920; the rest of the drain still renders). (§2.1) # D3 TWO-TIER trust: orientation decides the no-op case with ZERO tool calls; # an actionable fact is a CLAIM-TO-VERIFY, never auto-actioned. (§2.1) # D4 SCRUB: a secret-canary + ANSI/bidi/zero-width in SOURCE free-text is # stripped/neutralized in the rendered digest. (§2.1) # H1 NON-CIRCULAR HMAC: wake_id is NOT a member of the signed field-tuple and # is independently generated; wake_mac is not its own input; tampering ANY # signed field (or wake_id) breaks the MAC. (§2.5) # H2 KEY BY-NAME, NEVER INLINE: the key is resolved by NAME from the credential # store; no flag accepts key material; the key never leaks into output; the # same-uid threat boundary is documented. (§2.5) # D5 (#914a) EMBEDDED ACK NAMESPACE: the copy-run ack line rendered at # WAKE_AGENT=X render time targets X EXPLICITLY, so an env-less copy-run # still resolves to the correct per-agent namespace (never silently # falls back to `default`). (§2.1/§2.2) # D6 (#914b) DIGEST-CLASS LOCATOR THREADING: an ORIENTATION pointer for a # digest-class entry (the shape detector.sh actually enqueues: # kind/id/observed_hash/remote/path, not repo/issue/sha/file) carries a # non-empty, usable locator — and the ACTIONABLE-tier hard-locator # FAIL-LOUD is PRESERVED (now per-entry quarantine, #920) for a malformed # actionable claim. (§2.1) # # Isolated: every test runs against a fresh temp state / credential file. set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" STORE="$SCRIPT_DIR/store.sh" DIGEST="$SCRIPT_DIR/digest.sh" SIGN="$SCRIPT_DIR/sign.sh" command -v jq >/dev/null 2>&1 || { echo "SKIP: jq not available" >&2 exit 0 } # openssl gates the HMAC legs (H1/H2). #912: the wake trust layer MUST be # exercised in real CI, so when running under CI (Woodpecker sets CI=woodpecker) # openssl is HARD-REQUIRED — a missing openssl fails the suite LOUD rather than # silently skipping the signer (the §4 G6 evidence must come from an # actually-run HMAC leg, not a skipped one). In an openssl-less LOCAL DEV env # the whole suite still skips so `pnpm test` stays runnable without openssl. if ! command -v openssl >/dev/null 2>&1; then if [ -n "${CI:-}" ]; then echo "FATAL (#912): openssl is REQUIRED in CI to exercise the wake digest/HMAC trust layer, but is not on PATH. The CI image must provide openssl (see .woodpecker/ci-image.yml)." >&2 exit 1 fi echo "SKIP: openssl not available (local dev; CI hard-requires it)" >&2 exit 0 fi TMP_ROOT="$(mktemp -d)" trap 'rm -rf "$TMP_ROOT"' EXIT # Failures recorded to a marker FILE (not a shell var): each test runs in a # subshell for env isolation and a subshell cannot mutate a parent variable, so # a var-based counter would silently swallow failures. 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" } # A 40-hex sha, for hard locators. SHA40="abcdef0123456789abcdef0123456789abcdef01" # fresh_cred NAME KEYNAME KEYVALUE — write a temp credential store with a wake # HMAC key at .wake.hmac_keys. and echo its path. fresh_cred() { local f="$TMP_ROOT/$1.cred.json" jq -cn --arg k "$2" --arg v "$3" '{wake:{hmac_keys:{($k):$v}}}' >"$f" printf '%s' "$f" } # =========================================================================== echo "== D1: CUMULATIVE-STATE — two pending changes BOTH render (not just latest) ==" ( WAKE_STATE_HOME="$(fresh_state d1)" export WAKE_STATE_HOME unset WAKE_AGENT WAKE_LANE # Two distinct obligations observed since the last CONSUMED. A delta-style # renderer would show only the newest; cumulative-state must show BOTH. "$STORE" enqueue --seq 1 --class actionable --locators '{"repo":"r","issue":11}' >/dev/null "$STORE" enqueue --seq 2 --class actionable --locators '{"repo":"r","issue":22}' >/dev/null out="$("$DIGEST" render)" || fail_msg "D1: render exited non-zero" echo "$out" | grep -q 'issue=#11' || fail_msg "D1: OLDER change (issue #11) dropped — digest is a delta, not cumulative state" echo "$out" | grep -q 'issue=#22' || fail_msg "D1: newer change (issue #22) missing" echo "$out" | grep -q 'seq 1' || fail_msg "D1: seq 1 not listed in cumulative set" echo "$out" | grep -q 'seq 2' || fail_msg "D1: seq 2 not listed in cumulative set" # A coalescing digest-class entry is STATE (full), not a delta: a later digest # subsumes the earlier, but the cumulative unacked set (both actionables) stays. echo "$out" | grep -q 'pending=2' || fail_msg "D1: cumulative pending count wrong (expected 2)" ) && ok echo "== D2: HARD-LOCATOR enforcement — a malformed actionable claim is QUARANTINED (fail-loud PER-ENTRY, #920); never delivered as valid ==" ( h="$(fresh_state d2)" WAKE_STATE_HOME="$h" export WAKE_STATE_HOME unset WAKE_AGENT WAKE_LANE # An actionable claim carrying NO hard locator (no repo+issue / sha / file). "$STORE" enqueue --seq 1 --class actionable --locators '{"claim":"mergeable=true"}' >/dev/null err="$TMP_ROOT/d2.err" rc=0 out="$("$DIGEST" render 2>"$err")" || rc=$? # #920: NO longer a whole-digest exit-4 — the malformed entry is QUARANTINED # (dead-lettered + alarmed + excluded) and the digest STILL renders (exit 0), # but the unlocated claim is NEVER delivered as a valid CLAIM (fail-loud is # preserved, now per-entry). The old exit-4 wedged the entire drain (#920). [ "$rc" -eq 0 ] || fail_msg "D2: a malformed actionable must be quarantined (render exit 0, #920), got $rc" printf '%s' "$out" | grep -q 'mergeable=true' && fail_msg "D2: an unlocated actionable claim must NOT be delivered as a valid CLAIM" grep -q 'mergeable=true' "$h/default/dead-letter.jsonl" 2>/dev/null || fail_msg "D2: the malformed claim must be DEAD-LETTERED (fail-loud preserved, per-entry)" grep -qi 'QUARANTINE' "$err" || fail_msg "D2: a LOUD per-entry alarm must fire for the malformed claim" # A present-but-imprecise sha (not 40 hex) does NOT satisfy the hard locator -> # quarantined, not delivered. h="$(fresh_state d2b)" WAKE_STATE_HOME="$h" export WAKE_STATE_HOME "$STORE" enqueue --seq 1 --class actionable --locators '{"claim":"ci=green","sha":"abc123"}' >/dev/null rc=0 out="$("$DIGEST" render 2>/dev/null)" || rc=$? [ "$rc" -eq 0 ] || fail_msg "D2: imprecise-sha entry must be quarantined (render exit 0), got $rc" printf '%s' "$out" | grep -q 'ci=green' && fail_msg "D2: imprecise sha (not 40-hex) must not satisfy the hard-locator gate (claim must not deliver)" grep -q 'ci=green' "$h/default/dead-letter.jsonl" 2>/dev/null || fail_msg "D2: imprecise-sha claim must be dead-lettered" # The SAME claim WITH a precise 40-hex sha renders fine (delivered, not quarantined). h="$(fresh_state d2c)" WAKE_STATE_HOME="$h" export WAKE_STATE_HOME "$STORE" enqueue --seq 1 --class actionable --locators "$(jq -cn --arg s "$SHA40" '{claim:"ci=green",sha:$s}')" >/dev/null out="$("$DIGEST" render 2>/dev/null)" || fail_msg "D2: a well-located actionable claim must render" printf '%s' "$out" | grep -q "$SHA40" || fail_msg "D2: a well-located actionable claim must be DELIVERED" [ -s "$h/default/dead-letter.jsonl" ] && fail_msg "D2: a well-located claim must NOT be quarantined" # --- #905 bypass-prevention (STILL enforced, now via quarantine): 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 be gated. The classifier honors `.claim // # .locators.claim`, so a hand-crafted caller cannot bypass the hard-locator gate # — the entry is QUARANTINED (not delivered), not silently rendered. h="$(fresh_state d2d)" WAKE_STATE_HOME="$h" export WAKE_STATE_HOME toplevel_claim_entry='{"observed_seq":1,"class":"reaction","claim":"mergeable=true","locators":{}}' rc=0 out="$(printf '%s\n' "$toplevel_claim_entry" | "$DIGEST" render --stdin 2>/dev/null)" || rc=$? [ "$rc" -eq 0 ] || fail_msg "D2(#905): top-level .claim must be quarantined via --stdin (exit 0), got $rc" printf '%s' "$out" | grep -q 'mergeable=true' && fail_msg "D2(#905): --stdin top-level-.claim must NOT be delivered (gate not bypassed)" grep -q 'mergeable=true' "$h/default/dead-letter.jsonl" 2>/dev/null || fail_msg "D2(#905): --stdin top-level .claim must be dead-lettered (gate enforced)" ff="$TMP_ROOT/d2d-entry.jsonl" printf '%s\n' "$toplevel_claim_entry" >"$ff" h="$(fresh_state d2e)" WAKE_STATE_HOME="$h" export WAKE_STATE_HOME rc=0 out2="$("$DIGEST" render --from-file "$ff" 2>/dev/null)" || rc=$? [ "$rc" -eq 0 ] || fail_msg "D2(#905): top-level .claim must be quarantined via --from-file (exit 0), got $rc" printf '%s' "$out2" | grep -q 'mergeable=true' && fail_msg "D2(#905): --from-file top-level-.claim must NOT be delivered (gate not bypassed)" grep -q 'mergeable=true' "$h/default/dead-letter.jsonl" 2>/dev/null || fail_msg "D2(#905): --from-file top-level .claim must be dead-lettered (gate enforced)" ) && ok echo "== D3: TWO-TIER — orientation no-op with ZERO tool calls; actionable = claim-to-verify ==" ( WAKE_STATE_HOME="$(fresh_state d3)" export WAKE_STATE_HOME unset WAKE_AGENT WAKE_LANE # Tool-call tripwire: shim git/gh/tea/curl on PATH; any invocation touches a # marker. The orientation no-op path must decide with ZERO of them. bin="$TMP_ROOT/d3bin" mkdir -p "$bin" for t in git gh tea curl wget; do { printf '#!/usr/bin/env bash\n' printf 'touch "%s/TOOL_CALLED"\n' "$WAKE_STATE_HOME" printf 'exit 0\n' } >"$bin/$t" chmod +x "$bin/$t" done # No pending obligations => no-op common case. out="$(PATH="$bin:$PATH" "$DIGEST" render --lane build)" || fail_msg "D3: no-op render failed" echo "$out" | grep -q 'NO-OP' || fail_msg "D3: empty inbox must render an explicit NO-OP orientation" [ -e "$WAKE_STATE_HOME/TOOL_CALLED" ] && fail_msg "D3: orientation no-op made a live tool call (must be ZERO)" # Now an actionable, consequential fact. It must be a CLAIM-TO-VERIFY, never # rendered as a trusted assertion or an auto-action. "$STORE" enqueue --seq 1 --class actionable \ --locators "$(jq -cn --arg s "$SHA40" '{repo:"r",issue:7,claim:"ci=success",file:"pkg/x.ts",sha:$s}')" >/dev/null out2="$(PATH="$bin:$PATH" "$DIGEST" render)" || fail_msg "D3: actionable render failed" # Rendering itself still makes ZERO live calls (it only lays out claims). [ -e "$WAKE_STATE_HOME/TOOL_CALLED" ] && fail_msg "D3: rendering an actionable claim made a live call (must defer to the consumer's gate)" echo "$out2" | grep -q 'CLAIM@seq' || fail_msg "D3: consequential fact not framed as CLAIM@seq" echo "$out2" | grep -qi 'VERIFY LIVE' || fail_msg "D3: claim not marked for live verification" echo "$out2" | grep -qi 'do NOT act on this line' || fail_msg "D3: claim missing do-not-auto-action framing" # The consequential fact must NOT appear as a bare trusted directive. echo "$out2" | grep -qiE 'merge now|go ahead and (merge|deploy)|safe to merge' && fail_msg "D3: digest auto-actioned a consequential fact (imperative present)" # And its hard locator + one-call re-verify hint are present. echo "$out2" | grep -q "re-verify (ONE call)" || fail_msg "D3: actionable claim missing one-call re-verify locator" true ) && ok echo "== D4: SCRUB — secret-canary + ANSI/bidi/zero-width in source free-text neutralized ==" ( WAKE_STATE_HOME="$(fresh_state d4)" export WAKE_STATE_HOME unset WAKE_AGENT WAKE_LANE # Build hostile source free-text: ANSI CSI, bidi override (U+202E), zero-width # space (U+200B), ZWNBSP (U+FEFF), a C0 control (BEL), and two secret canaries. nasty="$(printf 'good\x1b[31mRED\xe2\x80\xaeEVIL\xe2\x80\x8bZW\xef\xbb\xbfBOM\x07BEL ghp_0123456789abcdefghijABCDEFGHIJ012345 AKIAIOSFODNN7EXAMPLE')" loc="$(jq -cn --arg s "$nasty" --arg sha "$SHA40" '{repo:"r",issue:3,claim:"changed",sha:$sha,summary:$s}')" "$STORE" enqueue --seq 1 --class actionable --locators "$loc" >/dev/null out="$("$DIGEST" render)" || fail_msg "D4: render failed" # ANSI escape / CSI must be gone. printf '%s' "$out" | LC_ALL=C grep -q "$(printf '\x1b')" && fail_msg "D4: ANSI ESC survived the scrub" # bidi/zero-width/BOM UTF-8 sequences must be gone. # #912: patterns are LITERAL bytes + `grep -E`, NOT PCRE `grep -P`. BusyBox # grep (Alpine/musl CI) has no `-P` — a `grep -qP` there errors # ("unrecognized option: P"), returns non-zero, and the `&&` silently skips # the assertion, so the scrub was NEVER checked in CI. Literal-byte ranges # under `grep -E` + LC_ALL=C match identically on BusyBox and GNU grep. # Two DISJOINT byte ranges: U+200B..U+200F (E2 80 8B..8F, zero-width) and # U+202A..U+202E (E2 80 AA..AE, bidi). NOT a single 8B..AE range — that would # wrongly flag legitimate E2 80 xx punctuation in between, e.g. U+2014 EM DASH # (E2 80 94) which the digest body uses. _b280="$(printf '%b' '\xe2\x80')" _b8b="$(printf '%b' '\x8b')"; _b8f="$(printf '%b' '\x8f')" _baa="$(printf '%b' '\xaa')"; _bae="$(printf '%b' '\xae')" _bbom="$(printf '%b' '\xef\xbb\xbf')" printf '%s' "$out" | LC_ALL=C grep -qE "${_b280}[${_b8b}-${_b8f}${_baa}-${_bae}]|${_bbom}" && fail_msg "D4: bidi/zero-width/BOM survived the scrub" # C0 control bytes (except tab/newline) must be gone. _c00="$(printf '%b' '\x01')"; _c08="$(printf '%b' '\x08')" _c0e="$(printf '%b' '\x0e')"; _c1f="$(printf '%b' '\x1f')"; _c7f="$(printf '%b' '\x7f')" printf '%s' "$out" | LC_ALL=C grep -qE "[${_c00}-${_c08}${_c0e}-${_c1f}${_c7f}]" && fail_msg "D4: a C0 control byte survived the scrub" # Secret canaries must be redacted, never inlined. printf '%s' "$out" | grep -q 'ghp_0123456789' && fail_msg "D4: GitHub-token canary LEAKED into the digest" printf '%s' "$out" | grep -q 'AKIAIOSFODNN7EXAMPLE' && fail_msg "D4: AWS-key canary LEAKED into the digest" printf '%s' "$out" | grep -q 'REDACTED-SECRET' || fail_msg "D4: secret redaction marker absent — canary may not have been scrubbed" # Free-text is quoted inside a DELIMITED untrusted block, framed as NOT instructions. printf '%s' "$out" | grep -q 'BEGIN UNTRUSTED DATA' || fail_msg "D4: source free-text not placed in a delimited untrusted block" # The 40-hex git SHA locator must NOT be mangled by the secret scrubber. printf '%s' "$out" | grep -q "$SHA40" || fail_msg "D4: legitimate 40-hex SHA locator was wrongly scrubbed" true ) && ok echo "== H1: NON-CIRCULAR HMAC — wake_id independent + not a signed field; tamper breaks MAC ==" ( MOSAIC_CREDENTIALS_FILE="$(fresh_cred h1 default 'key-material-h1')" export MOSAIC_CREDENTIALS_FILE WAKE_AGENT=agentA WAKE_MISSION_GENERATION=3 export WAKE_AGENT WAKE_MISSION_GENERATION unset WAKE_HMAC_KEY_NAME env1="$("$SIGN" sign --observed-seq 5 --content-hash "$SHA40")" || fail_msg "H1: sign failed" # wake_id is NOT a member of the signed field-tuple (it is prepended to the MAC # input, not one of the signed fields whose authenticity the MAC establishes). echo "$env1" | jq -e '.signed | has("wake_id") | not' >/dev/null || fail_msg "H1: wake_id must NOT appear inside the signed field-tuple" # wake_mac is not its own input: it is not a member of `signed` either. echo "$env1" | jq -e '.signed | has("wake_mac") | not' >/dev/null || fail_msg "H1: wake_mac must not be part of its own signed inputs (self-reference)" echo "$env1" | jq -e 'has("wake_id") and has("wake_mac")' >/dev/null || fail_msg "H1: envelope must carry independent wake_id + wake_mac" # Independently generated: two emits over IDENTICAL fields yield DIFFERENT # wake_ids (id not derived from the tuple) AND different MACs (id is bound in). env2="$("$SIGN" sign --observed-seq 5 --content-hash "$SHA40")" w1="$(echo "$env1" | jq -r .wake_id)" w2="$(echo "$env2" | jq -r .wake_id)" [ -n "$w1" ] && [ "$w1" != "null" ] || fail_msg "H1: wake_id missing" [ "$w1" != "$w2" ] || fail_msg "H1: wake_id not independently generated (identical fields produced identical id — derived, not fresh)" m1="$(echo "$env1" | jq -r .wake_mac)" m2="$(echo "$env2" | jq -r .wake_mac)" [ "$m1" != "$m2" ] || fail_msg "H1: MAC did not bind the independent wake_id (same MAC despite different wake_id)" # Untampered verifies. echo "$env1" | "$SIGN" verify >/dev/null 2>&1 || fail_msg "H1: untampered envelope failed to verify" # Tampering ANY signed field breaks the MAC. for mut in \ '.signed.agent_identity="attacker"' \ '.signed.mission_generation="9"' \ '.signed.observed_seq="6"' \ '.signed.emit_ts="0"' \ '.signed.content_hash="deadbeef"'; do if echo "$env1" | jq "$mut" | "$SIGN" verify >/dev/null 2>&1; then fail_msg "H1: tampering [$mut] did NOT break the MAC" fi done # Tampering the independent wake_id also breaks the MAC (it is bound in). if echo "$env1" | jq '.wake_id="wake_forged"' | "$SIGN" verify >/dev/null 2>&1; then fail_msg "H1: forging wake_id did not break the MAC (id not bound)" fi # A wrong key fails verification (the MAC genuinely depends on the key). wrong="$(fresh_cred h1b default 'DIFFERENT-key')" if echo "$env1" | MOSAIC_CREDENTIALS_FILE="$wrong" "$SIGN" verify >/dev/null 2>&1; then fail_msg "H1: verify passed under a DIFFERENT key (MAC not key-dependent)" fi ) && ok echo "== H2: KEY BY-NAME, never inline; no key leak; same-uid boundary documented ==" ( MOSAIC_CREDENTIALS_FILE="$(fresh_cred h2 signing-key 'SUPERSECRET-KEYVALUE-XYZ')" export MOSAIC_CREDENTIALS_FILE WAKE_AGENT=agentB WAKE_MISSION_GENERATION=1 export WAKE_AGENT WAKE_MISSION_GENERATION # Resolve BY NAME (the credential-store key name), never the key material. env1="$(WAKE_HMAC_KEY_NAME=signing-key "$SIGN" sign --observed-seq 1 --content-hash "$SHA40")" || fail_msg "H2: by-name key resolution failed" echo "$env1" | jq -e .wake_mac >/dev/null || fail_msg "H2: no MAC produced from by-name key" # The key VALUE must never appear in the signed output. echo "$env1" | grep -q 'SUPERSECRET-KEYVALUE-XYZ' && fail_msg "H2: key material LEAKED into the signed envelope" # A signed store-entry (hmac placeholder filled) must not leak the key either. entry="$(printf '{"observed_seq":1,"locators":{"repo":"r","issue":1},"class":"actionable","emit_ts":1700000000,"hmac":""}' | WAKE_HMAC_KEY_NAME=signing-key "$SIGN" sign-entry)" echo "$entry" | grep -q 'SUPERSECRET-KEYVALUE-XYZ' && fail_msg "H2: key material LEAKED into the signed entry" echo "$entry" | jq -e '.hmac != "" and .hmac != null' >/dev/null || fail_msg "H2: sign-entry did not fill the hmac placeholder" # No flag may accept key MATERIAL inline — only a key NAME. An attempt to pass # a literal key must be rejected as an unknown option (never silently honored). for badflag in --key --secret --hmac-key --key-value; do if WAKE_HMAC_KEY_NAME=signing-key "$SIGN" sign --observed-seq 1 --content-hash "$SHA40" "$badflag" 'INLINE-KEY' >/dev/null 2>&1; then fail_msg "H2: sign.sh accepted an inline-key flag [$badflag] (key must be by-name only)" fi done # An unknown key NAME fails loud (never signs unsigned / with an empty key). if WAKE_HMAC_KEY_NAME=does-not-exist "$SIGN" sign --observed-seq 1 --content-hash "$SHA40" >/dev/null 2>&1; then fail_msg "H2: signing with an unresolvable key name must FAIL LOUD" fi # The same-uid threat boundary + off-uid follow-up must be DOCUMENTED in the tool. grep -qi 'same-uid' "$SIGN" || fail_msg "H2: same-uid threat boundary not documented in sign.sh" grep -qi 'off-uid' "$SIGN" || fail_msg "H2: off-uid future signer not named in sign.sh" ) && ok echo "== D5 (#914a): EMBEDDED ACK NAMESPACE — env-less copy-run targets the RENDER-TIME agent, not 'default' ==" ( WAKE_STATE_HOME="$(fresh_state d5)" export WAKE_STATE_HOME # A pending obligation observed under the 'someagent' namespace (render-time # WAKE_AGENT is known; the bug is that the EMBEDDED line forgets it). WAKE_AGENT=someagent "$STORE" enqueue --seq 1 --class actionable --locators '{"repo":"r","issue":41}' >/dev/null out="$(WAKE_AGENT=someagent "$DIGEST" render)" || fail_msg "D5: render (WAKE_AGENT=someagent) failed" ack_line="$(printf '%s\n' "$out" | awk '/^-- ACK/{f=1;next} f && NF {print; exit}')" [ -n "$ack_line" ] || fail_msg "D5: no embedded ack line extracted from the digest" printf '%s' "$ack_line" | grep -q 'WAKE_AGENT=someagent' || fail_msg "D5: embedded ack line has no explicit WAKE_AGENT=someagent prefix — an env-less copy-run silently resolves to 'default' [$ack_line]" # Actually RUN it with NO WAKE_AGENT in the environment (the real failure # mode: a consumer copy-pastes the line into a fresh shell). ( unset WAKE_AGENT; env -u WAKE_AGENT sh -c "$ack_line" >/dev/null 2>&1 ) rc=$? [ "$rc" -eq 0 ] || fail_msg "D5: env-less copy-run of the embedded ack line exited non-zero (rc=$rc) [$ack_line]" someagent_consumed="$(cat "$WAKE_STATE_HOME/someagent/consumed_seq" 2>/dev/null || echo '?')" [ "$someagent_consumed" = "1" ] || fail_msg "D5: env-less copy-run did NOT advance the someagent namespace's consumed_seq (got '$someagent_consumed') — resolved to the wrong namespace" # The 'default' namespace must stay untouched — no silent fallback. if [ -f "$WAKE_STATE_HOME/default/consumed_seq" ]; then default_consumed="$(cat "$WAKE_STATE_HOME/default/consumed_seq" 2>/dev/null || echo 0)" [ "$default_consumed" = "0" ] || fail_msg "D5: env-less copy-run advanced the WRONG ('default') namespace instead of someagent" fi # --- injection-safety: a hostile agent value must not become a shell # injection vector when the embedded line is later copy-run, and must be # scrubbed (control/ANSI bytes stripped) like any other inlined value. wd="$TMP_ROOT/d5-wd" rm -rf "$wd" mkdir -p "$wd" WAKE_STATE_HOME2="$(fresh_state d5b)" # shellcheck disable=SC2016 # deliberately literal: this is the injection # payload under test, not an expression we want the test script to expand. nasty='$(touch INJECTED)nasty' WAKE_AGENT="$nasty" WAKE_STATE_HOME="$WAKE_STATE_HOME2" "$STORE" enqueue --seq 1 --class actionable --locators '{"repo":"r","issue":1}' >/dev/null out2="$(WAKE_AGENT="$nasty" WAKE_STATE_HOME="$WAKE_STATE_HOME2" "$DIGEST" render)" || fail_msg "D5: render with a hostile agent value failed" ack_line2="$(printf '%s\n' "$out2" | awk '/^-- ACK/{f=1;next} f && NF {print; exit}')" ( cd "$wd" && unset WAKE_AGENT && env -u WAKE_AGENT WAKE_STATE_HOME="$WAKE_STATE_HOME2" sh -c "$ack_line2" >/dev/null 2>&1 ) [ -e "$wd/INJECTED" ] && fail_msg "D5: a hostile WAKE_AGENT value achieved shell injection via the embedded ack line [$ack_line2]" true ) && ok echo "== D6 (#914b): DIGEST-CLASS LOCATOR THREADING — ORIENTATION pointer carries its (soft) locator; ACTIONABLE hard-locator FAIL-LOUD unchanged ==" ( WAKE_STATE_HOME="$(fresh_state d6)" export WAKE_STATE_HOME unset WAKE_AGENT WAKE_LANE # The REAL shape store.sh receives for a digest-class entry, per detector.sh's # own enqueue-building jq filter (kind/id/observed_hash + whichever of # repo/path/anchor/remote/branches the source def declares) — NOT # repo/issue/sha/file, which is what _locator_line originally recognized. loc="$(jq -cn '{kind:"repo", id:"r1", observed_hash:"deadbeefcafe0123456789abcdef0123456789abcdef0123456789abcdef01", remote:"example/repo"}')" "$STORE" enqueue --seq 1 --class digest --locators "$loc" >/dev/null out="$("$DIGEST" render)" || fail_msg "D6: render failed" orientline="$(printf '%s\n' "$out" | grep -E '^\s*\* seq 1 \[digest\]')" [ -n "$orientline" ] || fail_msg "D6: no ORIENTATION line found for seq 1" printf '%s' "$orientline" | grep -qE 'locator: *$' && fail_msg "D6: digest-class ORIENTATION pointer rendered an EMPTY locator despite a populated .locators field [$orientline]" # Must surface something a consumer can act on to re-verify. printf '%s' "$orientline" | grep -qE 'remote=example/repo|id=r1|kind=repo' || fail_msg "D6: digest-class ORIENTATION pointer does not carry a usable locator [$orientline]" # --- ACTIONABLE-tier hard-locator FAIL-LOUD must be PRESERVED (now PER-ENTRY # quarantine, #920): a digest-class ORIENTATION pointer (above) renders soft, # but a genuine ACTIONABLE claim with no hard locator is still fail-loud — it is # QUARANTINED (dead-lettered + alarmed + excluded), never delivered as valid. h="$(fresh_state d6b)" WAKE_STATE_HOME="$h" export WAKE_STATE_HOME "$STORE" enqueue --seq 1 --class actionable --locators '{"claim":"mergeable=true"}' >/dev/null err="$TMP_ROOT/d6b.err" rc=0 out="$("$DIGEST" render 2>"$err")" || rc=$? [ "$rc" -eq 0 ] || fail_msg "D6: a malformed actionable is now per-entry quarantined (render exit 0, #920), got $rc" printf '%s' "$out" | grep -q 'mergeable=true' && fail_msg "D6: a malformed actionable claim must NOT be delivered as valid (fail-loud preserved)" grep -q 'mergeable=true' "$h/default/dead-letter.jsonl" 2>/dev/null || fail_msg "D6: a malformed actionable must be DEAD-LETTERED (fail-loud preserved, per-entry)" grep -qi 'QUARANTINE' "$err" || fail_msg "D6: a malformed actionable must raise a loud per-entry alarm" ) && ok echo if [ -s "$FAILFILE" ]; then echo "wake digest/hmac harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2 exit 1 fi echo "wake digest/hmac harness: all invariants passed ($pass groups)"