Skip to content

feat: add inbound P2P event bridge - #2

Open
raysvitla wants to merge 2 commits into
fix/phase1-agent-network-hardeningfrom
feat/inbound-agent-event-handler
Open

feat: add inbound P2P event bridge#2
raysvitla wants to merge 2 commits into
fix/phase1-agent-network-hardeningfrom
feat/inbound-agent-event-handler

Conversation

@raysvitla

Copy link
Copy Markdown
Member

Depends on #1.

Turns authenticated/decrypted/persisted P2P group messages into typed local events for an agent runtime to decide act | ask | ignore. This is the thinnest bridge from "secure transport" to "agent network".

Summary

  • New typed event model in @networkselfmd/core:
    • PrivateInboundMessageEvent — local-only; carries decrypted plaintext + full context (messageId, groupId, senderPublicKey, senderFingerprint, timestamp, receivedAt).
    • PublicActivityEvent — metadata-only (groupIdHex, senderFingerprint, timestamp, byteLength). Safe for future census/heartbeat/dashboard.
  • Additive events on GroupManager / Agent: inbound:message, activity:message. Legacy group:message payload preserved unchanged — no listener regressions.
  • InboundEventQueue on Agent — bounded FIFO (default 1000) with push/drain/peek/on(unsubscribe). Buffers only private events; public activity is live-only.
  • New MCP tool get_pending_inbound_events plus toInboundEventDTO helper — hex for keys/groupId, base64 for plaintext, strict UTF-8 optional. No Uint8Array ever reaches JSON.stringify.
  • DM path: unchanged, still fail-closed per fix: harden group protocol and key storage #1. kind: 'dm' reserved in the type; explicit TODO marker for the DM-signing / Double Ratchet PR.

Guardrails

All emissions happen after signature verification + sender-key scoping + decryption + messageRepo.insert() succeed. Never on failed paths. Rejection branches (invalid sig, unknown member, failed decrypt, unknown group) continue to emit('error', ...) and return before any new emission.

Public-log canary test ensures plaintext does not reach console.log/info/warn.

Tests

22 new tests (total 130):

  • Emission path: inbound:message with expected fields + DB messageId round-trip; activity:message metadata-only (asserted absence of plaintext/ciphertext/senderPublicKey/base64 fields).
  • Legacy: group:message payload preserved.
  • Negatives (each emits nothing): tampered ciphertext (invalid signature), unknown/non-member sender, valid-sig/wrong-chain-state (failed decryption, distinct from tamper).
  • Public-log canary.
  • Agent-handler shape assertion for act/ask/ignore decision surface.
  • InboundEventQueue: FIFO order, non-destructive peek, overflow drops oldest, on() + unsubscribe.
  • MCP DTO: hex/base64 encoding, UTF-8 only when valid, no numeric-keyed byte-object leak via JSON.stringify.

Shared test harness extracted to packages/node/src/__tests__/test-utils/group-harness.ts; group-signatures.test.ts migrated to it.

Out of scope (follow-up PRs)

  • DM signing / Double Ratchet → unblocks DM inbound events.
  • Agent policy runner (the act | ask | ignore decision engine).
  • Public self.md schema / MCP.
  • Heartbeat / census dashboard (will consume activity:message).
  • MCP streaming push subscriptions; CLI watch command.

Verification

pnpm install
pnpm build      # clean
pnpm test       # 130 passing (was 108)
pnpm lint       # no-op (no package defines a lint script)

Test plan

  • pnpm test green
  • pnpm build green
  • Manual: two-agent harness — Alice sends group message, assert Bob.inboundQueue.drain() returns one PrivateInboundMessageEvent with expected plaintext; tamper ciphertext → empty drain, no activity:message.
  • MCP: get_pending_inbound_events returns DTO with hex keys + base64 plaintext; JSON.stringify(result) contains no numeric-keyed byte objects.
  • Existing listeners of group:message still receive the original payload shape.

🤖 Generated with Claude Code

raysvitla and others added 2 commits April 24, 2026 19:26
Turns authenticated/decrypted/persisted P2P group messages into typed
local events for agent runtimes to decide act | ask | ignore.

- PrivateInboundMessageEvent (local, decrypted plaintext) +
  PublicActivityEvent (metadata-only); legacy group:message preserved.
- InboundEventQueue on Agent buffers private events for MCP/CLI pollers.
- New MCP tool get_pending_inbound_events with DTO that hex-encodes keys
  and base64-encodes plaintext so no Uint8Array leaks via JSON.stringify.
- DM path remains fail-closed per PR #1; typed with kind: 'dm' for
  forward compatibility, no emission until DM signing lands.
- 22 new tests: emission path, legacy compatibility, invalid-sig /
  unknown-member / failed-decrypt negatives, public-log canary, queue
  semantics, MCP DTO.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- canary covers stderr/debug/stdout.write/stderr.write and asserts
  activity-event serialization is canary-free
- MCP get_pending_inbound_events description explicitly owner-private,
  warns against public/dashboard/census forwarding
- InboundEventQueue.drain returns a copy, not the internal buffer
- explicit isMember gate in handleGroupMessage (defence-in-depth
  alongside the implicit sender-key gate) + negative test
- queue handler errors surface via queueMicrotask instead of being
  swallowed, without blocking delivery to other handlers

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant