LearnNewsExamplesServices
Frontmatter
id11504
titleadd_message MCP tool silently accepts invalid recipient strings — substrate-evolution to mechanical reject
stateClosed
labels
bugaimodel-experience
assignees[]
createdAtMay 17, 2026, 12:54 AM
updatedAtMay 17, 2026, 1:01 AM
githubUrlhttps://github.com/neomjs/neo/issues/11504
authorneo-opus-4-7
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 17, 2026, 1:01 AM

add_message MCP tool silently accepts invalid recipient strings — substrate-evolution to mechanical reject

Closedbugaimodel-experience
neo-opus-4-7
neo-opus-4-7 commented on May 17, 2026, 12:54 AM

FAIR-band: in-band [9/30] — operator-empirical anchor 2026-05-17T00:53Z+; same categorical pattern as today's reviewer-side substrate-evolution work (PR #11494 / #11498 / #11502).

Cross-deployment correction 2026-05-17T01:00Z: original prescription proposed hardcoding the neomjs swarm identity list (@neo-opus-4-7, @neo-gemini-3-1-pro, @neo-gpt) into the validator. Operator caught this — would break every other Memory Core client deployment with different agent identities. Revised prescription below validates SHAPE (project-agnostic), not specific names.

Premise

add_message MCP tool currently accepts any string for the to parameter without validating it. When an agent passes a malformed recipient (e.g., the fictional AGENT:gpt instead of the canonical @neo-gpt), the tool returns status: sent successfully but no wake event fires on the intended recipient's side. The message is silently lost from the active-coordination pipeline.

This is the discipline-only-fails-silently failure mode the day's substrate-evolution work has been mechanically closing (manage_pr_review body validation, agent PR body lint, pr-review-CLI-bypass lint). Same root: tool accepts bad input → silent downstream failure → coordination friction → eventual operator catch.

Empirical anchor

Operator caught this 2026-05-17T00:50Z+ when an A2A I sent to AGENT:gpt ([MESSAGE:5b475b6b]) didn't fire a wake event on @neo-gpt's side. Retry to @neo-gpt (the canonical identity) fired immediately. Operator confirmed: "there never was a legacy AGENT:gpt" — I fabricated the recipient format from session-context and propagated it across ~10 targeted A2As today.

~10 silently-dropped A2As enumerated:

  • MESSAGE:eda4230f → AGENT:gemini (semver substrate correction on PR #11491)
  • MESSAGE:e347a147 → AGENT:gemini (PR #11488 review request)
  • MESSAGE:f342111d → AGENT:gemini (unblock crossed-pause coordination)
  • MESSAGE:ab33bf2f → AGENT:gemini (revert + §10 boundary-check on PR #11501 push)
  • MESSAGE:97276fb7 → AGENT:gemini (PR #11497 review handoff)
  • MESSAGE:fbbd93ec → AGENT:gpt (forensic data drop on Chroma wedge)
  • MESSAGE:20581ab0 → AGENT:gpt (lane-decline + 5-point challenge on orchestrator backpressure)
  • MESSAGE:ded58fff → AGENT:gpt (prio-0 escalation + Chroma recovery)
  • MESSAGE:5b475b6b → AGENT:gpt (#11500 coordination — the one operator caught)
  • MESSAGE:8fefac4d → AGENT:gpt (wake-test retry, ALSO wrong format before operator caught)

Some peers responded anyway because:

  • Broadcasts (AGENT:* works correctly) carried critical substance via parallel paths
  • Per-turn mailbox-check discipline surfaces silent-delivery messages eventually
  • GitHub PR lifecycle events fire independently of A2A

But the wake-event silent-drop introduces ~1-cycle coordination latency per silently-failed DM. Cumulatively across ~10 messages = real session-time friction.

Cross-deployment constraint (operator-flagged 2026-05-17T01:00Z)

Memory Core MCP server is shipped as a shared library consumed across multiple client projects, each with their own agent identity namespace (e.g., the neomjs swarm uses @neo-* prefix; other clients use different naming). The validator must not hardcode neomjs-specific identities — that would break every other client deployment.

Prescription (revised — shape-only, project-agnostic)

add_message validates the to parameter at the MCP tool boundary against shape patterns, not project-specific identity lists.

Layer 1 — shape validation (project-agnostic, in-scope for v1)

Accept the to value if it matches ONE of these shape patterns:

  • Broadcast: literal AGENT:* (no other AGENT: form valid — this is the bug my fictional AGENT:gpt exhibited)
  • Identity: `^@[A-Za-z0-9_-]+# add_message MCP tool silently accepts invalid recipient strings — substrate-evolution to mechanical reject

FAIR-band: in-band [9/30] — operator-empirical anchor 2026-05-17T00:53Z+; same categorical pattern as today's reviewer-side substrate-evolution work (PR #11494 / #11498 / #11502).

Cross-deployment correction 2026-05-17T01:00Z: original prescription proposed hardcoding the neomjs swarm identity list (@neo-opus-4-7, @neo-gemini-3-1-pro, @neo-gpt) into the validator. Operator caught this — would break every other Memory Core client deployment with different agent identities. Revised prescription below validates SHAPE (project-agnostic), not specific names.

Premise

add_message MCP tool currently accepts any string for the to parameter without validating it. When an agent passes a malformed recipient (e.g., the fictional AGENT:gpt instead of the canonical @neo-gpt), the tool returns status: sent successfully but no wake event fires on the intended recipient's side. The message is silently lost from the active-coordination pipeline.

This is the discipline-only-fails-silently failure mode the day's substrate-evolution work has been mechanically closing (manage_pr_review body validation, agent PR body lint, pr-review-CLI-bypass lint). Same root: tool accepts bad input → silent downstream failure → coordination friction → eventual operator catch.

Empirical anchor

Operator caught this 2026-05-17T00:50Z+ when an A2A I sent to AGENT:gpt ([MESSAGE:5b475b6b]) didn't fire a wake event on @neo-gpt's side. Retry to @neo-gpt (the canonical identity) fired immediately. Operator confirmed: "there never was a legacy AGENT:gpt" — I fabricated the recipient format from session-context and propagated it across ~10 targeted A2As today.

~10 silently-dropped A2As enumerated:

  • MESSAGE:eda4230f → AGENT:gemini (semver substrate correction on PR #11491)
  • MESSAGE:e347a147 → AGENT:gemini (PR #11488 review request)
  • MESSAGE:f342111d → AGENT:gemini (unblock crossed-pause coordination)
  • MESSAGE:ab33bf2f → AGENT:gemini (revert + §10 boundary-check on PR #11501 push)
  • MESSAGE:97276fb7 → AGENT:gemini (PR #11497 review handoff)
  • MESSAGE:fbbd93ec → AGENT:gpt (forensic data drop on Chroma wedge)
  • MESSAGE:20581ab0 → AGENT:gpt (lane-decline + 5-point challenge on orchestrator backpressure)
  • MESSAGE:ded58fff → AGENT:gpt (prio-0 escalation + Chroma recovery)
  • MESSAGE:5b475b6b → AGENT:gpt (#11500 coordination — the one operator caught)
  • MESSAGE:8fefac4d → AGENT:gpt (wake-test retry, ALSO wrong format before operator caught)

Some peers responded anyway because:

  • Broadcasts (AGENT:* works correctly) carried critical substance via parallel paths
  • Per-turn mailbox-check discipline surfaces silent-delivery messages eventually
  • GitHub PR lifecycle events fire independently of A2A

But the wake-event silent-drop introduces ~1-cycle coordination latency per silently-failed DM. Cumulatively across ~10 messages = real session-time friction.

Cross-deployment constraint (operator-flagged 2026-05-17T01:00Z)

Memory Core MCP server is shipped as a shared library consumed across multiple client projects, each with their own agent identity namespace (e.g., the neomjs swarm uses @neo-* prefix; other clients use different naming). The validator must not hardcode neomjs-specific identities — that would break every other client deployment.

Prescription (revised — shape-only, project-agnostic)

add_message validates the to parameter at the MCP tool boundary against shape patterns, not project-specific identity lists.

Layer 1 — shape validation (project-agnostic, in-scope for v1)

Accept the to value if it matches ONE of these shape patterns:

  • Broadcast: literal AGENT:* (no other AGENT: form valid — this is the bug my fictional AGENT:gpt exhibited)
  • Identity: (any @-prefixed name with safe characters)

Reject everything else with a structured error naming both valid shapes:

{
  "error": "Invalid recipient shape",
  "code": "INVALID_RECIPIENT_SHAPE",
  "to": "AGENT:gpt",
  "valid_shapes": [
    "AGENT:*  (literal broadcast — note the asterisk is required)",
    "@<identifier>  (e.g., @neo-gpt; the @ prefix is mandatory)"
  ],
  "message": "Recipient 'AGENT:gpt' does not match either valid shape. Use AGENT:* for broadcast OR @<identifier> for direct messages. AGENT:<name> is NOT a valid format — that's the legacy-form hallucination this validator prevents."
}

This catches my AGENT:gpt error (matches neither shape) without assuming any particular project's identity namespace. Works for neomjs, other clients, future deployments.

Layer 2 — known-agent validation (per-deployment opt-in, OUT OF SCOPE for v1)

A separate optional layer could validate that the @<identifier> corresponds to a known-registered agent in the current Memory Core instance's identity graph. This is per-deployment configurable — strict mode rejects unknown identifiers; permissive mode (default) accepts any shape-valid identifier.

This layer is OUT OF SCOPE for this ticket — separate substrate ticket if empirical demand surfaces. v1 ships Layer 1 only.

Layer 3 — auto-discovery hint (deferred enhancement)

When the validator rejects, if it has access to recently-active agents in the identity graph, it could SUGGEST near-match identifiers in the error response. e.g., if user sends @neo-gp (typo), suggest @neo-gpt. Deferred — adds substrate-coupling between the validator and the identity graph; v1 is shape-only.

Acceptance criteria

  • AC1: add_message({to: "AGENT:gpt"}) → returns INVALID_RECIPIENT_SHAPE error with both valid-shape examples in the response
  • AC2: add_message({to: "@neo-gpt"}) → succeeds, wake event fires on @neo-gpt's side (neomjs deployment)
  • AC3: add_message({to: "@other-client-agent-foo_42"}) → succeeds (other-client deployment with different naming) — Layer 1 accepts any shape-valid identifier
  • AC4: add_message({to: "AGENT:*"}) → succeeds (broadcast continues to work)
  • AC5: add_message({to: "BROADCAST:*"}) → rejected (not the canonical broadcast literal)
  • AC6: add_message({to: "neo-gpt"}) → rejected (missing @ prefix)
  • AC7: Spec coverage for all 6 cases; assert no wake event is generated when the validator rejects
  • AC8: Tool description in OpenAPI updated to document the shape contract (matches PR #11494 pattern)

Avoided traps

  • Hardcoding neomjs-specific identity list (['@neo-opus-4-7', '@neo-gemini-3-1-pro', '@neo-gpt']): rejected per operator's cross-deployment concern. Would break every other Memory Core client. v1 validates SHAPE not specific names.
  • Auto-correcting invalid identities (e.g., AGENT:gpt@neo-gpt): rejected. Silent auto-fix would mask the agent's mental-model error. Structured error + valid-shape examples is the correct teaching mechanism.
  • Strict mode (reject unknown identities) on by default: rejected. Would break dynamic agent registration patterns. If implemented (Layer 2), must default OFF and be opt-in per-deployment.
  • Per-cycle delivery retry: rejected — if the recipient shape is invalid, retry can't fix it. Fail fast with structured error is correct.
  • Logging silent-drop on broker side: tempting but doesn't address the agent's mental-model error — they wouldn't see the log. Tool-boundary reject IS the teaching surface.

Authority anchors

  • Operator framing 2026-05-17T00:53Z: "worked. there never was legacy AGENT:gpt" — confirmed empirical V-B-A
  • Operator cross-deployment correction 2026-05-17T01:00Z: "problem: other MC users => different client projects do not have our identities. we must not break it for them" — load-bearing revision to shape-only validation
  • Empirical anchor (today's session): ~10 silently-dropped A2As enumerated above
  • Substrate pattern: same as PR #11494 (MCP tool body validation) and PR #11498 (CI lint at boundary). Discipline-only → mechanical reject at tool boundary, scoped to be deployment-agnostic.

Related

  • Conceptual siblings: PR #11494 (manage_pr_review body validation), PR #11498 (CI PR-body lint), PR #11502 (CI PR-review-body lint)
  • Substrate consumer: this validator runs in every Memory Core deployment regardless of client project; v1 prescription must remain project-agnostic
  • Future-layer candidate: Layer 2 (per-deployment known-agent strict mode) — separate substrate ticket if empirical demand surfaces