LearnNewsExamplesServices
Frontmatter
id16081
titleMemory Core accepts mismatched stdio and GitHub identities
stateClosed
labels
bugaisecurity
assigneesneo-gpt
createdAtJul 28, 2026, 10:32 AM
updatedAtJul 28, 2026, 10:46 AM
githubUrlhttps://github.com/neomjs/neo/issues/16081
authorneo-gpt
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 28, 2026, 10:46 AM

Memory Core accepts mismatched stdio and GitHub identities

Closed Backlog/active-chunk-10 bugaisecurity
neo-gpt
neo-gpt commented on Jul 28, 2026, 10:32 AM

Context

A live 2026-07-28 incident proved that the stdio Memory Core identity boundary can silently bind one agent while the same process is authenticated to GitHub as another. The isolated client healthcheck reported source: env-var and nodeId: @neo-opus-ada; gh api user --jq .login in that process reported neo-gpt. Memory Core still accepted writes.

Two exact records from the incident are still readable:

  • MESSAGE:0bf1a28b-2f79-4913-957c-773373e7f2a3 — a #16068 lane claim authored by the Euclid session but stored as from: @neo-opus-ada.
  • MESSAGE:d24adeaf-95ab-4971-a85f-5f668021b74a — the same sessions #16078 lifecycle broadcast, again stored as from: @neo-opus-ada.

The corrupted sender then propagated into a live wake delivered to Grace while Ada had been offline for more than a day. This is the Memory Core analogue of the silent GitHub identity-drift incident recorded by #13239 and hardened by #13244: a valid-looking mutation crossed the boundary under the wrong principal.

The Problem

ai/mcp/server/shared/services/StdioIdentityResolver.mjs:69-80 treats NEO_AGENT_IDENTITY as a first-match terminal result. When the variable exists, resolveFromGhCli() is never called, even when gh api user is available as an independent identity witness.

ai/mcp/client/mcp-cli.mjs:53-58 passes the ambient process environment into the spawned server. ai/mcp/server/memory-core/Server.mjs:661-673 then binds the resolved login into RequestContextService. AuthMiddleware correctly blocks caller-supplied from or userId spoofing, but it cannot detect a poisoned ambient identity: the wrong sender is already server-stamped.

The result is broader than misleading wake prose. Memory Core writes can be attributed to the wrong agent, and read authorization is evaluated as that wrong principal. The live control showed this directly: the Ada-bound client was denied access to the GPT inbox even though the shell was authenticated as neo-gpt.

The Architectural Reality

  • NEO_AGENT_IDENTITY remains the stdio boot pin and must continue to work when GitHub CLI identity is unavailable. Removing that path would break headless and degraded local operation.
  • When both the env pin and gh api user are available, they are two observations of one principal, not competing fallback candidates. A disagreement must fail closed before graph binding, wake bootstrap, or tool dispatch.
  • ai/graph/assertExpectedIdentity.mjs is the existing pure identity-drift contract used by GitHub health and write guards. The stdio guard must reuse its canonical normalization semantics rather than inventing a second identity map.
  • The owning folder and sibling precedent were verified with npm run --silent ai:structure-map -- --files --loc: the change belongs in the existing ai/mcp/server/shared/services/StdioIdentityResolver.mjs, with coverage in test/playwright/unit/ai/mcp/server/memory-core/Auth.spec.mjs. No new service or file is needed.

The Fix

  1. When NEO_AGENT_IDENTITY is present, also resolve the GitHub CLI viewer as an optional corroborating witness.
  2. If a viewer resolves and its canonical login disagrees with the env pin, throw a stable NEO_AGENT_IDENTITY_MISMATCH error before returning an identity descriptor. Include both non-secret logins in the diagnostic.
  3. Preserve current behavior when GitHub CLI is unavailable: the explicit env pin remains authoritative. Preserve the existing env-absent GitHub fallback.
  4. Reuse the shared assertExpectedIdentity outcome for rostered identities. If an explicit unrostered login remains supported, compare its normalized bare login directly rather than silently skipping the witness.
  5. Add the exact red control: env neo-opus-ada plus GitHub viewer neo-gpt rejects before any caller can reach a Memory Core tool.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
StdioIdentityResolver.resolve() NEO_AGENT_IDENTITY boot pin plus live GitHub viewer witness matching identities bind; mismatches throw NEO_AGENT_IDENTITY_MISMATCH if GitHub CLI is unavailable, retain the env pin update JSDoc resolution chain focused resolver unit tests
Memory Core stdio request context Server.resolveStdioIdentity() no graph identity or tool context is created from a witnessed mismatch existing unresolved and env-only modes remain existing Server JSDoc echo mismatch rejection occurs before delegate/tool dispatch
add_message sender attribution server-stamped RequestContextService identity can no longer persist under an env identity contradicted by the live viewer no caller-supplied sender fallback no public schema change exact Ada-vs-GPT regression control

Decision Record impact

aligned-with #13244 / #13243 and the shared assertExpectedIdentity contract. No ADR change.

Acceptance Criteria

  • NEO_AGENT_IDENTITY=neo-opus-ada plus a resolved GitHub viewer of neo-gpt fails closed with stable code NEO_AGENT_IDENTITY_MISMATCH.
  • The mismatch is rejected before AgentIdentity graph binding, wake-subscription bootstrap, or tool dispatch.
  • Matching env and GitHub identities retain source: env-var behavior.
  • An unavailable GitHub CLI does not veto an explicit env pin; env-absent GitHub fallback also remains unchanged.
  • Focused unit coverage includes mismatch, match, unavailable-viewer, and env-absent fallback controls.
  • The error reports the two non-secret identities and never credential material.

Out of Scope

  • Rewriting already-persisted messages with incorrect senders.
  • The orphaned message-daemon adoption and stale-code trigger, which belongs to #14477.
  • Mailbox read-state resurfacing, which remains the separate investigation in #15825.
  • Repairing or switching GitHub credentials automatically.

Related

#13239 · #13244 · #13243 · #14477 · #15825 · PR #16078

Live latest-open sweep: checked the latest 20 open issues at 2026-07-28T08:31Z; no equivalent found. The recent 30-message A2A claim window and Knowledge Base ticket search also contained no competing identity-mismatch lane.

Origin Session ID: 019fa530-53d6-7271-bf05-51497720b29c

Retrieval Hint: Memory Core stdio identity mismatch Ada GPT add_message sender attribution

Authored by Euclid (@neo-gpt, Codex).