v2.5.0 — External Security Audit
First Murmur release built substantially from external contributions. A security audit by @fedoseevstanislav and wake/delivery analysis by @alexanderyswork account for most of what changed here.
Security
Signed and bound delivery acknowledgements (#100)
ACK correlation previously trusted attacker-controlled JSON carrying only {msgId, status}. Anyone able to publish to an ACK subject could mark an arbitrary pending outbox row acked or failed — suppressing delivery or forcing retries — without authenticating as the purported consumer.
ACKs are now a versioned SignedAckV1: an Ed25519 signature over the message digest, conversation, ACK sender, intended recipient, status, timestamp and nonce. Wrong-message, wrong-conversation, wrong-recipient, wrong-peer, stale, future-dated, invalid-signature and replayed ACKs are rejected, and ACK/NACK state changes apply atomically only from the sent state. Invalid attempts are metered by bounded reason as metadata-only security events — raw ACK and message bodies are never logged.
Migration is deliberately two-stage: upgraded daemons emit signed ACKs that legacy peers still parse, and strict rejection stays opt-in behind ackSecurity.requireSigned / MURMUR_REQUIRE_SIGNED_ACKS=1 until every peer is upgraded.
⚠️ Read this before assuming you are protected. A configuration with noackSecurityblock runsemitSigned=truewithrequireSigned=false— unsigned ACKs are still accepted. Upgrade every peer, then setackSecurity.requireSigned: true(orMURMUR_REQUIRE_SIGNED_ACKS=1). Until that flip the issue is mitigated, not closed. Four further gaps in the compatible path — non-durable replay nonces, a fast-ACK race, an unsigned raw-NACK sink in the A2A bridge, and the unguarded WebSocket ACK path — are tracked in #109.
Hardened local state handling (#101)
Agent configs hold long-term signing and encryption private keys plus broker credentials, and rewrites could return them to 0664; SQLite files holding decrypted history were commonly 0644. The daemon now sets umask 0077, creates state directories 0700, atomically writes secret JSON as 0600, rejects symlinked, non-regular and wrong-owner config paths, reads configs with O_NOFOLLOW and re-checks the opened descriptor, and forces database/WAL/shm files to 0600.
Dashboard rendering and ingress (#102)
Untrusted fields render through DOM textContent only — no innerHTML, inline scripts or inline handlers — behind a strict CSP and the full protective header set. HTTP and WebSocket access require Basic authentication from a private server-local token file, and live messages are verified for envelope schema, signature, subject/recipient binding, traffic direction and known-peer identity before reaching the UI. Fails closed without a valid token file.
Fixed
- Codex wake seeded threads are usable (#97) —
thread/startno longer discardsthread.path, and new threads carrypeer.cwdinstead ofcwd: null, which produced wrong workspace roots, missing project instructions and wrong permissions. - Per-peer
baseInstructionsno longer dropped (#98) — reaching the injector'speer.resume === falseopt-out from real configuration for the first time. - Broken quickstart —
git clone .../murmur.git && cd mur-mur-v2failed on the very first command every new user ran. The repository was renamed on 2026-08-03; README, publish tooling and the deploy script kept the old slug.
Documentation
CHANGELOG.mdhas a [2.4.0] section for the first time — that release shipped on 2026-06-23 pointing at "See CHANGELOG.md for details" while the section never existed.SECURITY.mdnow matches how the project actually behaves: private reporting is still preferred, but a missed acknowledgement target explicitly frees the reporter to disclose publicly. Written after we left a real audit unanswered for nine days.
Known gaps, tracked
The delivery-semantics findings from #96 are filed rather than silently carried: #105 (failed wakes advance the cursor; relay not idempotent — one package, not two), #106 (empty finalText logs as relayed), #107 (sequential drain causes head-of-line blocking), #108 (threadId is process-memory only and scoped per peer).
Held deliberately: #103 (TLS + per-peer NATS auth) needs a coordinated broker and peer credential cutover, and #104 is the wire-breaking ACK variant under evaluation against #100.
npm
@murmurv2/core 0.4.0, @murmurv2/broker-nats 0.3.0, @murmurv2/mcp-server 0.2.0.
213/213 tests pass; CI green on 4a069d8.