Skip to content

v2.6.0 — Signed-ACK Hardening

Choose a tag to compare

@alexfrmn alexfrmn released this 20 Aug 15:50
· 16 commits to main since this release

Closes the four gaps that v2.5.0's compatible signed-ACK path left open. They were found by diffing #100 against @fedoseevstanislav's strict variant in #104 — the compatible PR looked complete on its own, and only the comparison exposed what it did not cover.

Landed compatibly in #110: no wire break, mixed-version meshes keep working, and the two-stage ackSecurity.requireSigned rollout still applies.

Security

Replay protection survives a restart. ACK nonces used to live in a bounded in-memory Set. A restart forgot them, so a signed NACK could be replayed against a fresh process: the retry returned the row to sent and the replayed NACK failed it again. AckReceiptStore plus an ack_receipts table give claim-once semantics on (sender_agent_id, nonce) that survive a restart. The in-memory store remains an explicit fallback, and the daemon now logs a warning when that fallback is what is running instead of implying protection it does not have.

The fast-ACK race no longer causes a spurious retry. An ACK arriving between publish() and markSent() was rejected as message-not-in-flight, leaving the row to time out. applyAckTransition now accepts pending alongside sent, and markSent() refuses to downgrade a terminal status so the late call cannot resurrect a settled row.

The A2A bridge no longer honours an unsigned NACK. It resolved a pending task from a bare {msgId, status: "nack"} object — anyone able to publish to the ACK subject could settle someone else's in-flight task with an arbitrary failure string. A verified SignedAckV1 is now required; signingPublicKeys was added to BridgeA2AConfig.

The WebSocket ACK path is verified like the NATS one. processAckFrame verified nothing and called markAcked/markFailed straight from the frame. It now checks record lookup, digest, conversation, recipient, known peer, ack-subject binding, signature and nonce claim.

Fixed

Five packages were built and tested against a stale core. bridge-a2a, bridge-openclaw, bridge-telegram, broker-ws and federation-nats declared @murmurv2/core: ^0.2.0. Once core reached 0.4.0 npm could no longer satisfy that from the workspace and silently installed 0.2.0 from the registry — their passing tests were passing against code two minor versions behind.

Still a rollout step, not a code step

ackSecurity.requireSigned / MURMUR_REQUIRE_SIGNED_ACKS=1 must be turned on across every peer before unsigned ACKs stop being accepted. Until then this release hardens the signed path but does not close the door on the unsigned one.

npm

@murmurv2/core 0.5.0, @murmurv2/broker-ws 0.2.0, @murmurv2/bridge-a2a 0.2.0, @murmurv2/broker-nats 0.3.1.


224/224 tests pass, 11 of them new regressions in tests/ack-hardening.test.mjs. CI green on 0c0d86b.