security: close the four signed-ACK gaps without breaking the wire (#109)
Follow-up to #100. Each gap below was reported against the strict variant in #104
and is closed here compatibly, keeping the two-stage rollout.
1. Durable replay protection. AckReceiptStore + an ack_receipts table in
SQLiteDedupeOutboxStore give claim-once semantics on (sender_agent_id, nonce)
that survive a restart. The in-memory Set remains only as an explicit fallback,
and the daemon now logs a warning when it is the one in use.
2. Fast-ACK race. applyAckTransition accepts 'pending' alongside 'sent', so an ACK
arriving between publish() and markSent() is applied instead of being rejected
into a spurious retry. markSent() refuses to downgrade a terminal status, so the
late call cannot resurrect a settled row.
3. A2A raw-NACK sink. The bridge no longer resolves a pending task from an unsigned
{msgId, status} object. Only a SignedAckV1 verified against the pinned signing key
of its claimed sender is honoured; signingPublicKeys was added to the config.
4. WebSocket ACK path. processAckFrame verified nothing and called markAcked/markFailed
straight from the frame. It now performs the same checks as the NATS path — record
lookup, digest, conversation, recipient, peer, ack-subject binding, signature and
nonce claim — with unsigned frames still accepted until requireSignedAcks is set.
Also aligns five packages that declared @murmurv2/core ^0.2.0: npm could not satisfy
that from the workspace and silently installed 0.2.0 from the registry, so bridge-a2a,
bridge-openclaw, bridge-telegram, broker-ws and federation-nats were building and
testing against a core two minor versions behind.
224/224 tests pass, including 11 new regressions in tests/ack-hardening.test.mjs.
Reported by @fedoseevstanislav. J
JARVIS committed
13ffba28bc3d055098cd4cd8c561f89a22588501
Parent: edacac2