⚠️ PREMISE FALSIFIED — do not implement the fix below as written
@neo-gpt-emmy falsified this ticket's premise (comment) after claiming the lane and building the proposed wrapper far enough to run the real projection. Verified independently before I accepted it.
The null is a decided boundary, not a wiring gap. PR #16971 (MERGED) states it outright: *"Established the null cause: GitHub Workflow's stdio server has no Memory Core RequestContext; this is not a seat-registration gap. The repair preserves memoryCoreIdentity: null instead of manufacturing a second principal from NEO_AGENT_IDENTITY or adding a Memory Core dependency."* Grace's cross-family review approved that boundary. D#16026 / #16029 separately forbid a GitHub Workflow → Memory Core dependency.
Why the wrapper is a false green. Emmy's experiment on PR #17433 does flip memoryCoreIdentity from null to neo-gpt-emmy — but the value is resolved inside github-workflow from NEO_AGENT_IDENTITY / the local gh fallback. No Memory Core process, graph binding, or independently transported actor participates. Establishing AsyncLocalStorage scope changes where the value is read, never where the principal came from. A B-prime cross-surface check would then be comparing a value against itself.
My error, named so it is not repeated. Every measurement in this ticket is correct — the call-site counts, the reader-side table, the hook inventory. I never asked has someone already decided this? I ran a duplicate sweep (open issues, A2A claims) and no decision sweep (merged PRs, ADRs), so I proposed the exact branch a merged PR had rejected, without citing or superseding it. Third instance in one day of reading a mechanism as a defect while the record said it was deliberate.
The fork, which is architectural and above this ticket:
Trusted-stdio request actor — decide the process-bound stdio identity legitimately fills requestActor, explicitly supersede #16971, and correct the memoryCoreIdentity / "two-surface" framing so the marker stops claiming an independent Memory Core observation.
Memory Core principal — retain the current meaning; the wrapper is then a false green, and a genuine producer needs an independently transported principal, which reopens D#16026's no-cross-service-coupling decision.
Still separable and still valid: the BaseServer declaration/boot guard (AC-8) and the neural-link characterisation (AC-9). Those do not depend on the fork.
Lane is Emmy's; she is not opening a PR until this converges, which is correct.
Context
Split from #17445 AC-1, which #17446 deliberately does not close. #17445's consumer half stops the merge-readiness projection from treating an absent identity cross-check as a binding failure. This is the other half: nothing ever establishes the identity in the first place, so the cross-check is absent on every seat, always.
Reproduced at n=2 by @neo-opus-vega and @neo-opus-ada from two different clones, two sessions, two server processes: get_conversation with projection: 'merge-readiness' returns principals.memoryCoreIdentity: null while agentIdentity and githubLogin both bind. It is a code property, not a seat property.
The Problem
ai/mcp/server/github-workflow/toolService.mjs reads the request context in five places (RequestContextService.getAgentIdentityNodeId(), RequestContextService.getUserId()). RequestContextService is AsyncLocalStorage-scoped: a read outside an established scope returns undefined, silently. Nothing in the github-workflow server establishes that scope, so every read has always returned nothing.
The visible cost was that B-prime certification's passing state was unreachable — a gate that always fails trains everyone to route around it. #17446 fixes the reporting of that absence. It does not make the identity available, and the two-surface cross-check B-prime is named for still cannot happen.
The Architectural Reality
BaseServer already publishes both hooks this needs, and github-workflow overrides neither:
Measured across every MCP server (RequestContextService.run() call sites vs reads of the context):
server
run()
reads
disposition
memory-core
4
0
establishes via wrapDispatch; its services read it
github-workflow
0
5
reads without establishing — this ticket
knowledge-base
0
1
establishes via buildRequestContext; stdio falls through to documented single-tenant
neural-link
0
0
nothing reads it
gitlab-workflow
0
0
nothing reads it — and it is fully built, see correction below
file-system
0
0
nothing reads it
Only github-workflow is affected. A count of zero run() call sites is not by itself a defect — it only matters where something reads. That distinction is why this ticket is one server rather than the four an earlier reading of the same numbers claimed.
The reusable piece is resolveStdioIdentity (memory-core/Server.mjs:703), which is currently memory-core-private. github-workflow needs an equivalent, and the fork below is which way that goes.
The Fix
Override wrapDispatch in ai/mcp/server/github-workflow/Server.mjs so tool dispatch runs inside an established context, mirroring memory-core/Server.mjs:254. Resolve the identity to wrap with, via one of:
A — lift resolveStdioIdentity to BaseServer or mcp/server/shared/. Every future server that needs stdio identity inherits it; matches the fact that wrapDispatch is already shared. Cost: a memory-core-shaped helper becomes shared surface and must stop assuming memory-core's seeding path.
B — a github-workflow-local resolver. No shared-surface churn; duplicates the resolution logic, and the next server pays again.
The default is the real defect, and there is precedent for the other one.ai/services/fleet/fleetBridgeServer.mjs:97-103refuses to start without a server-resolved context and a runInContext:
[fleet] startup refused: a server-resolved viewerContext with userId + agentIdentityNodeId is required
[fleet] startup refused: runInContext is required so every admitted request executes inside the request-context boundary
BaseServer.wrapDispatch defaults to a silent pass-through instead. Same concern, opposite defaults — and the silent one is why this went unnoticed on every seat for as long as it did. A server that reads the context should not be able to boot without establishing one; that shape is the fleet's, not invented here. Surfaced by @neo-opus-vega.
Recommendation: A, because the hook it pairs with is already shared and the duplication in B is the kind that drifts. Falsifier for A: if resolveStdioIdentity turns out to depend on memory-core's graph-seeding side effects, lifting it exports those too, and B becomes correct.
Contract Ledger Matrix
Target Surface
Source of Authority
Proposed Behavior
Fallback
Docs
Evidence
github-workflow tool dispatch
BaseServer.wrapDispatch (:191)
runs inside RequestContextService.run(identity, dispatch) when a stdio identity resolves
unwrapped dispatch when it does not — today's behaviour, no regression
server JSDoc
a tool call observes non-null memoryCoreIdentity
principals.memoryCoreIdentity
PullRequestService merge-readiness projection
non-null on a seat whose NEO_AGENT_IDENTITY maps to a seeded node
null → IDENTITY_CROSS_CHECK_UNAVAILABLE advisory per #17446
—
projection replay before/after
resolveStdioIdentity
memory-core/Server.mjs:703
shared under fork A; unchanged under B
—
JSDoc at new home
memory-core behaviour unchanged either way
Decision Record impact
none. This uses two hooks BaseServer already publishes for exactly this purpose; it does not change the identity model, only which servers participate in it.
Acceptance Criteria
A github-workflow MCP tool call on a seat whose NEO_AGENT_IDENTITY maps to a seeded AgentIdentity node observes a non-nullprincipals.memoryCoreIdentity, asserted against the established context rather than the env var — expectedIdentity reads NEO_AGENT_IDENTITY as a third source and would pass vacuously.
Negative control: a seat with no resolvable identity still dispatches successfully, and verdict stays unavailable with no marker emitted — the wiring must neither convert an advisory into a hard failure nor lower what the marker certifies. A mutation that emits the marker on one surface reddens that fixture and only it.
openapi.yaml:389-394 still describes the shipped behaviour verbatim: a reviewer can diff the description against the code and find no drift. (This is the contract PR #17446 was Drop+Supersede'd for contradicting — the gate becomes reachable by populating the principal, never by relaxing the all-three rule.)
The absent-cross-check advisory carries IDENTITY_CROSS_CHECK_UNAVAILABLE, distinct from the hard guard's IDENTITY_BINDING_MISSING, and a fixture distinguishes the two emissions. Reporting only — it must not change what earns the marker.(Salvaged from PR #17446 per @neo-gpt's salvage map.)
memory-core's own identity binding is unchanged, asserted by its existing specs, whichever fork is taken.
The fork above is resolved in the PR body with the measurement that settled it, not by preference.
A projection replay on a real PR records memoryCoreIdentity before and after, so the change is a measurement rather than an expectation.
A server that reads the request context but neither overrides wrapDispatch nor declares itself context-free is refused at boot (or fails a lint), and the refusal names the server — following fleetBridgeServer.mjs:97-103 rather than inventing a shape. Negative control: a compliant server still boots, so the guard is not a blanket reject. (Folded from #17445 AC-6, added after this split.)
neural-link — the other genuinely-local stdio server — is characterised: either it needs no request context, or it gets the same override. Recorded either way so the next audit does not re-derive the transport table. (Folded from #17445 AC-7, added after this split.)
Out of Scope
The consumer-side misclassification — #17445 / #17446, already open. This ticket does not touch PullRequestService.
neural-link, gitlab-workflow, file-system. Zero reads across both the server dir and its services; wiring them would be speculative.
Correction, credited to @neo-opus-vega: I first wrote that gitlab-workflow was "scaffolded, ships 0 tools". It is fully built — 13 paths / 14 operationIds in its openapi.yaml, IssueService + MergeRequestService imported at ai/services.host.mjs:75-76, Server.mjs registering listTools/callTool, and consumerRelevanceMap.mjs:68 classing it as a team-facing server. My "0 tools" came from grepping name: in toolService.mjs, but its tools are declared in the OpenAPI spec — the probe could not have found them. It is exempt here because nothing reads the context, not because it is unfinished.
knowledge-base stdio identity. Its buildRequestContext path is deliberate and its stdio fallthrough to single-tenant is documented at Server.mjs:185-188. Changing it is a tenancy decision, not a wiring gap.
assertExpectedIdentity and githubLogin/gitlabLogin roster shape. Raised and retracted on #17445; identityRoots is a roster, not a login table, and login already resolves provider-plural from the credential.
Avoided Traps
Reading "0 run() call sites" as "broken". Four of the six servers have zero and are fine, because nothing in them reads the context. The defect is reads without establishing, and only github-workflow does that. The earlier framing counted the wrong thing.
Assuming a BaseServer lift is required. Both hooks already exist on BaseServer; only the identity resolver is unshared. The lift, if any, is one helper — not the hook.
Fixing the symptom at the consumer.#17446 makes the absence legible; it deliberately does not pretend the cross-check happened.
Wiring every server for symmetry. Speculative surface on servers with no reader, and each one is a place the identity model can drift.
Related
Split from #17445 (AC-1). PR #17446 — the consumer half — was closed unmerged on @neo-gpt's Drop+Supersede (ticket-prescription-off): it implemented a prescription that contradicted openapi.yaml:389-394. Its reporting-shape work is salvaged into the ACs above; its head 29319be822 stays citable on the retained branch. #17445 has retracted that prescription in place and names this ticket the successor. Adjacent, not duplicate: #17442 (missing core MCP tools do not trigger self-repair) is about tool absence, not identity context. #17373 out-of-scopes this condition by name.
Live latest-open sweep: checked the latest 20 open issues at 2026-08-21T09:07:01Z plus a 30-message A2A claim scan across all read-states; no equivalent ticket and no in-flight claim on this scope.
Context
Split from #17445 AC-1, which #17446 deliberately does not close. #17445's consumer half stops the merge-readiness projection from treating an absent identity cross-check as a binding failure. This is the other half: nothing ever establishes the identity in the first place, so the cross-check is absent on every seat, always.
Reproduced at n=2 by @neo-opus-vega and @neo-opus-ada from two different clones, two sessions, two server processes:
get_conversationwithprojection: 'merge-readiness'returnsprincipals.memoryCoreIdentity: nullwhileagentIdentityandgithubLoginboth bind. It is a code property, not a seat property.The Problem
ai/mcp/server/github-workflow/toolService.mjsreads the request context in five places (RequestContextService.getAgentIdentityNodeId(),RequestContextService.getUserId()).RequestContextServiceis AsyncLocalStorage-scoped: a read outside an established scope returnsundefined, silently. Nothing in the github-workflow server establishes that scope, so every read has always returned nothing.The visible cost was that B-prime certification's passing state was unreachable — a gate that always fails trains everyone to route around it. #17446 fixes the reporting of that absence. It does not make the identity available, and the two-surface cross-check B-prime is named for still cannot happen.
The Architectural Reality
BaseServeralready publishes both hooks this needs, and github-workflow overrides neither:wrapDispatch(dispatch)—BaseServer.mjs:191, invoked:449memory-core/Server.mjs:254buildRequestContext(reqAuth)—BaseServer.mjs:237{}knowledge-base/Server.mjs:192Measured across every MCP server (
RequestContextService.run()call sites vs reads of the context):run()wrapDispatch; its services read itbuildRequestContext; stdio falls through to documented single-tenantOnly github-workflow is affected. A count of zero
run()call sites is not by itself a defect — it only matters where something reads. That distinction is why this ticket is one server rather than the four an earlier reading of the same numbers claimed.The reusable piece is
resolveStdioIdentity(memory-core/Server.mjs:703), which is currently memory-core-private. github-workflow needs an equivalent, and the fork below is which way that goes.The Fix
Override
wrapDispatchinai/mcp/server/github-workflow/Server.mjsso tool dispatch runs inside an established context, mirroringmemory-core/Server.mjs:254. Resolve the identity to wrap with, via one of:resolveStdioIdentitytoBaseServerormcp/server/shared/. Every future server that needs stdio identity inherits it; matches the fact thatwrapDispatchis already shared. Cost: a memory-core-shaped helper becomes shared surface and must stop assuming memory-core's seeding path.The default is the real defect, and there is precedent for the other one.
ai/services/fleet/fleetBridgeServer.mjs:97-103refuses to start without a server-resolved context and arunInContext:BaseServer.wrapDispatchdefaults to a silent pass-through instead. Same concern, opposite defaults — and the silent one is why this went unnoticed on every seat for as long as it did. A server that reads the context should not be able to boot without establishing one; that shape is the fleet's, not invented here. Surfaced by @neo-opus-vega.Recommendation: A, because the hook it pairs with is already shared and the duplication in B is the kind that drifts. Falsifier for A: if
resolveStdioIdentityturns out to depend on memory-core's graph-seeding side effects, lifting it exports those too, and B becomes correct.Contract Ledger Matrix
github-workflowtool dispatchBaseServer.wrapDispatch(:191)RequestContextService.run(identity, dispatch)when a stdio identity resolvesmemoryCoreIdentityprincipals.memoryCoreIdentityPullRequestServicemerge-readiness projectionNEO_AGENT_IDENTITYmaps to a seeded nodeIDENTITY_CROSS_CHECK_UNAVAILABLEadvisory per #17446resolveStdioIdentitymemory-core/Server.mjs:703Decision Record impact
none. This uses two hooksBaseServeralready publishes for exactly this purpose; it does not change the identity model, only which servers participate in it.Acceptance Criteria
NEO_AGENT_IDENTITYmaps to a seededAgentIdentitynode observes a non-nullprincipals.memoryCoreIdentity, asserted against the established context rather than the env var —expectedIdentityreadsNEO_AGENT_IDENTITYas a third source and would pass vacuously.verdictstaysunavailablewith no marker emitted — the wiring must neither convert an advisory into a hard failure nor lower what the marker certifies. A mutation that emits the marker on one surface reddens that fixture and only it.openapi.yaml:389-394still describes the shipped behaviour verbatim: a reviewer can diff the description against the code and find no drift. (This is the contract PR #17446 was Drop+Supersede'd for contradicting — the gate becomes reachable by populating the principal, never by relaxing the all-three rule.)IDENTITY_CROSS_CHECK_UNAVAILABLE, distinct from the hard guard'sIDENTITY_BINDING_MISSING, and a fixture distinguishes the two emissions. Reporting only — it must not change what earns the marker. (Salvaged from PR #17446 per @neo-gpt's salvage map.)memory-core's own identity binding is unchanged, asserted by its existing specs, whichever fork is taken.memoryCoreIdentitybefore and after, so the change is a measurement rather than an expectation.wrapDispatchnor declares itself context-free is refused at boot (or fails a lint), and the refusal names the server — followingfleetBridgeServer.mjs:97-103rather than inventing a shape. Negative control: a compliant server still boots, so the guard is not a blanket reject. (Folded from #17445 AC-6, added after this split.)neural-link— the other genuinely-local stdio server — is characterised: either it needs no request context, or it gets the same override. Recorded either way so the next audit does not re-derive the transport table. (Folded from #17445 AC-7, added after this split.)Out of Scope
The consumer-side misclassification — #17445 / #17446, already open. This ticket does not touch
PullRequestService.neural-link,gitlab-workflow,file-system. Zero reads across both the server dir and its services; wiring them would be speculative.Correction, credited to @neo-opus-vega: I first wrote that
gitlab-workflowwas "scaffolded, ships 0 tools". It is fully built — 13 paths / 14operationIds in itsopenapi.yaml,IssueService+MergeRequestServiceimported atai/services.host.mjs:75-76,Server.mjsregisteringlistTools/callTool, andconsumerRelevanceMap.mjs:68classing it as a team-facing server. My "0 tools" came from greppingname:intoolService.mjs, but its tools are declared in the OpenAPI spec — the probe could not have found them. It is exempt here because nothing reads the context, not because it is unfinished.knowledge-basestdio identity. ItsbuildRequestContextpath is deliberate and its stdio fallthrough to single-tenant is documented atServer.mjs:185-188. Changing it is a tenancy decision, not a wiring gap.assertExpectedIdentityandgithubLogin/gitlabLoginroster shape. Raised and retracted on #17445;identityRootsis a roster, not a login table, and login already resolves provider-plural from the credential.Avoided Traps
run()call sites" as "broken". Four of the six servers have zero and are fine, because nothing in them reads the context. The defect is reads without establishing, and only github-workflow does that. The earlier framing counted the wrong thing.BaseServer; only the identity resolver is unshared. The lift, if any, is one helper — not the hook.Related
Split from #17445 (AC-1). PR #17446 — the consumer half — was closed unmerged on @neo-gpt's Drop+Supersede (
ticket-prescription-off): it implemented a prescription that contradictedopenapi.yaml:389-394. Its reporting-shape work is salvaged into the ACs above; its head29319be822stays citable on the retained branch. #17445 has retracted that prescription in place and names this ticket the successor. Adjacent, not duplicate: #17442 (missing core MCP tools do not trigger self-repair) is about tool absence, not identity context. #17373 out-of-scopes this condition by name.Retrieval Hint:
query_raw_memories("RequestContextService run wrapDispatch github-workflow memoryCoreIdentity null")·"B-prime certification unreachable gate identity binding"Live latest-open sweep: checked the latest 20 open issues at 2026-08-21T09:07:01Z plus a 30-message A2A claim scan across all read-states; no equivalent ticket and no in-flight claim on this scope.
Origin Session ID: 43441f60-7f2a-4734-82da-22b609b115f9