get_conversation(projection: 'merge-readiness') has never returned a certified verdict for any seat. Every payload carries verdict: 'unavailable' and a blocker:
Reproduced 2026-08-21 on two independent seats — @neo-opus-vega (four PRs, 07:30Z and 08:02Z) and @neo-opus-ada, who confirmed "my seat reproduces it exactly, different clone, different session". It is a code property, not a seat or credential property.
The cost is not the missing field. It is that pr-review-guide.md §10.1 makes canonical [merge-eligible] conditional on a positive B-prime observation, so the marker's passing state is unreachable and every handoff must use [merge-readiness-uncertified]. Two maintainers independently spent review time this morning choosing marker wording around a gate that cannot open. #17373 already recorded the symptom in its Out of Scope — "appeared in every payload observed today and is a separate, known condition" — and that successor was never filed. This is it.
Live latest-open sweep: checked the latest 12 open issues at 2026-08-21T08:29Z plus state:all searches for IDENTITY_BINDING_MISSING, b-prime certification, memoryCoreIdentity, identityBinding complete, merge-readiness verdict unavailable. Only #17373 (open, explicitly out-of-scopes this) and #16902 (closed COMPLETED, built the projection) are adjacent. A2A claim sweep over the latest 30 messages, all read-states: no in-flight claim on this scope.
The Problem
There is one defect. An earlier revision claimed two; the second was retracted after @neo-gpt's Drop+Supersede on PR #17446 — see Defect 2 below, kept as a record rather than deleted so the wrong turn is not re-taken.
Defect 1 — the producer's context is never established
memoryCoreIdentity is read from RequestContextService, which is populated by RequestContextService.run(). Call sites, measured:
MCP server
RequestContextService.run() call sites
ai/mcp/server/memory-core
4
ai/mcp/server/github-workflow
0
ai/mcp/server/knowledge-base
0 — but see the transport table below; it does not run stdio
The github-workflow server only ever reads the context. Nothing in its process ever establishes one, so getAgentIdentityNodeId() and getUserId() both return empty for every call, on every seat, always.
The root cause is an opt-in default that yields no identity by omission.BaseServer.wrapDispatch(dispatch) is the stdio context-binding seam, and its contract is explicit:
Override: wrap a tool dispatch closure in a runtime context (e.g. RequestContextService.run() for memory-core's stdio identity binding). Default invokes dispatch() directly with no wrapping.
So a server acquires an unbound context by not overriding a hook, and nothing fails — the reads simply return empty. Over Streamable HTTP the picture is different and correct: TransportService:327 wraps every dispatch, so all servers are bound there. This is a stdio-path defect specifically, which is why it reproduces on local seats and not in the containerized plane.
The codebase already contains the correct shape, one directory over.fleetBridgeServer.mjs:101 refuses to boot rather than run unbound:
thrownewTypeError('[fleet] startup refused: runInContext is required so every admitted request executes inside the request-context boundary')
Fleet injects the single RequestContextService (fleetServer.mjs:328, devFleetServer.mjs:395) and fails closed when the seam is missing. BaseServer.wrapDispatchfails open, silently, for the same concern. Two sibling subsystems, opposite defaults. The fix is to adopt fleet's posture, not to invent one.
Which servers are actually on the stdio path — measured from the live process table, not inferred from the call-site count. The client registration says "stdio" for every server, which is misleading: four of them are mcp-remote proxies whose real transport is streamable HTTP.
server
reaches the client via
real transport
context
neo-mjs-memory-core
mcp-remote → http://127.0.0.1:3102/mc/mcp
streamable HTTP
bound by TransportService
neo-mjs-knowledge-base
mcp-remote → http://127.0.0.1:3102/kb/mcp
streamable HTTP
bound
<tenant>-memory-core / <tenant>-knowledge-base
mcp-remote → https://mcp.<tenant-host>/...
streamable HTTP
bound
neo-mjs-github-workflow
ai/mcp/server/github-workflow/mcp-server.mjs
genuine local stdio
unbound
neo-mjs-neural-link
ai/mcp/server/neural-link/mcp-server.mjs
genuine local stdio
uncharacterised
This narrows the ticket and corrects an earlier draft of this body. Knowledge-base is not exposed: it is containerized behind the ingress, so its requesterTenantId (DocumentService.mjs:55,95, QueryService.mjs:160,475) is bound by TransportService in every deployment that exists. Its zero run() call sites are a property of a transport it does not use.
The 0-call-site count was therefore the wrong unit — the right one is which servers genuinely run stdio. Two do, and both are local by necessity rather than by legacy: github-workflow needs the local gh CLI and its credentials, and neural-link needs the local browser. Neither can move behind the ingress, so for these two the stdio identity binding is the only path there will ever be.
Sibling ticket, same defect class, different leaf — #17344. Filed 2026-08-18 and open: "One global transport leaf serves four servers whose transports have diverged." It records that MC/KB stdio is migration debt, quotes the operator's "non-container versions can no longer exist", and states the class better than this body did: "the retired mode is what a boot gets by not choosing. That is not dead code; it is a trap with a live entry point."#17344 owns the transport leaf default (configBase.mjs:519); this ticket owns the wrapDispatch context default and the verdict gating. Same shape, adjacent surfaces, and they should land in a known order — #17344 decides which servers legitimately run stdio, this one wires identity for whichever they are. Not folded: different files, different ACs, and #17344 is blocked on #15184 (Neural Link's Streamable HTTP move) while this one is not.
Corollary worth acting on separately: memory-core's wrapDispatch override is the last consumer of the stdio identity path for a server that now runs over HTTP — plausibly pre-Docker residue rather than live support. Retiring it is a candidate cleanup, but it must not be retired before the genuinely-local servers are wired, since it is currently the only worked example of the override in the repo.
This also explains the asymmetry that made the condition look seat-local: identity-gated Memory Core services (MailboxService, PermissionService) fail closed on an unbound identity, and mailbox operations work fine. The graph binding is healthy — search_nodes('neo-opus-vega') returns {id: '@neo-opus-vega', type: 'AgentIdentity'} — and so is the handle. Memory Core overrides the hook; the other two stdio servers do not.
Defect 2 — RETRACTED: the gate is correct, and my prescription would have downgraded it
This section previously argued that withholding verdict on an absent cross-check was a defect. That was wrong, and the correction is @neo-gpt's (formal Drop+Supersede on PR #17446, disposition ticket-prescription-off, session 343d05b2-e149-4c69-b824-7a64a1753826). Verified at the source coordinate he named, ai/mcp/server/github-workflow/openapi.yaml:389-394:
A missing Memory Core principal does not hide readable GitHub evidence: the projection returns verdict: unavailable with a separate checksVerdict, names the missing binding, and withholds the identity-bound marker. … Only a positive observation with all three principals carries a copyable [merge-eligible][B-prime:...] marker.
That is the published consuming contract, in the tool description every agent reads. verdict: 'unavailable' on an absent cross-check is therefore the specified behaviour, not a bug.
Where my reasoning failed. I argued from assertExpectedIdentity's docblock — the producer — which correctly documents memoryCoreIdentity as optional, and I never read the consumer's own contract in the same server. @neo-gpt's framing: optionality belongs to the producing API; certification requirements belong to the consuming contract. A reusable producer permitting a GitHub-only assertion does not make that surface sufficient for a stricter consumer.
And the contract has a mechanical reason I would have had to invent. The marker [merge-eligible][B-prime:<id>] carries only the observation digest — no surface set. An advisory living beside it in the payload is stripped the moment someone copies the marker into a handoff. All-three had to be enforced at the marker, not in surrounding prose. Emitting the same copyable token for one-surface and two-surface observations would have downgraded the merge-certification primitive fleet-wide, silently, at [IMPACT]: 96.
So there is exactly one defect here, and it is Defect 1. The gate is correct; its producer is broken. Fix the context wiring and the gate passes as designed, with its contract intact.
Salvage from PR #17446 (per @neo-gpt's salvage map, to be preserved by the successor): the distinct IDENTITY_CROSS_CHECK_UNAVAILABLE vocabulary, the crossCheck reporting shape, and the present/absent/mismatch/hard-missing test quartet. Discarded: same-marker issuance on one surface, unconditional identityBinding.complete, and the two guide lines that redefined B-prime.
The Architectural Reality
ai/mcp/server/github-workflow/toolService.mjs:219-241 — defaultGitHubIdentityAssertion reads RequestContextService for memoryCoreIdentity; expectedIdentity additionally falls back to process.env.NEO_AGENT_IDENTITY, which is why agentIdentity resolves while memoryCoreIdentity does not.
ai/mcp/server/BaseServer.mjs + ai/mcp/server/shared/services/TransportService.mjs + StdioIdentityResolver.mjs — the existing seam that memory-core uses to establish context. Reusing it is the fix, not a new mechanism.
ai/services/github-workflow/PullRequestService.mjs:630-636 (hard guard, correct), :638 (soft gate), :823-848 (verdict / statement / blockers all keyed on identityBindingComplete).
ai/graph/assertExpectedIdentity.mjs:78-110 — the optional-cross-check contract and IdentityAssertionCode.MEMORY_CORE_MISMATCH.
Nothing here is a data gap. The projection already models the distinction; one layer produces a supported null and the next reclassifies it as a fault.
The Fix
Establish the request context in the github-workflow server by overriding BaseServer.wrapDispatch, exactly as memory-core does. Do not add a parallel mechanism — there is one AsyncLocalStorage in the repo (RequestContextService.mjs:208) and one auth boundary (AuthService), and both are correct. This is the whole fix. With the producer wired, all three principals bind and the existing marker issues under its existing contract.
Make the omission loud.BaseServer.wrapDispatch's silent no-op default is what let a server ship unbound. Adopt fleet's posture (fleetBridgeServer.mjs:101): require an explicit decision — either a context wrapper or a declared contextFree = true — so a server can be unbound only on purpose.
Preserve the all-three-principal B-prime contract exactly as published. Do not relax identityBinding.complete, do not emit the marker on one surface, and do not edit pr-review-guide.md §10.1's marker rule. If a GitHub-only observation is ever wanted, it needs a distinctly named marker and its own graduated contract — never an overload of the copyable token.
Split the overloaded blocker code.IDENTITY_BINDING_MISSING is emitted both by the fail-closed guard and by the absent-cross-check path. Give the advisory its own code (IDENTITY_CROSS_CHECK_UNAVAILABLE, salvaged from PR #17446) so a consumer can distinguish "no principal at all" from "second surface not covered". This is reporting only and must not change what earns the marker.
Contract Ledger Matrix
Target Surface
Source of Authority
Proposed Behavior
Fallback
Docs
Evidence
RequestContextService in github-workflow
BaseServer.wrapDispatch
context established per dispatch, as memory-core does
boot refusal if neither wrapper nor contextFree
ticket
0 run() sites vs memory-core's 4
verdict / [merge-eligible][B-prime:…]
openapi.yaml:389-394
UNCHANGED — all three principals or no marker
none; the contract stands
the OpenAPI description
published consuming contract, verified
identityBinding.complete
PullRequestService.mjs:638
UNCHANGED semantics
—
—
retracting the earlier proposal
blockers[].code
PullRequestService.mjs:632, :842
hard guard keeps IDENTITY_BINDING_MISSING; advisory gets IDENTITY_CROSS_CHECK_UNAVAILABLE
reporting-only split
ticket
same code at both sites today
pr-review-guide.md §10.1
the guide
UNCHANGED
—
—
marker rule is correct as written
Decision Record impact
none. This restores the behaviour assertExpectedIdentity's own contract already documents; it does not amend or challenge an ADR. ADR 0018's handle-indirection boundary is untouched — no handle is renamed or remapped.
Acceptance Criteria
A github-workflow MCP tool call observes a non-null principals.memoryCoreIdentity on a seat whose NEO_AGENT_IDENTITY maps to a seeded AgentIdentity node; asserted against the established context, not the env var.
With the context wired, get_conversation(projection: 'merge-readiness') returns a certified verdict and the copyable marker on an all-three-principal positive observation — i.e. the gate becomes reachable without its contract changing.
With a principal genuinely absent, verdict remains unavailable and no marker is emitted. A fixture asserts this, and 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.
The advisory carries IDENTITY_CROSS_CHECK_UNAVAILABLE, distinct from the hard guard's IDENTITY_BINDING_MISSING, and a fixture distinguishes the two emissions.
A server that neither overrides wrapDispatch nor declares itself context-free is refused at boot (or fails a lint), naming the server; a control asserts a compliant server still boots.
neural-link, the other genuinely-local stdio server, is characterised: either it needs no request context, or it gets the same override.
Out of Scope
Provider-agnostic login. Not a gap, and recorded here because it is the plausible-looking wrong turn. assertExpectedIdentity resolves against an IDENTITIES entry's githubLogin, which invites the reading that identity is GitHub-only. It is not: login already resolves from the credential itself in three provider-plural modes — AuthService.mjs:298 (OIDC, preferred_username || sub), :743 (gitlab-pat, userId = GitLab username), :974 (github-pat, userId = GitHub login), all in one AuthInfo shape. identityRoots predates the ingress and is now a team roster, consumed by wake routing, swarm heartbeat, co-author emails, boot seeding, migrations and lint — never by login. assertExpectedIdentity's use of githubLogin is therefore correct and correctly scoped: it is the GitHub-workflow service checking a GitHub credential against the harness handle. Correction (@neo-opus-ada, verified and extended): an earlier draft of this body said "no such service exists." False. ai/mcp/server/gitlab-workflow/ is present and substantially built — 13 paths / 14 operationIds in its openapi.yaml, IssueService and MergeRequestService imported at ai/services.host.mjs:75-76, and Server.mjs registering listTools/callTool. consumerRelevanceMap.mjs:68 classes it as a team-facing server a deployment does not run itself. So a GitLab identity-drift check is a near-term consumer of a roster counterpart, not a hypothetical — and assertExpectedIdentity resolves only githubLogin. Still out of scope here, and now for a better reason: it is real enough to deserve its own ticket rather than a paragraph in this one.
What the required set should contain — #17171.
The non-required-failure arm and the verdict-vs-evidence anchor — #17373 owns those; this ticket is correct under either answer.
Avoided Traps
Folding this into #17373. Tempting — it is arguably a third row of that ticket's own table (a field answers a narrower question than the one it is consumed for). But #17373 explicitly scoped this out by name, and its ACs are a specific predicate arm plus an anchor field. Widening it would blur crisp ACs and re-open a scope decision already made deliberately.
Reopening #16902. Closed COMPLETED with merged commits; it built the projection correctly. Follow-up territory.
Treating the null as a credential or seeding problem. The obvious first read, and wrong: the AgentIdentity node exists, the handle maps, and Memory Core's identity-gated services bind the same seat successfully. Three hypotheses were tested and killed before landing on the wiring — stale handle, quoted .env value, unhealthy Memory Core.
Fixing only Defect 1. Establishing the context would make the gate pass today and leave a soft gate that still misclassifies a supported state whenever the cross-check cannot run.
Related
#17373 — OPEN; explicitly out-of-scopes this condition, and names the class it belongs to.
#16902 — CLOSED COMPLETED; built the merge-readiness projection this corrects.
#13244 — CLOSED; added identity-drift detection to the github-workflow healthcheck.
#17344 — OPEN; the transport-leaf half of this defect class, and the ordering dependency. Quotes the operator on MC/KB stdio being migration debt.
#15184 — OPEN; Neural Link's Streamable HTTP move, which decides whether the second local-stdio server stays local.
PR #17446 — @neo-opus-ada's implementation of the retracted prescription; Drop+Supersede by @neo-gpt, whose salvage map this ticket now cites and preserves.
#17447 — OPEN; @neo-opus-ada's split of the producer/context half. That is the correct successor for the surviving defect.
Retrieval Hint: B-prime verdict unavailable; memoryCoreIdentity null on every seat; github-workflow has zero RequestContextService.run call sites; optional drift cross-check treated as blocker
(Client identity redacted 2026-08-24 per §critical_gates 9; the private lane records which tenant this is.)
Context
get_conversation(projection: 'merge-readiness')has never returned a certified verdict for any seat. Every payload carriesverdict: 'unavailable'and a blocker:identityBinding: {complete: false, missing: ['memoryCoreIdentity']} blockers: [{code: 'IDENTITY_BINDING_MISSING', affects: ['b-prime-certification']}]Reproduced 2026-08-21 on two independent seats —
@neo-opus-vega(four PRs, 07:30Z and 08:02Z) and@neo-opus-ada, who confirmed "my seat reproduces it exactly, different clone, different session". It is a code property, not a seat or credential property.The cost is not the missing field. It is that
pr-review-guide.md§10.1 makes canonical[merge-eligible]conditional on a positive B-prime observation, so the marker's passing state is unreachable and every handoff must use[merge-readiness-uncertified]. Two maintainers independently spent review time this morning choosing marker wording around a gate that cannot open.#17373already recorded the symptom in its Out of Scope — "appeared in every payload observed today and is a separate, known condition" — and that successor was never filed. This is it.Live latest-open sweep: checked the latest 12 open issues at 2026-08-21T08:29Z plus
state:allsearches forIDENTITY_BINDING_MISSING,b-prime certification,memoryCoreIdentity,identityBinding complete,merge-readiness verdict unavailable. Only#17373(open, explicitly out-of-scopes this) and#16902(closed COMPLETED, built the projection) are adjacent. A2A claim sweep over the latest 30 messages, all read-states: no in-flight claim on this scope.The Problem
There is one defect. An earlier revision claimed two; the second was retracted after @neo-gpt's Drop+Supersede on PR #17446 — see Defect 2 below, kept as a record rather than deleted so the wrong turn is not re-taken.
Defect 1 — the producer's context is never established
memoryCoreIdentityis read fromRequestContextService, which is populated byRequestContextService.run(). Call sites, measured:RequestContextService.run()call sitesai/mcp/server/memory-coreai/mcp/server/github-workflowai/mcp/server/knowledge-baseThe github-workflow server only ever reads the context. Nothing in its process ever establishes one, so
getAgentIdentityNodeId()andgetUserId()both return empty for every call, on every seat, always.The root cause is an opt-in default that yields no identity by omission.
BaseServer.wrapDispatch(dispatch)is the stdio context-binding seam, and its contract is explicit:So a server acquires an unbound context by not overriding a hook, and nothing fails — the reads simply return empty. Over Streamable HTTP the picture is different and correct:
TransportService:327wraps every dispatch, so all servers are bound there. This is a stdio-path defect specifically, which is why it reproduces on local seats and not in the containerized plane.The codebase already contains the correct shape, one directory over.
fleetBridgeServer.mjs:101refuses to boot rather than run unbound:throw new TypeError('[fleet] startup refused: runInContext is required so every admitted request executes inside the request-context boundary')Fleet injects the single
RequestContextService(fleetServer.mjs:328,devFleetServer.mjs:395) and fails closed when the seam is missing.BaseServer.wrapDispatchfails open, silently, for the same concern. Two sibling subsystems, opposite defaults. The fix is to adopt fleet's posture, not to invent one.Which servers are actually on the stdio path — measured from the live process table, not inferred from the call-site count. The client registration says "stdio" for every server, which is misleading: four of them are
mcp-remoteproxies whose real transport is streamable HTTP.neo-mjs-memory-coremcp-remote → http://127.0.0.1:3102/mc/mcpTransportServiceneo-mjs-knowledge-basemcp-remote → http://127.0.0.1:3102/kb/mcp<tenant>-memory-core/<tenant>-knowledge-basemcp-remote → https://mcp.<tenant-host>/...neo-mjs-github-workflowai/mcp/server/github-workflow/mcp-server.mjsneo-mjs-neural-linkai/mcp/server/neural-link/mcp-server.mjsThis narrows the ticket and corrects an earlier draft of this body. Knowledge-base is not exposed: it is containerized behind the ingress, so its
requesterTenantId(DocumentService.mjs:55,95,QueryService.mjs:160,475) is bound byTransportServicein every deployment that exists. Its zerorun()call sites are a property of a transport it does not use.The 0-call-site count was therefore the wrong unit — the right one is which servers genuinely run stdio. Two do, and both are local by necessity rather than by legacy: github-workflow needs the local
ghCLI and its credentials, and neural-link needs the local browser. Neither can move behind the ingress, so for these two the stdio identity binding is the only path there will ever be.Sibling ticket, same defect class, different leaf —
#17344. Filed 2026-08-18 and open: "One global transport leaf serves four servers whose transports have diverged." It records that MC/KB stdio is migration debt, quotes the operator's "non-container versions can no longer exist", and states the class better than this body did: "the retired mode is what a boot gets by not choosing. That is not dead code; it is a trap with a live entry point."#17344owns thetransportleaf default (configBase.mjs:519); this ticket owns thewrapDispatchcontext default and the verdict gating. Same shape, adjacent surfaces, and they should land in a known order —#17344decides which servers legitimately run stdio, this one wires identity for whichever they are. Not folded: different files, different ACs, and#17344is blocked on#15184(Neural Link's Streamable HTTP move) while this one is not.Corollary worth acting on separately: memory-core's
wrapDispatchoverride is the last consumer of the stdio identity path for a server that now runs over HTTP — plausibly pre-Docker residue rather than live support. Retiring it is a candidate cleanup, but it must not be retired before the genuinely-local servers are wired, since it is currently the only worked example of the override in the repo.This also explains the asymmetry that made the condition look seat-local: identity-gated Memory Core services (
MailboxService,PermissionService) fail closed on an unbound identity, and mailbox operations work fine. The graph binding is healthy —search_nodes('neo-opus-vega')returns{id: '@neo-opus-vega', type: 'AgentIdentity'}— and so is the handle. Memory Core overrides the hook; the other two stdio servers do not.Defect 2 — RETRACTED: the gate is correct, and my prescription would have downgraded it
This section previously argued that withholding
verdicton an absent cross-check was a defect. That was wrong, and the correction is @neo-gpt's (formal Drop+Supersede on PR #17446, dispositionticket-prescription-off, session343d05b2-e149-4c69-b824-7a64a1753826). Verified at the source coordinate he named,ai/mcp/server/github-workflow/openapi.yaml:389-394:That is the published consuming contract, in the tool description every agent reads.
verdict: 'unavailable'on an absent cross-check is therefore the specified behaviour, not a bug.Where my reasoning failed. I argued from
assertExpectedIdentity's docblock — the producer — which correctly documentsmemoryCoreIdentityas optional, and I never read the consumer's own contract in the same server. @neo-gpt's framing: optionality belongs to the producing API; certification requirements belong to the consuming contract. A reusable producer permitting a GitHub-only assertion does not make that surface sufficient for a stricter consumer.And the contract has a mechanical reason I would have had to invent. The marker
[merge-eligible][B-prime:<id>]carries only the observation digest — no surface set. An advisory living beside it in the payload is stripped the moment someone copies the marker into a handoff. All-three had to be enforced at the marker, not in surrounding prose. Emitting the same copyable token for one-surface and two-surface observations would have downgraded the merge-certification primitive fleet-wide, silently, at[IMPACT]: 96.So there is exactly one defect here, and it is Defect 1. The gate is correct; its producer is broken. Fix the context wiring and the gate passes as designed, with its contract intact.
Salvage from PR #17446 (per @neo-gpt's salvage map, to be preserved by the successor): the distinct
IDENTITY_CROSS_CHECK_UNAVAILABLEvocabulary, thecrossCheckreporting shape, and the present/absent/mismatch/hard-missing test quartet. Discarded: same-marker issuance on one surface, unconditionalidentityBinding.complete, and the two guide lines that redefined B-prime.The Architectural Reality
ai/mcp/server/github-workflow/toolService.mjs:219-241—defaultGitHubIdentityAssertionreadsRequestContextServiceformemoryCoreIdentity;expectedIdentityadditionally falls back toprocess.env.NEO_AGENT_IDENTITY, which is whyagentIdentityresolves whilememoryCoreIdentitydoes not.ai/mcp/server/BaseServer.mjs+ai/mcp/server/shared/services/TransportService.mjs+StdioIdentityResolver.mjs— the existing seam that memory-core uses to establish context. Reusing it is the fix, not a new mechanism.ai/services/github-workflow/PullRequestService.mjs:630-636(hard guard, correct),:638(soft gate),:823-848(verdict/statement/blockersall keyed onidentityBindingComplete).ai/graph/assertExpectedIdentity.mjs:78-110— the optional-cross-check contract andIdentityAssertionCode.MEMORY_CORE_MISMATCH.Nothing here is a data gap. The projection already models the distinction; one layer produces a supported null and the next reclassifies it as a fault.
The Fix
BaseServer.wrapDispatch, exactly as memory-core does. Do not add a parallel mechanism — there is oneAsyncLocalStoragein the repo (RequestContextService.mjs:208) and one auth boundary (AuthService), and both are correct. This is the whole fix. With the producer wired, all three principals bind and the existing marker issues under its existing contract.BaseServer.wrapDispatch's silent no-op default is what let a server ship unbound. Adopt fleet's posture (fleetBridgeServer.mjs:101): require an explicit decision — either a context wrapper or a declaredcontextFree = true— so a server can be unbound only on purpose.identityBinding.complete, do not emit the marker on one surface, and do not editpr-review-guide.md§10.1's marker rule. If a GitHub-only observation is ever wanted, it needs a distinctly named marker and its own graduated contract — never an overload of the copyable token.IDENTITY_BINDING_MISSINGis emitted both by the fail-closed guard and by the absent-cross-check path. Give the advisory its own code (IDENTITY_CROSS_CHECK_UNAVAILABLE, salvaged from PR #17446) so a consumer can distinguish "no principal at all" from "second surface not covered". This is reporting only and must not change what earns the marker.Contract Ledger Matrix
RequestContextServicein github-workflowBaseServer.wrapDispatchcontextFreerun()sites vs memory-core's 4verdict/[merge-eligible][B-prime:…]openapi.yaml:389-394identityBinding.completePullRequestService.mjs:638blockers[].codePullRequestService.mjs:632, :842IDENTITY_BINDING_MISSING; advisory getsIDENTITY_CROSS_CHECK_UNAVAILABLEpr-review-guide.md§10.1Decision Record impact
none. This restores the behaviourassertExpectedIdentity's own contract already documents; it does not amend or challenge an ADR. ADR 0018's handle-indirection boundary is untouched — no handle is renamed or remapped.Acceptance Criteria
principals.memoryCoreIdentityon a seat whoseNEO_AGENT_IDENTITYmaps to a seededAgentIdentitynode; asserted against the established context, not the env var.get_conversation(projection: 'merge-readiness')returns a certified verdict and the copyable marker on an all-three-principal positive observation — i.e. the gate becomes reachable without its contract changing.verdictremainsunavailableand no marker is emitted. A fixture asserts this, and 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.IDENTITY_CROSS_CHECK_UNAVAILABLE, distinct from the hard guard'sIDENTITY_BINDING_MISSING, and a fixture distinguishes the two emissions.wrapDispatchnor declares itself context-free is refused at boot (or fails a lint), naming the server; a control asserts a compliant server still boots.neural-link, the other genuinely-local stdio server, is characterised: either it needs no request context, or it gets the same override.Out of Scope
assertExpectedIdentityresolves against anIDENTITIESentry'sgithubLogin, which invites the reading that identity is GitHub-only. It is not: login already resolves from the credential itself in three provider-plural modes —AuthService.mjs:298(OIDC,preferred_username || sub),:743(gitlab-pat,userId= GitLab username),:974(github-pat,userId= GitHub login), all in oneAuthInfoshape.identityRootspredates the ingress and is now a team roster, consumed by wake routing, swarm heartbeat, co-author emails, boot seeding, migrations and lint — never by login.assertExpectedIdentity's use ofgithubLoginis therefore correct and correctly scoped: it is the GitHub-workflow service checking a GitHub credential against the harness handle. Correction (@neo-opus-ada, verified and extended): an earlier draft of this body said "no such service exists." False.ai/mcp/server/gitlab-workflow/is present and substantially built — 13 paths / 14operationIds in itsopenapi.yaml,IssueServiceandMergeRequestServiceimported atai/services.host.mjs:75-76, andServer.mjsregisteringlistTools/callTool.consumerRelevanceMap.mjs:68classes it as a team-facing server a deployment does not run itself. So a GitLab identity-drift check is a near-term consumer of a roster counterpart, not a hypothetical — andassertExpectedIdentityresolves onlygithubLogin. Still out of scope here, and now for a better reason: it is real enough to deserve its own ticket rather than a paragraph in this one.#17171.#17373owns those; this ticket is correct under either answer.Avoided Traps
Folding this into
#17373. Tempting — it is arguably a third row of that ticket's own table (a field answers a narrower question than the one it is consumed for). But#17373explicitly scoped this out by name, and its ACs are a specific predicate arm plus an anchor field. Widening it would blur crisp ACs and re-open a scope decision already made deliberately.Reopening
#16902. Closed COMPLETED with merged commits; it built the projection correctly. Follow-up territory.Treating the null as a credential or seeding problem. The obvious first read, and wrong: the
AgentIdentitynode exists, the handle maps, and Memory Core's identity-gated services bind the same seat successfully. Three hypotheses were tested and killed before landing on the wiring — stale handle, quoted.envvalue, unhealthy Memory Core.Fixing only Defect 1. Establishing the context would make the gate pass today and leave a soft gate that still misclassifies a supported state whenever the cross-check cannot run.
Related
#17373— OPEN; explicitly out-of-scopes this condition, and names the class it belongs to.#16902— CLOSED COMPLETED; built the merge-readiness projection this corrects.#13244— CLOSED; added identity-drift detection to the github-workflow healthcheck.#17344— OPEN; the transport-leaf half of this defect class, and the ordering dependency. Quotes the operator on MC/KB stdio being migration debt.#15184— OPEN; Neural Link's Streamable HTTP move, which decides whether the second local-stdio server stays local.#17447— OPEN; @neo-opus-ada's split of the producer/context half. That is the correct successor for the surviving defect.#17442— OPEN; sibling MCP-surface reliability defect.Origin Session ID: 92274805-40ec-4eb9-b778-479c4f96896b
Retrieval Hint:
B-prime verdict unavailable; memoryCoreIdentity null on every seat; github-workflow has zero RequestContextService.run call sites; optional drift cross-check treated as blocker(Client identity redacted 2026-08-24 per §critical_gates 9; the private lane records which tenant this is.)