fix(issue-23): the remote audit record names the dispatch it vouches for; a record without identity fails closed

Review round 4 (planner-reviewer-claude-01 B-1, planner-reviewer-codex-01
P2, reached independently): dispatch_verified is a boolean. It says the
subscriber's token check ran when the record was written, but names no
dispatch, the token being masked by the audit redactor and attempt_id not
written. events.ndjson is appended to across a job's attempts, so a started
verified under attempt 1 satisfied every later attempt's wait - the very
misattribution R1 forbids, alive on the remote path while the local path
(data.dispatch_token in events.log) was already immune. codex reproduced it
with the real signed producer, the real _Watcher, the real audit writer and
the real operator re-dispatch; both waits answered 0 where 1 was due, even
when attempt 2's subscriber had correctly dropped attempt 1's replayed packet.

The subscriber now writes dispatch_fingerprint = SHA-256 of the token it
verified, and attempt_id for readers. With a token demanded, the predicate
accepts an audit record only if it is dispatch_verified AND its fingerprint
equals SHA-256 of that token; a record with no fingerprint - written before
the field existed - names nothing and is rejected on that path. Without a
token, any received started counts, as before.

Fingerprint over attempt_id as the identity, weighing the two proposals: the
local path already compares the token, so hashing it keeps one identity axis
for both sources; the token is a uuid4 nonce (registry.py:141), 128 bits, so
its SHA-256 is not recoverable and it is a CAS fence rather than the HMAC
secret in the first place; and attempt_id cannot tell apart a token
re-minted within one attempt, where the token can. attempt_id is recorded
because it is cheap and readable, not relied on.

W7 pins codex's five cases end to end - current dispatch accepted, an old
attempt's record rejected after a real re-dispatch, still rejected when the
stale packet is replayed and dropped, the new dispatch's own started
accepted, a bad HMAC never recorded - with create's wait and the wrapper's
wait required to agree on each. W8 pins the fail-closed legacy record and
the untokened compatibility path. W5 gains the no-fingerprint and
other-dispatch cases; W6 checks the real _Watcher writes the fingerprint and
never the token. S7e's status reset had no reason left and is gone. codex's
own reproduction file, unmodified, passes 5/5 against this tree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Fable 5.1 committed 2026-09-22 17:52:51 +00:00
1 parent 087eacaf21
commit f4ba205c9f
3 files changed
+148 -15

No files matched your search

+14 -4
View File
@@ -2233,8 +2233,15 @@ except Exception:
# `started` it received, the only evidence a REMOTE agent (no shared disk)
# can leave. The subscriber verifies the HMAC and drops any event whose
# data.dispatch_token differs from the job's expected token before it
# writes this record, and marks it dispatch_verified when that check ran;
# with <dispatch_token> given, only a dispatch_verified record counts.
# writes this record (dispatch_verified), and writes dispatch_fingerprint
# = SHA-256 of that token. The file is appended to across a job's
# attempts, so the flag alone would let an earlier attempt's record
# acknowledge a later dispatch (review round 4): with <dispatch_token>
# given, a record counts only if it is dispatch_verified AND its
# fingerprint equals SHA-256(<dispatch_token>). A record with no
# fingerprint (written before this field existed) names no dispatch and
# is rejected on that path - fail closed. Without a token, any received
# started counts, as before.
#
# Never the registry status: a record leaves `pending` when another writer
# closes it or another dispatch claims it. Lines are parsed as JSON, never
@@ -2247,8 +2254,9 @@ delegate_started_recorded() {
local audit="$logs_dir/$job_id/events.ndjson"
[ -f "$ev_log" ] || [ -f "$audit" ] || return 1
EV_LOG="$ev_log" EV_AUDIT="$audit" EV_TOK="$tok" python3 -c '
import json, os, sys
import hashlib, json, os, sys
tok = os.environ.get("EV_TOK", "")
fp = hashlib.sha256(tok.encode("utf-8")).hexdigest() if tok else ""
def lines(path):
if not os.path.isfile(path):
@@ -2271,7 +2279,9 @@ for e in lines(os.environ["EV_LOG"]):
for e in lines(os.environ["EV_AUDIT"]):
if e.get("event") != "received" or e.get("source_event") != "started":
continue
if not tok or e.get("dispatch_verified") is True:
if not tok:
sys.exit(0)
if e.get("dispatch_verified") is True and e.get("dispatch_fingerprint") == fp:
sys.exit(0)
sys.exit(1)
' 2>/dev/null