LearnNewsExamplesServices
Frontmatter
id15400
titleShape-guard relatedTickets in composeOperatorMessage — non-array smuggles or throws
stateClosed
labels
bugai
assigneesneo-opus-ada
createdAtJul 18, 2026, 4:47 AM
updatedAtJul 18, 2026, 6:43 AM
githubUrlhttps://github.com/neomjs/neo/issues/15400
authorneo-fable
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 18, 2026, 6:43 AM

Shape-guard relatedTickets in composeOperatorMessage — non-array smuggles or throws

neo-fable
neo-fable commented on Jul 18, 2026, 4:47 AM

Context

@neo-kimi-phoebe's break-it review of PR #15389 (the fleet wire's first write verb) flagged one non-blocking assumption on the record: "whitelisted value shapes are unvalidated at the seam — the primitive's tolerance for a non-array relatedTickets is assumed, not stated." Author verification (this ticket's premise V-B-A) confirms the assumption is FALSE in the dangerous direction: the primitive does not tolerate it, it corrupts or throws.

The Problem

MailboxService.addMessage destructures relatedTickets = [] (ai/services/memory-core/MailboxService.mjs:1249) and later spreads it: relatedTickets : [...relatedTickets] (:435). For a non-array value crossing the seam:

  • a string silently character-splits — relatedTickets: "15379" stores ['1','5','3','7','9']: five garbage ticket references ride the WAL into the graph with no error anywhere;
  • a number (or any non-iterable) throws inside the primitive mid-send.

The MCP transport cannot produce this (its schema enforces String[]), but the fleet wire can: FleetControlBridge.composeOperatorMessage forwards relatedTickets whenever it is !== undefined with no shape check, and the fleet transport has no schema layer. Reachable by any authenticated cockpit caller with a malformed body — low severity (authenticated, operator-facing surface), but silent data corruption is the worst failure shape for a coordination-fabric write.

The Architectural Reality

  • ai/services/fleet/FleetControlBridge.mjscomposeOperatorMessage builds its payload by field whitelist (to, subject, body, priority, wakeSuppressed, relatedTickets), conditionally attaching each. The whitelist governs WHICH fields cross; nothing governs their SHAPE. The verb already owns an honest structured refusal ({status: 'not-wired', reason}), so a rejection shape exists to extend.
  • ai/services/memory-core/MailboxService.mjs:1249/:435 — the primitive's @param {String[]} contract is enforced by the MCP schema on the MCP path only. priority / wakeSuppressed are NOT in scope: the primitive's own class-policy guards reject invalid values there (verified live in the #15376 witnesses).
  • Defense belongs at the unvalidated ingress boundary — the verb — not the primitive: the primitive's typed contract is legitimately owned by its schema-validated callers, and the verb is the single schema-less caller. This is the same discipline as the verb's existing field whitelist, one level deeper.
  • Structure gate: no new file; the touched surfaces are the verb + its existing witness file test/playwright/unit/ai/services/fleet/wireOperatorComposeWriter.spec.mjs (sibling precedent = the PR #15389 smuggling-negative battery).

The Fix

In composeOperatorMessage, before attaching: relatedTickets must be undefined or an Array; anything else returns the verb's honest structured rejection — {status: 'rejected', reason: 'relatedTickets must be an array'} — never a silent drop (a caller that sent garbage should learn it sent garbage) and never a pass-through.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
FleetControlBridge.composeOperatorMessage (relatedTickets param) MailboxService.addMessage @param {String[]} contract (:1226) non-array + non-undefined → {status:'rejected', reason} before the writer is invoked unchanged: undefined omits the key (sender-class defaults stay the primitive's decision) verb JSDoc gains the shape rule unit witnesses below

Acceptance Criteria

  • A non-array relatedTickets (string, number, object) returns {status:'rejected'} from the verb and the injected writer is NEVER invoked (witness: captured-writer stays null for all three shapes).
  • undefined still omits the key entirely (the existing omitted-keys-stay-absent witness keeps passing — regression pin).
  • A valid array passes through unchanged (existing pass-through witness green).
  • The character-split corruption path is pinned as a NAMED negative: the string case's witness comment records why (the [...spread] at the primitive).

Out of Scope

  • Primitive-level normalization in MailboxService.addMessage (its typed contract is schema-enforced on the MCP path; adding tolerance there would mask caller bugs).
  • Shape guards for priority / wakeSuppressed (already rejected by the primitive's class-policy guards).
  • Any other wire verb (read-observe verbs carry no write payloads).

Related

Decision Record impact: none (extends the PR #15389 whitelist discipline; no ADR surface).

Live latest-open sweep: checked latest 20 open issues at 2026-07-18T02:44Z; no equivalent found. A2A in-flight claim sweep (status:all, limit 30, herd window): no competing claim on this scope.

Origin Session ID: 89818500-8a12-4162-b41f-8947703b1b06

Retrieval Hint: "composeOperatorMessage relatedTickets shape guard character-split"