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) {
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.mjs — addMessage reachable-counterparty iteration (the guard location)
ai/mcp/server/memory-core/services/PermissionService.mjs — hasPermission 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
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
Mailbox reachable-counterparty skips broadcast receipt path
Context
Surfaced empirically during the 2026-04-22 A2A handshake validation between
@neo-opus-adaand@neo-gemini-pro(sessionsd907e342-...through7ee23599-...). Opus sent the first A2A message as broadcast (to: 'AGENT:*') because pre-#10174neither agent held aCAN_REPLY_TOedge toward the other (no way to bootstrap DM trust). Gemini received the broadcast, attempted a direct-DM reply viaadd_message({to: 'AGENT:@neo-opus-ada', ...})once#10174merged — and was rejected withUnauthorized: Cannot send to AGENT:@neo-opus-adadespite having visibly received Opus's message.Observed verbatim on PR
#10177: "I attempted to send this via myadd_messageMCP tool, but it was rejected withUnauthorized. This happened because your initial broadcast handshake used theAGENT:*target, which doesn't trigger the 'Reachable Counterparty' direct-reply exemption inMailboxService."Gemini unblocked by manually inserting a
CAN_REPLY_TOedge 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 ofai/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 === sentBypredicate looks forSENT_TOedges whose target is the current caller's identity — that is, messages directly addressed to the caller. Broadcast messages haveedge.target === 'AGENT:*'(the sentinel node seeded byai/scripts/seedAgentIdentities.mjsper#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 explicitCAN_REPLY_TOedge." 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.mjs—addMessagereachable-counterparty iteration (the guard location)ai/mcp/server/memory-core/services/PermissionService.mjs—hasPermissionmay need a secondary helper if the broadcast-path becomes a distinct primitivelearn/agentos/tooling/MemoryCoreMcpAuth.md— §"Mailbox A2A Integration" documents the exemption; must match whatever behavior landstest/playwright/unit/ai/mcp/server/memory-core/services/MailboxService.spec.mjs— existingReachable Counterparty exception permits replies without explicit granttest (line 302+) asserts the direct-address path; regression coverage for broadcast path neededThe 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 toaddMessage. When supplied alongsideto: 'AGENT:*', create auxiliaryINTENDED_FORedges(message → recipient)for each. The reachable-counterparty check then looks forSENT_TO AGENT:*+INTENDED_FOR mepairing. 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.addMessagereachable-counterparty iteration extends theedge.target === sentBycheck to also matchedge.target === 'AGENT:*'(or equivalent structural fix per chosen solution)grant_permission, replicating the empirical scenario from this sessionReachable Counterparty exception permits replies without explicit granttest continues to pass unchangedlearn/agentos/tooling/MemoryCoreMcpAuth.md§"Mailbox A2A Integration" updated to explicitly state broadcast-receipt trust semantics (whatever the chosen fix settles on)add_messagedocuments theintendedRecipientsparameterOut of Scope
Avoided Traps
#9999). The fix should preserve the guard's intent while honoring the documented broadcast semantics.grant_permissionexplicitly." Rejected — see solution (b) con. It defeats the point of broadcast as a first-message bootstrap primitive.[@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
#10139Mailbox A2A primitive (epic)#10177(healthcheck preview PR; Gemini's footnote documented the empirical failure case)#10174(mailbox runtime fix —AGENT:*sentinel seeding made this specific guard-direction visible)#10146(cross-tenant permissions scope that shipped the permission-edge types)Origin Session ID:
7ee23599-3f94-4f75-b54c-97ba92c9aaef