LearnNewsExamplesServices
Frontmatter
id10145
titleOAuth2 authentication layer for Memory Core MCP connections
stateClosed
labels
enhancementaiarchitecturecore
assigneestobiu
createdAtApr 21, 2026, 11:29 AM
updatedAtJun 7, 2026, 7:19 PM
githubUrlhttps://github.com/neomjs/neo/issues/10145
authortobiu
commentsCount0
parentIssue10016
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[x] 10144 AgentIdentity node type + GitHub account binding for model identities
blocking[x] 10139 Extend Memory Core with Explicit A2A Primitive, [x] 10146 Cross-tenant permission edges + multi-tenant validation test suite
closedAtApr 21, 2026, 10:16 PM

OAuth2 authentication layer for Memory Core MCP connections

Closed v13.0.0/archive-v13-0-0-chunk-5 enhancementaiarchitecturecore
tobiu
tobiu commented on Apr 21, 2026, 11:29 AM

OAuth2 authentication layer for Memory Core MCP connections

Context

Per-model GitHub accounts (#10144 ✓ complete) make each agent a first-class graph identity — @neo-opus-ada, @neo-gemini-pro, @tobiu all seeded via ai/scripts/seedAgentIdentities.mjs. MC MCP must resolve every invocation to a single authoritative identity so that (a) ChromaDB writes carry the correct tenant tag, (b) reads filter per tenant, (c) future AgentIdentity graph edges (AUTHORED_BY, OWNED_BY) terminate on the right node.

Scope-reshape note (intake 2026-04-21, session d69ac7a0-9fe8-4416-b766-cd9edb8bee71): the ticket was drafted before a full survey of the existing auth substrate. Intake revealed that #10000 already shipped the OIDC/OAuth path for SSE transport. Original body's prescription "GitHub OAuth2 flow in Memory Core connect handshake" is partially redundant for SSE and doesn't address the real stdio gap. This body is updated to reflect the architectural reality discovered during intake, per the ticket-intake "Hypothesis vs Root Cause" challenge.

The Problem

Three distinct gaps, all reachable in one cohesive change:

  1. Stdio identity is never resolved. With per-agent GitHub accounts, every stdio connection needs to flow an identity through RequestContextService so MemoryService.addMemory, mutate_frontier, and future write tools tag writes with the resolved userId. Today stdio skips the context entirely — writes fall through to single-tenant mode regardless of which agent is running.
  2. AgentIdentity graph-node ID isn't bound to the runtime identity. #10144 created the node type and seeded three identities, but RequestContext today only carries {userId, username}. Services that want to build AUTHORED_BY / OWNED_BY edges at write time need the resolved agentIdentityNodeId in context.
  3. No preemptive anti-spoof guard. Current write schemas (AddMemoryRequest) have no client-supplied identity field, so there is no spoof surface today. But Mailbox (#10139) will add from / recipient fields; without a server-side guard, a Gemini-harness session could claim from: '@neo-opus-ada'. The guard belongs at the callTool dispatch choke-point, fires before any write-tool service method.

The Architectural Reality

Layer Stdio (today) SSE (today, via #10000) #10145 delta
Identity source ❌ none ✅ OIDC Bearer token introspection (AuthService.mjs) Stdio: env-var → gh api user fallback
RequestContextService populated ❌ no ✅ yes (TransportService wraps /mcp in run()) Stdio: wrap mcpServer.connect in run()
userId on ChromaDB writes ❌ untagged metadata.userId (MemoryService.mjs:92) Stdio: tagged once RequestContext flows
AgentIdentity node binding ❌ not done ❌ not done Both paths: GraphService.getNode({id: githubLogin})agentIdentityNodeId in context
Anti-spoof on write tools n/a (no surface) n/a (no surface) New: middleware rejects client-supplied identity-override fields

Existing infrastructure (touch, don't invent):

  • ai/mcp/server/shared/services/AuthService.mjs — OIDC handler (#10000). Extend to populate agentIdentityNodeId.
  • ai/mcp/server/shared/services/RequestContextService.mjs — AsyncLocalStorage propagation (#10000). Extend shape with agentIdentityNodeId, source.
  • ai/mcp/server/memory-core/services/MemoryService.mjs — already tags writes via getUserId(); no change needed, benefits transitively.
  • ai/scripts/seedAgentIdentities.mjs — binding consumer (#10144).

New files:

  • ai/mcp/server/shared/services/StdioIdentityResolver.mjs — env-var → gh-CLI → unresolved chain.
  • ai/mcp/server/shared/services/AuthMiddleware.mjs — anti-spoof guard at callTool dispatch.
  • learn/agentos/tooling/MemoryCoreMcpAuth.md — design doc.
  • test/playwright/unit/ai/mcp/server/memory-core/Auth.spec.mjs — coverage.

Modified file:

  • ai/mcp/server/memory-core/Server.mjs — stdio branch wraps mcpServer.connect(transport) in RequestContextService.run(identity, ...).

The Fix

  1. StdioIdentityResolver — reads NEO_AGENT_IDENTITY env-var; falls back to gh api user via child_process; returns {userId, username, agentIdentityNodeId, source: 'env-var' | 'gh-cli' | 'unresolved'}. Resolution runs once at server initAsync(); result cached on the running server instance.
  2. AgentIdentity graph binding — both paths (SSE AuthService.verifyAccessToken + stdio StdioIdentityResolver) call GraphService.getNode({id: githubLogin}); attach agentIdentityNodeId to the RequestContext. Missing-node case = null (not a fatal error — an unseeded identity can still write, just without graph edges).
  3. Server wiring — stdio branch in Server.mjs wraps mcpServer.connect(transport) in RequestContextService.run(identity, ...) so every subsequent callTool dispatch inherits the identity context.
  4. Anti-spoof middleware — thin layer at the callTool choke-point; rejects any write-tool call where args contains fields that would override server-stamped identity (userId, agentId, githubLogin, future from, etc.). Present-day no-op on existing tools; activates once schemas grow such fields.
  5. Design doclearn/agentos/tooling/MemoryCoreMcpAuth.md — documents both paths, identity resolution chain, AgentIdentity binding, anti-spoof invariants, OAuth 2.1 spec-version pin (per epic-review Stage 5 observation on #10016).
  6. PlaywrightAuth.spec.mjs covers: env-var resolution → RequestContext populated; gh-CLI fallback (mocked child_process.execSync); AgentIdentity graph lookup binds agentIdentityNodeId; anti-spoof accept-path + reject-path.

Acceptance Criteria

  • StdioIdentityResolver resolves NEO_AGENT_IDENTITYgh api user → unresolved; unit test covers all three branches
  • RequestContext shape extended with agentIdentityNodeId and source; existing getUserId() / getUsername() accessors unchanged (backward-compatible)
  • Server.mjs stdio branch wraps mcpServer.connect in RequestContextService.run() — subsequent addMemory calls tag writes with resolved userId
  • AgentIdentity graph lookup happens once per session (cached on connection), not per callTool invocation
  • Anti-spoof middleware rejects any write-tool call containing an identity-override field; accept-path test + reject-path test both green
  • Design doc shipped at learn/agentos/tooling/MemoryCoreMcpAuth.md; registered in learn/tree.json
  • Playwright spec at test/playwright/unit/ai/mcp/server/memory-core/Auth.spec.mjs passes under npm run test-unit

Out of Scope

  • OIDC/OAuth handshake for SSE transport — already shipped by #10000 (AuthService.mjs); this ticket only augments its context shape
  • OAuth App registration (client ID / secret provisioning) — out-of-repo ops task handled separately
  • GitHub account provisioning for per-model identities (email addresses, GH accounts for Opus + Gemini) — handled externally
  • Cross-tenant permission edges + integration test suite — scope of #10146
  • Team vs private read filter — scope of #10010
  • Migration of pre-existing untagged memories — scope of #10153 (lazy back-fill)
  • Neo Agent Harness external auth#10119 (different substrate, not blocking)

Avoided Traps

  • Re-implementing OAuth2 handshake for SSE. Rejected. AuthService.mjs already does introspection-based auth via #10000; a parallel handshake would create a second identity source of truth and diverge from the OIDC Gold Standard.
  • Trusting client-supplied userId / agent / githubLogin on write tools. Rejected. Server-stamps identity from RequestContext; middleware blocks override attempts even when schemas don't have such fields yet (defense-in-depth for #10139 additions).
  • Hard-failing stdio without NEO_AGENT_IDENTITY. Rejected. gh api user fallback preserves zero-friction local dev for humans; agents set the env-var explicitly in their harness config (Claude Code settings.json, Gemini .gemini/settings.json).
  • Threading identity as an explicit parameter through every service method. Rejected per #10000 precedent. AsyncLocalStorage isolates the cross-cutting concern cleanly; the two boundaries (Transport sets, Services read) are the only code touching identity.
  • Per-tool identity resolution instead of per-connection. Rejected. Resolve once at initAsync, cache on the context. Re-resolving per callTool would hit the GitHub API N times per session.
  • Conflating agent metadata (harness profile name) with githubLogin identity. agent: 'antigravity' stays metadata in AddMemoryRequest; githubLogin: '@neo-gemini-pro' is the auth key. Never reconcile them.

Related

  • Parent: #10016 (Multi-Tenant Identity & Data Privacy) — epic-review by Gemini 3.1 Pro (2026-04-21T14:28:30Z) + Claude Opus 4.7 (2026-04-21T18:22Z, this session)
  • Predecessor (already shipped): #10000 (Hardened Identity Ingestion — OIDC path + RequestContextService)
  • Predecessor (already shipped): #10144 (AgentIdentity node type + seed script, via PR #10162)
  • Blocks: #10146 (cross-tenant perms depends on identity plumbing), #10139 (Mailbox addMessage anti-spoof guard)
  • Adjacent: #10119 (different substrate — Neo Agent Harness external auth)
  • Files touched: ai/mcp/server/shared/services/{AuthService,RequestContextService,StdioIdentityResolver,AuthMiddleware}.mjs, ai/mcp/server/memory-core/Server.mjs, learn/agentos/tooling/MemoryCoreMcpAuth.md, learn/tree.json, test/playwright/unit/ai/mcp/server/memory-core/Auth.spec.mjs

Origin Session ID: 71dc3cd8-d39d-48e1-ac62-e240ca67d1a5 (ticket creation) Reshape Session ID: d69ac7a0-9fe8-4416-b766-cd9edb8bee71 (intake reshape 2026-04-21, Claude Opus 4.7 / Claude Code)

tobiu added the enhancement label on Apr 21, 2026, 11:29 AM
tobiu added the ai label on Apr 21, 2026, 11:29 AM
tobiu added the architecture label on Apr 21, 2026, 11:29 AM
tobiu added the core label on Apr 21, 2026, 11:29 AM
tobiu added parent issue #10016 on Apr 21, 2026, 11:32 AM
tobiu marked this issue as being blocked by #10144 on Apr 21, 2026, 11:33 AM
tobiu marked this issue as blocking #10146 on Apr 21, 2026, 11:33 AM
tobiu marked this issue as blocking #10139 on Apr 21, 2026, 11:33 AM
tobiu cross-referenced by PR #10155 on Apr 21, 2026, 1:32 PM
tobiu cross-referenced by #10016 on Apr 21, 2026, 4:28 PM
tobiu cross-referenced by PR #10161 on Apr 21, 2026, 4:36 PM
tobiu cross-referenced by PR #10162 on Apr 21, 2026, 4:54 PM
tobiu cross-referenced by #9999 on Apr 21, 2026, 7:03 PM
tobiu assigned to @tobiu on Apr 21, 2026, 8:23 PM
tobiu cross-referenced by PR #10166 on Apr 21, 2026, 9:06 PM
tobiu referenced in commit 47e209e - "fix(ai): bump gh CLI identity resolver timeout to 5s for MCP handshake budget (#10145) on Apr 21, 2026, 9:16 PM
tobiu referenced in commit 66f88bf - "fix(ai): invert gh CLI timeout to fail-fast at 1.5s (#10145) on Apr 21, 2026, 9:26 PM
tobiu referenced in commit 7db6c90 - "feat(ai): wire stdio identity + anti-spoof guard for Memory Core MCP (#10145) (#10166) on Apr 21, 2026, 10:16 PM
tobiu closed this issue on Apr 21, 2026, 10:16 PM
tobiu cross-referenced by #10147 on Apr 21, 2026, 10:30 PM
tobiu cross-referenced by #10146 on Apr 21, 2026, 10:30 PM
tobiu cross-referenced by PR #10167 on Apr 21, 2026, 11:06 PM
tobiu referenced in commit bb17e6b - "fix(ai): Memory Core ChromaDB legacy userId backfill + additive tenant read filter (#10556) (#10567) on May 1, 2026, 12:27 PM
tobiu referenced in commit 2405528 - "feat(ai): migrate memory-core/Server to extend BaseServer + beforeToolDispatch hook (#10965) (#10977) on May 8, 2026, 6:45 PM