Files
stack/packages/mosaic/framework/tools/wake/test-wake-digest-hmac.sh
mosaic-coder ef43cc3be8
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
fix(wake): #912 exercise the digest/HMAC trust suite in real CI
Make the wake digest/HMAC suite RUN and PASS in the real Woodpecker CI
runner (Alpine/musl, root), then hard-require the HMAC legs in CI.

Root cause of the runner-only D1/D4/D5/D6 failures (a masked-local run
passed, so it was runner-specific): a TOOLCHAIN divergence, not locale or
root. digest.sh's _scrub_ctrl used GNU-sed `\xNN` hex-escape byte matching.
The CI runner is node:24-alpine, whose sed is BusyBox — BusyBox sed REJECTS
a `\xNN` character range ("bad regex ... Invalid character range"), aborting
the whole scrub sed and silently VOIDING the scrub. Every scrubbed value
collapsed to empty, cascading into D1 (blank locators), D4 (no scrub/redact,
SHA blanked), D5 (blank agent prefix), D6 (blank [digest] class). Confirmed
by reproducing the exact 9-assertion failure in the ci-base image as root.

Fix (at the correct layer — a wake digest must render identically on any
runner):
- digest.sh _scrub_ctrl: patterns are now LITERAL bytes (printf %b), matching
  byte-identically under GNU sed (glibc dev) and BusyBox sed (Alpine CI).
  Verified identical output on both. Contract preserved: two-tier trust,
  exit-4 hard-locator FAIL-LOUD, secret-scrub, and 40-hex SHA preservation
  all unchanged — deterministic, not weakened.
- test-wake-digest-hmac.sh D4: replaced PCRE `grep -qP` (BusyBox grep has no
  -P; the `&&` silently skipped the check in CI) with portable literal-byte
  `grep -E` ranges (two disjoint bidi/zero-width ranges, excluding legit
  U+2014 em-dash).

CI enablement:
- Dockerfile.ci + .woodpecker/ci.yml test step: add openssl (the non-circular
  HMAC signer) so H1/H2, beacon B12, install I8 can run. The apk add in the
  test step covers PR pipelines before ci-base rebuilds.
- Flip the 3 openssl skip-guards (digest whole-file, beacon B12, install I8)
  to HARD-REQUIRE openssl when CI is set (Woodpecker CI=woodpecker) and FAIL
  loud if absent; KEEP the skip for openssl-less local dev.
- manifest.txt: wake 0.6.2 -> 0.6.3 (digest.sh scrub portability; precedent).

Red-first verified in the ci-base container (root): D4 catches a broken
redaction, H1 catches a tamper that doesn't break the MAC, B12/I8 catch a
corrupted signer.

Closes #912
Part of #892

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158NZqN2n2ymKFeJAZ4GUCb
2026-07-26 02:49:44 -05:00

415 lines
24 KiB
Bash
Executable File

#!/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
# FAILS LOUD (non-zero exit, nothing emitted). (§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 (exit 4) is UNCHANGED 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.<KEYNAME> 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 — actionable claim with no locator FAILS LOUD =="
(
WAKE_STATE_HOME="$(fresh_state d2)"
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
if out="$("$DIGEST" render 2>/dev/null)"; then
fail_msg "D2: malformed digest (actionable, no locator) must FAIL, but render succeeded"
fi
rc=0
"$DIGEST" render >/dev/null 2>&1 || rc=$?
[ "$rc" -eq 4 ] || fail_msg "D2: expected fail-loud exit 4, got $rc"
# Nothing emitted on stdout when malformed (no silent partial send).
[ -z "${out:-}" ] || fail_msg "D2: a malformed digest emitted body instead of failing loud [$out]"
# A present-but-imprecise sha (not 40 hex) does NOT satisfy the hard locator.
WAKE_STATE_HOME="$(fresh_state d2b)"
export WAKE_STATE_HOME
"$STORE" enqueue --seq 1 --class actionable --locators '{"claim":"ci=green","sha":"abc123"}' >/dev/null
rc=0
"$DIGEST" render >/dev/null 2>&1 || rc=$?
[ "$rc" -eq 4 ] || fail_msg "D2: imprecise sha (not 40-hex) must not satisfy the hard-locator gate (got exit $rc)"
# The SAME claim WITH a precise 40-hex sha renders fine.
WAKE_STATE_HOME="$(fresh_state d2c)"
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 =="
(
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 (exit 4) must be UNCHANGED ------
WAKE_STATE_HOME="$(fresh_state d6b)"
export WAKE_STATE_HOME
"$STORE" enqueue --seq 1 --class actionable --locators '{"claim":"mergeable=true"}' >/dev/null
rc=0
"$DIGEST" render >/dev/null 2>&1 || rc=$?
[ "$rc" -eq 4 ] ||
fail_msg "D6: actionable claim with no hard locator must still FAIL-LOUD exit 4 (got $rc) — regression in the unrelated hard-locator gate"
) && 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)"