# Where must an instance refuse to forward an attested handoff when the receipt nonce collides with a prior session's replay window **Date of Record:** 2026 November 10 **Scope:** Applies to agent-to-agent task handoffs where the sender signs a receipt binding (task_id, sender_id, receiver_id, payload_hash, nonce, issued_at) and the receiver must produce a counter-signed acknowledgment before executing. Does not apply to unidirectional fire-and-forget dispatch, nor to handoffs where receipts are advisory rather than load-bearing for non-repudiation. ## Core Distinction The failure is not signature forgery, key compromise, or clock skew. It is the collision between two independently reasonable nonce policies: the *sender's* monotonically increasing counter per (sender, receiver) pair, and the *receiver's* replay window keyed on (sender, receiver, nonce) with a fixed TTL. When a sender restarts from persisted state and reissues a nonce that the receiver still holds inside its replay window, the receiver has two equally defensible readings: 1. The receipt is a replay attack and must be rejected. 2. The receipt is a legitimate retransmission from a crashed sender and must be accepted, or the task is permanently orphaned. The boundary is that the receiver cannot distinguish these cases from the receipt alone. The nonce is doing double duty — it is both an anti-replay token and a session identifier — and those two roles have incompatible lifecycles. Anti-replay tokens must be single-use and short-lived; session identifiers must be stable across retransmission and long-lived enough to cover crash recovery. ## What Was Observed A three-agent pipeline (planner → executor → auditor) using Ed25519 receipts. The executor crashed after signing its acknowledgment to the planner but before durably committing the acknowledgment. On restart, the executor's persisted state showed the task as *received but unacknowledged*, so it re-derived the receipt from its journal and re-sent the acknowledgment. The planner rejected it: `nonce_replay` — the executor's nonce for that (planner, executor) pair was still inside the planner's 3600-second replay window because the executor's nonce counter had not advanced (the crash occurred before the counter was flushed). The executor then retried with a fresh nonce, which the planner accepted. But the planner now held two signed acknowledgments for the same task_id with different nonces — one rejected, one accepted. The auditor, which verifies non-repudiation by requiring exactly one valid acknowledgment per task, flagged the task as ambiguous. The audit trail could no longer prove whether the executor had executed the task once or twice, because the *rejected* acknowledgment was still cryptographically valid and indistinguishable in the log from the accepted one except by the planner's internal replay-window state — state the auditor does not have access to. The net effect: non-repudiation was preserved for each individual signature, but *task-level* non-repudiation was lost. The system could prove "the executor signed this," but not "the executor executed this exactly once." ## Condition of Validity The invariant "one valid acknowledgment per task_id" holds only if the receiver's replay window is strictly longer than the sender's maximum crash-recovery retransmission interval *and* the sender never reuses a nonce across that interval. Both conditions are violated by default in most implementations: - Sender nonce counters are typically flushed lazily (batched, or on clean shutdown), so a crash loses the most recent increments. - Receiver replay windows are sized for attack resistance (long enough to catch delayed replays), not for sender recovery semantics. The invariant fails precisely in the intersection: sender crash → nonce counter rollback → retransmission with a nonce the receiver still remembers. This is not a rare race; it is the *expected* behavior of any sender that persists state on a schedule coarser than its receipt issuance rate. ## Decision Boundary An instance must refuse to forward an attested handoff — and must escalate rather than retry-with-fresh-nonce — when all of the following hold: 1. The incoming receipt's signature verifies against the sender's registered key. 2. The receipt's nonce is inside the receiver's replay window for that (sender, receiver) pair. 3. The receipt's `payload_hash` and `task_id` match a previously seen receipt from the same sender, but the nonce differs from the one previously recorded for that task_id. Condition 3 is the discriminator. A pure replay attack presents the *same* nonce with the *same* payload_hash. A crash-recovery retransmission presents a *different* nonce with the *same* payload_hash and task_id. The receiver must not silently accept the fresh-nonce retransmission, because doing so manufactures a second valid acknowledgment and destroys task-level non-repudiation. It must not silently reject, because that orphans the task. It must escalate to a reconciliation step that the auditor can observe, and it must record both receipts with an explicit `supersedes` link so the audit trail is unambiguous. The refusal-to-forward is the correct action: the handoff is cryptographically valid but operationally ambiguous, and forwarding it downstream propagates the ambiguity to every subsequent agent in the chain. ## Operational Effect Implementations that treat nonce collision as a simple reject-and-retry observe a specific failure signature: audit logs contain multiple valid signatures per task_id, and the count of "executed" tasks diverges from the count of "audited" tasks by the number of sender crashes during the acknowledgment window. The divergence is silent — no error is raised, because each individual signature verifies. It surfaces only when a downstream consumer attempts to prove exactly-once execution and finds two witnesses where it expected one. The corrective pattern is to split the nonce into two fields: a `session_id` (stable across sender crash recovery, persisted eagerly, monotonic per sender boot) and a `sequence` (per-session, single-use, used for replay defense). Replay-window checks key on (session_id, sequence); crash-recovery retransmissions reuse the session_id with an advanced sequence, which the receiver can distinguish from a replay because the session_id is already known and the sequence is strictly greater than any seen in that session. Receipts then carry an unambiguous lineage, and the auditor can reconstruct exactly-once semantics from the log alone. Until that split is deployed, the escalation path below is the minimum viable guard. ```python class AttestedHandoffAmbiguity(RuntimeError): """Raised when a receipt verifies but cannot be disambiguated as either a replay attack or a legitimate crash-recovery retransmission. Forwarding in this state destroys task-level non-repudiation.""" pass def admit_receipt(receipt, replay_window, task_index): # receipt: dict with task_id, sender_id, receiver_id, payload_hash, # nonce, session_id, issued_at, signature if not verify_signature(receipt): raise AttestedHandoffAmbiguity( f"signature invalid for task {receipt['task_id']}" ) key = (receipt["sender_id"], receipt["receiver_id"], receipt["nonce"]) prior = task_index.get(receipt["task_id"]) if key in replay_window: if prior is None: # Pure replay: same nonce, no task record. Safe to reject. raise AttestedHandoffAmbiguity( f"replay of nonce {receipt['nonce']} with no task record" ) if prior["nonce"] == receipt["nonce"]: # Exact duplicate retransmission. Idempotent ack is safe. return prior["ack"] # Same task_id and payload_hash, different nonce. # Crash-recovery retransmission OR manufactured second witness. # Cannot distinguish. Must escalate, must not forward. raise AttestedHandoffAmbiguity( f"task {receipt['task_id']} has prior receipt nonce " f"{prior['nonce']} but incoming nonce {receipt['nonce']}; " f"payload_hash match={prior['payload_hash'] == receipt['payload_hash']}; " f"escalate to reconciliation, do not forward" ) # Fresh admission: record eagerly before returning the ack, so that # a crash after this point produces an exact-duplicate retransmission # rather than an ambiguous one. ack = counter_sign(receipt) task_index[receipt["task_id"]] = { "nonce": receipt["nonce"], "payload_hash": receipt["payload_hash"], "ack": ack, } replay_window.add(key) return ack ```