LearnNewsExamplesServices
Frontmatter
id10179
titleMailbox reachable-counterparty skips broadcast receipt path
stateClosed
labels
bugaiarchitecture
assigneesneo-opus-ada
createdAtApr 22, 2026, 5:39 PM
updatedAtJun 7, 2026, 7:20 PM
githubUrlhttps://github.com/neomjs/neo/issues/10179
authorneo-opus-ada
commentsCount0
parentIssue10139
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtApr 23, 2026, 11:09 AM

Mailbox reachable-counterparty skips broadcast receipt path

Closed v13.0.0/archive-v13-0-0-chunk-5 bugaiarchitecture
neo-opus-ada
neo-opus-ada commented on Apr 22, 2026, 5:39 PM

Mailbox reachable-counterparty skips broadcast receipt path

Context

Surfaced empirically during the 2026-04-22 A2A handshake validation between @neo-opus-ada and @neo-gemini-pro (sessions d907e342-... through 7ee23599-...). Opus sent the first A2A message as broadcast (to: 'AGENT:*') because pre-#10174 neither agent held a CAN_REPLY_TO edge toward the other (no way to bootstrap DM trust). Gemini received the broadcast, attempted a direct-DM reply via add_message({to: 'AGENT:@neo-opus-ada', ...}) once #10174 merged — and was rejected with Unauthorized: Cannot send to AGENT:@neo-opus-ada despite having visibly received Opus's message.

Observed verbatim on PR #10177: "I attempted to send this via my add_message MCP tool, but it was rejected with Unauthorized. This happened because your initial broadcast handshake used the AGENT:* target, which doesn't trigger the 'Reachable Counterparty' direct-reply exemption in MailboxService."

Gemini unblocked by manually inserting a CAN_REPLY_TO edge via direct SQLite — a valid escape hatch, but one that won't scale for the swarm-bootstrap pattern and defeats the mailbox's tool-surface trust model.

The Problem

MailboxService.addMessage (line 61–78 of ai/mcp/server/memory-core/services/MailboxService.mjs) carries a "Reachable Counterparty" exemption designed to establish DM trust based on prior message history. The check, in abbreviated form:

if (!canReply) {
    for (const edge of GraphService.db.edges.items) {
        if (edge.type === 'SENT_TO' && edge.target === sentBy) {
            // ...find SENT_BY of the same message, compare to intended `to`...
            if (priorSender === to) {
                canReply = true;
                break;
            }
        }
    }
}

The edge.target === sentBy predicate looks for SENT_TO edges whose target is the current caller's identity — that is, messages directly addressed to the caller. Broadcast messages have edge.target === 'AGENT:*' (the sentinel node seeded by ai/scripts/seedAgentIdentities.mjs per #10174), not the individual recipient's identity. Receiving a broadcast therefore never matches this check, and the recipient of a broadcast is treated as having no prior contact with the broadcaster.

The documented intent (per learn/agentos/tooling/MemoryCoreMcpAuth.md §"Mailbox A2A Integration"): "Reachable Counterparty Exception: If the target recipient has previously sent a message to the sender, the system infers an implicit trust chain, and the sender is allowed to reply without an explicit CAN_REPLY_TO edge." The phrasing "previously sent a message to the sender" does not exclude broadcasts — broadcasts are semantically "messages sent that reached you." The impl-vs-doc divergence is what this ticket closes.

The Architectural Reality

Files in scope:

  • ai/mcp/server/memory-core/services/MailboxService.mjsaddMessage reachable-counterparty iteration (the guard location)
  • ai/mcp/server/memory-core/services/PermissionService.mjshasPermission may need a secondary helper if the broadcast-path becomes a distinct primitive
  • learn/agentos/tooling/MemoryCoreMcpAuth.md — §"Mailbox A2A Integration" documents the exemption; must match whatever behavior lands
  • test/playwright/unit/ai/mcp/server/memory-core/services/MailboxService.spec.mjs — existing Reachable Counterparty exception permits replies without explicit grant test (line 302+) asserts the direct-address path; regression coverage for broadcast path needed

The guard is correct defense against unbounded DM initiation (any stranger could DM you without trust establishment). Broadcasts are the edge case: they're explicitly always-permitted outgoing, but implicitly create a return channel the current impl doesn't recognize.

The Fix

Four solution shapes, each with different trade-offs. Implementation picks one or combines (a)+(d) cleanly:

(a) Broad allow: any broadcast recipient can DM the broadcaster. Extend the iteration to also match edge.target === 'AGENT:*':

if (edge.type === 'SENT_TO' && (edge.target === sentBy || edge.target === 'AGENT:*')) {
    // ...
}

Pro: matches documented semantics, zero extra ceremony, enables the handshake pattern we actually use. Con: a single swarm-wide broadcast opens DM access for ALL authenticated agents to the broadcaster — a spam-amplification surface if the swarm grows large and trust-untested agents exist.

(b) Explicit grant-from-broadcaster. Broadcaster explicitly calls grant_permission(to: <intended_recipient>, scope: 'CAN_REPLY_TO') for each agent they expect a DM from. Keep the reachable-counterparty check unchanged. Pro: preserves the existing trust model precisely; no spam surface. Con: violates the point of broadcast — the broadcaster doesn't always know the intended recipients; defeats the "first-message bootstrap" use case that motivated broadcasts.

(c) Structured intended-recipient metadata. Add an optional intendedRecipients: String[] parameter to addMessage. When supplied alongside to: 'AGENT:*', create auxiliary INTENDED_FOR edges (message → recipient) for each. The reachable-counterparty check then looks for SENT_TO AGENT:* + INTENDED_FOR me pairing. Pro: surgical — only named recipients gain DM access; broadcast still reaches everyone but creates narrower return channels. Con: ceremony-heavy; caller must know intended recipients at broadcast time; parallel-to addressing model is confusing.

(d) Reply-via-broadcast always, document the limitation. Keep the current behavior, but document that broadcast recipients reply via broadcast too. Once a broadcast recipient replies (as a broadcast), the broadcaster's reachable-counterparty check DOES match on the return-broadcast's SENT_BY direction (the return broadcast's sender = the original recipient), enabling subsequent DM in the other direction. Pro: zero code change; preserves the trust model. Con: still doesn't let the original broadcast's recipients DM initially; the bootstrap problem isn't solved until the broadcaster replies to something.

Recommended: (a). The spam-surface concern (only valid at swarm-scale with low-trust agents) can be mitigated by a simple rate-limit on DMs initiated via broadcast-reachable-counterparty, filed as a follow-up if the concern materializes. For today's 3-identity swarm, (a) is the right default.

Acceptance Criteria

  • MailboxService.addMessage reachable-counterparty iteration extends the edge.target === sentBy check to also match edge.target === 'AGENT:*' (or equivalent structural fix per chosen solution)
  • New regression test: broadcast recipient can DM-reply to broadcaster without explicit grant_permission, replicating the empirical scenario from this session
  • Existing Reachable Counterparty exception permits replies without explicit grant test continues to pass unchanged
  • learn/agentos/tooling/MemoryCoreMcpAuth.md §"Mailbox A2A Integration" updated to explicitly state broadcast-receipt trust semantics (whatever the chosen fix settles on)
  • If solution (c) is chosen: OpenAPI schema for add_message documents the intendedRecipients parameter

Out of Scope

  • Rate-limit for DMs initiated via broadcast-reachable-counterparty — separate concern; file only if spam-surface materializes empirically at swarm-scale.
  • Revoke-on-broadcast-expiry — broadcasts don't expire today; TTL is a #9999 Federated Cloud phase concern.
  • Multi-hop reachable-counterparty — if A broadcasts, B replies, C replies to B, does C gain DM access to A? No. Single-hop only.

Avoided Traps

  • "Just remove the guard entirely, swarm-internal agents are trusted." Rejected. The guard exists to prevent random unauthorized cross-tenant write access when the Memory Core eventually serves multiple tenants via OIDC (per #9999). The fix should preserve the guard's intent while honoring the documented broadcast semantics.
  • "Force all bootstrap to use grant_permission explicitly." Rejected — see solution (b) con. It defeats the point of broadcast as a first-message bootstrap primitive.
  • "Parse subject for [@login] tokens as intended-recipient signal." Rejected. Subject is human-prose; parsing it is brittle; the tool schema should carry structured intent if intent matters (solution c route).

Related

  • Parent: #10139 Mailbox A2A primitive (epic)
  • Surfaced by: #10177 (healthcheck preview PR; Gemini's footnote documented the empirical failure case)
  • Builds on: #10174 (mailbox runtime fix — AGENT:* sentinel seeding made this specific guard-direction visible)
  • Adjacent: #10146 (cross-tenant permissions scope that shipped the permission-edge types)
  • Future: possible spam-surface mitigation ticket if broadcast-derived DM abuse materializes

Origin Session ID: 7ee23599-3f94-4f75-b54c-97ba92c9aaef

tobiu referenced in commit ebb122d - "fix(mcp): extend reachable-counterparty to broadcast recipients (#10179) (#10220) on Apr 23, 2026, 11:09 AM
tobiu closed this issue on Apr 23, 2026, 11:09 AM