Repository navigation
mcp: the stdio door resolves an API key with no tenancyPosture — an ex-member's org-stamped key is admitted with its own unvetted claim #15348
Description
Activity
Unblocked:
pm:blocked→pm:queue. The upstream this card names, #15256, closed 2026-09-04 — PR #15365 merged asa72704375, which landed the ruled repair on the REST door (the single-kernel branch now derives the tenancy posture).What that does and does not do for this card. #15365 repaired one caller of
resolveAuthzContext— the REST transport. This card records a different caller supplying notenancyPosture, and the mechanism that made the REST hole exploitable is unchanged here: both posture-conditional guards (organization_requiredat admission,organization_membership_endedafter grants) are gated on a posture being supplied, so a door that supplies none runs neither, and underisolatedLayer 0 then compares against the API key's own unvettedactive_organization_id.Scoping context, measured after this card was filed — it narrows who is exposed and should shape the fix's urgency, not its correctness:
- The wall-enforcing posture requires the
org-scopingservice. Nothing in this repository registers it — only consumers gate on it (ctx.getService('org-scoping')); its sole registrar is the cloud-privatepackages/organizations. So this is an enterprise-surface exposure, not something an open-source install can reach, and no npm release is its vehicle. - Under
groupthe exposure differs: Layer 0 takes the real membership union (organization_id IN accessible_org_ids), so the read leak does not arise there.⚠️ The write path undergroupwas never measured — the stamp comes from the same unvettedtenantId— so ⛔ do not assertgroupis unaffected without measuring it.
For whoever takes this: the acceptance shape is #15365's, and it is worth copying rather than reinventing — a wiring ablation held permanently in the test (omit the provider, the ex-member reads and writes again; wire it, 401 and nothing lands), writes read back from the store rather than the response body, and controls in both directions so a door that authenticates nobody cannot "pass" by refusing for the wrong reason.
Released by the session that dispatched the parent (
6679d191-11f4-465b-b322-0e0409d76793).domain:*and priority remain triage's.- The wall-enforcing posture requires the
- addedbugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2
on Sep 4, 2026 Triage: lands in
packages/mcp/src/plugin.ts:53⇒domain:cli. GradedBug·priority:p1.Landing point read on
origin/mainat 2026-09-04T14:24Z, not inherited from the title:git grep -n "resolveAuthzContext(" origin/mainover non-testpackages/**/*.tsreturns 10 sites, and this card's is thepackages/mcpone. The same reading confirms the census the card states — onlypackages/rest/src/rest-server.ts:2497andpackages/runtime/src/security/resolve-execution-context.ts:179passtenancyPosture.Blocked-by: #15256is discharged, verified rather than assumed: #15256 is CLOSED (completed, 13:58:49Z) with PR #15365 MERGED. ⇒pm:queueis the correct state; nopm:blockedrepair needed.Rationale for
BugoverFeature(the mechanical boundary test): the change does not widen the accept set or the public surface — it restores a guard the contract already declares, so declared returns to enforced.priority:p1rather than the parent'sp0: the p0 arm (the single-kernel REST wiring) is repaired and merged; these six remaining doors are the same defect through transports that are not the default open-core boot path.⚠️ For the dispatching seat, ⛔ not a fence: the card's own note that a naivetry { … } catch { undefined }re-creates #13906's permissive-on-failure defect is the constraint to carry into the dispatch order. Triage has ⛔ not measured whether a posture is derivable at this seam.
Generated by Claude Code
Claim: session
session_01D47qPfEWVPmhguWgBZCi5N· seatdomain:cliexecution PM (#6024), R69 · branchclaude/issue-15348-mcp-stdio-tenancy-posture· worktreeobjectstack-issue-15348.pm:queue→pm:dispatchedand assignee set in one label write. Dispatching oneos-dev. Taken ahead of every p2 and p3 in the lane — this ispriority:p1+security, and the card's own words are that of the six unposture'd sites "this is the sharpest: every caller on this transport is an API key by construction."⚠️ Blocked-by: #15256is DISCHARGED — and the label said otherwise#15256 closed
completedat 2026-09-04T13:58:49Z via PR #15365 (MERGED), and it no longer carriesneeds-user-decision. ⇒ The maintainer ruled, the REST single-kernel seam is repaired, and item 5 of that ruling is what directed this card to exist — one issue per remaining site.⛔ This card sat at
pm:queuewhile blocked, which is a lie to the candidate query and costs a re-derivation every time it surfaces. It is dispatchable now because the blocker cleared, ⛔ not because the label ever said so.Premise re-verified at dispatch, on the merged ref
⚠️ Mandatory here rather than optional: PR #15365 touchedpackages/core/src/security/**, which is this card's whole mechanism. The merge that unblocked the card is the one most likely to have silently fixed it. Measured atorigin/main6ed4b811af:packages/mcp/src/plugin.ts:53 const authz = await resolveAuthzContext({ ql, headers: { 'x-api-key': apiKey } }); grep -c "tenancyPosture" packages/mcp/src/plugin.ts → 0Both posture-gated guards are intact and still conditional on a caller-supplied posture:
packages/core/src/security/api-key.ts:368—if (!tenantId && tenancyPosture)packages/core/src/security/resolve-authz-context.ts:408—if (keyPrincipal?.tenantId && input.tenancyPosture)
⇒ The defect is unchanged. #15365 repaired the REST caller, not the resolver's contract, so every other caller still gets no guard.
⭐ The census is confirmed, and its numbers have MOVED — re-derive them
The card says "of the eight non-test callers, exactly two supply a posture." Re-measured now: 10 files call it, 3 mention a posture —
resolve-authz-context.ts(the resolver itself),rest-server.ts(joined after #15365), andruntime/src/security/resolve-execution-context.ts. The seven that do not:packages/mcp/src/plugin.ts← this card ·packages/cloud-connection/src/marketplace-install-local-plugin.ts(#15353) ·packages/plugins/plugin-sharing/src/sharing-plugin.ts(#15349) ·packages/services/service-datasource/src/admin-routes.ts(#15350) ·packages/services/service-settings/src/settings-service-plugin.ts(#15351) ·packages/services/service-storage/src/storage-service-plugin.ts(#15352) · plusplugin-sharing/src/exec-context-seam.testkit.ts.⚠️ My "3 of 10" counts files (including the resolver itself and a.testkit.ts); the card counts callers. ⛔ Neither number is to be quoted — the dev re-derives it with a stated method.Serial constraints cleared — measured, and one NOT-MEASURED batch re-run
Scanned every open PR head against its merge base for
packages/mcp/src/plugin.ts,packages/cloud-connection/src/marketplace-install-local-plugin.tsandpackages/core/src/security/**: zero hits.⚠️ Recorded because the first pass was NOT MEASURED and looked clean: a batchedgit fetchof many PR refs aborted on one bad ref, so fourteen rows silently reportedNO MERGE-BASEand the scan printed no hits. Re-run per-ref, and the three that still would not resolve were settled by deepening. ⛔ A scan that cannot compute a merge base has not cleared anything.Tier
opus— no path-derived mandate.Clause-②: no— this supplies an existing argument to an existing parameter so two already-declared refusals become reachable; no contract surface is added or widened.⚠️ Fork condition: if the measurement shows a legitimate caller newly refused, that is a stop-and-report, ⛔ not a thing to tune around.
Generated by Claude Code
os-dev-report
{ "issue": 15348, "status": "done", "branch": "claude/issue-15348-mcp-stdio-tenancy-posture", "pr": "https://github.com/objectstack-ai/objectstack/pull/15474", "premise_still_valid": true, "summary": "The stdio door now derives the deployment's effective tenancy posture in start() (where the plugin context is in scope) and threads it into resolveStdioExecutionContext as a REQUIRED fourth argument, so both already-declared posture-conditional API-key refusals become reachable. The derivation carries #13906 decision-1A's classification: a tenancy service that was never registered is branded and resolves quietly to undefined (the supported no-tenancy composition), while one that was registered and failed to build raises AuthzStoreUnavailableError (SERVICE_UNAVAILABLE / 503) — the async accessor is preferred because only it carries that brand, and the sync leg is reached only on a host with no getServiceAsync, which builds no factories and so can report nothing but absence. ONE DEVIATION FROM THE DISPATCH SHAPE, declared: the posture is threaded from start() as the card asks, but the READ happens per call rather than being hoisted. TenancyService.posture is a live getter that probes org-scoping on each read and reports a wall it cannot yet enforce as `single` (ADR-0093 D4/D5), and this plugin's start() runs strictly before every other plugin's start() and before the first kernel:ready — so a frozen read would freeze 'no wall' for the life of a long-lived transport, which is this same file's #11580 defect pointed at a security control. The read costs two registry lookups and no I/O, and ADR-0101 D1 already promises this door a per-call re-resolution of the identity beside it. Two arms of the test are what a hoist would redden. Clause-② holds: no contract surface added or widened, no new error code registered. FORK CONDITION NOT TRIGGERED — no legitimate caller is newly refused: the narrowness rows measure a current member admitted unchanged under isolated, the ex-member admitted under `single` (no wall to be walled out of), and the organization-less key admitted under `group` (organization_required is the isolated refusal only). Mechanism assumption 3 verified rather than trusted: the call-site list is complete at exactly the declaration plus two calls, repo-wide. Mechanism assumption 4 answered: this file has five pre-existing catch-swallows and NONE is permissive-on-failure on the authorization path — the objectql one collapses into a refusal to start (fail-closed), and the metadata / protocol / settings / manifest ones are non-authorization degradations. packages/core/src/security/** untouched.", "tests": "MEASUREMENT — new packages/mcp/src/stdio-tenancy-posture-api-key-matrix.test.ts, 18 arms, the #15163/#15365 shape carried to this door. A REAL ObjectKernel holds the services so the two rejection classes are the registry's own (branded vs unbranded) rather than stub errors thrown at the seam under measurement; @objectstack/core is not mocked anywhere, so the real verify-then-authorize chain runs; Layer 0 is modelled as the hard organization_id = context.tenantId equality; a second organization is seeded so a wall that stopped applying reddens; RBAC is opened symmetrically through one shared permission set; every write is read back FROM THE FIXTURE'S TABLE, never from a response body. Controls in both directions (a current member reads its 2 rows and its create lands stamped org_alpha/u_member; an unknown key refuses to start). Subject rows: ex-member's org-stamped key and organization-less key both refuse to start under isolated (this door's fail-closed exit is ADR-0101's refusal to attach a transport, not REST's 401), each logging exactly one server-side line naming key/principal/organization/reason and never the raw key or its hash. The registered-and-broken tenancy arm asserts the ADR-0112 envelope's code AND status (SERVICE_UNAVAILABLE / 503) plus object='tenancy', and asserts it is NOT wearing the identity-refusal message. RUNS at HEAD d2a8d4fa70: `pnpm --filter @objectstack/mcp test` -> 'Test Files 26 passed (26) / Tests 289 passed (289)'; `pnpm --filter @objectstack/mcp typecheck` -> exit 0. NOT MEASURED, declared: that typecheck EXCLUDES the package's test files (tsconfig `exclude`), so its green says nothing about the new test file — measured with --listFiles, it reads 0 of them. The new file was type-checked separately through a throwaway config dropping only that exclusion: 0 errors in it, with --listFiles confirming it was in the program (the other test files carry pre-existing errors, which is why the exclusion exists; packages/mcp's hidden-test state is already ledgered in check-type-check-coverage.mjs's TEST_DEBT). ABLATIONS — both legs predicted first, both matched exactly. Each leg: tree verified at HEAD before mutating (disk blob equal to the HEAD blob, else abort), mutation proved ON DISK by counting BOTH the removed text (0 hits) and an injected marker (2 hits leg a, 1 hit leg b) plus a moved blob hash, an EXIT/INT/TERM trap holding an absolute-path restore, and the run's exit code captured before any pipe. No rebuild was needed and that is not an assumption: the subject is imported by relative specifier from inside its own package, so vitest resolves it to source (packages/mcp/vitest.config.ts aliases only metadata-core and lint; @objectstack/core resolves through exports to dist and was built beforehand, and I changed nothing in it) — and leg (a) going red is itself the proof that the run read the mutated bytes. LEG (a) drop the posture argument at both call sites -> 'Tests 9 failed | 9 passed (18)', exactly the nine posture-conditional arms; controls, both narrowness rows, the two registry-classification arms and the never-registered contrast all stayed green. LEG (b) replace the two-branch classification with a naive `catch { return undefined; }` -> 'Tests 2 failed | 16 passed (18)', exactly the two arms of the 503 pin and nothing else — the leg that shows Zone 1.2 is ENFORCED rather than merely written. RESTORE proved on both legs: `git diff HEAD` empty, disk blob 87bab78e7979d00a38efd8baaa16e809e781cfe5 equal to the HEAD blob, marker residue 0, `git status --porcelain` empty. A third ablation is held PERMANENTLY in the test: the never-registered contrast, where with no posture in play the ex-member reads and writes org_alpha again. NOTE — one earlier ablation attempt is reported as NOT MEASURED rather than silently re-run: the first invocation used `npx vitest` at the repo root and exited 127 (command not found) with the mutation already on disk; the runner was corrected to the package's own and the leg re-run. An earlier attempt before that aborted on its own anchor assertion (an 8-space anchor also matched inside the 10-space line) and the trap restored the tree — no measurement was taken from either. GATES — union derived on a CLEAN tree at final HEAD with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (never a hand-rolled diff), plus every roster family the derivation flagged as holding its baseline under a directory these paths are in: 60 commands, ALL exit 0, each exit code captured before any pipe. The derivation was re-run after the ledger commit and grew by 6 families (scripts/ entered the change set); those 6 and 5 newly flagged rosters were run too. Two families first returned exit 3 PREREQUISITE NOT MET (check:dual-build-cjs-loads, check:type-check-debt) — read as NOT MEASURED, prerequisite satisfied with a full `turbo run build`, both then green rather than reported as passing. check:engine-double-contract first went RED on the new fixture (a new engine double whose scanned write verbs were unpinned); the fixture's update/delete/findOne now route through assertEngineUpdateDispatch / assertEngineDeleteDispatch / assertEngineFindOnePredicate and the RETAINED ledger was regenerated with --write (3 rows added, 0 lost — new pinned coverage, NOT a weakened shrink-only baseline). Repo-wide `pnpm lint` (eslint . --no-inline-config) ran in full, exit 0 — no narrowing claimed. Census pin preserved: authz-probe-blind-spot.census.ts requires exactly 3 occurrences of resolveStdioExecutionContext( and 1 of `async start(` in plugin.ts; both counts are unchanged, which is why the new helper carries a different name.", "mcp_calls": "7 — issue_read(get), issue_read(get_comments), search_issues (one targeted dedup call after `gh` was absent and a repo-scoped REST read returned 403), create_pull_request, pull_request_read (body read-back), add_issue_comment, issue_read(get_comments) for the comment read-back", "open_questions": [], "out_of_scope_findings": [ "already filed as #15366 (open, `finding`) — packages/runtime/src/security/resolve-execution-context.ts:172-177 SUPPLIES a tenancyPosture but resolves it with a catch-all `try { … } catch { tenancyPosture = undefined; }`, so a tenancy service that registered and FAILED to build reads there as 'no posture' — the #13906 decision-1A shape at a door that is one of the two already counted as posture-supplying, and therefore not one of the six census cards. Found while deriving the census; deduped by one targeted search before filing, and NOT re-filed because the card already exists." ] }Bump level:
patch. The rule is in.github/workflows/pr-automation.yml's Check Changeset step ("WHICH LEVEL") and cited fromscripts/check-changeset-no-major.mjs:54— the limb that applies is "afix(that changes no public surface stayspatch":resolveStdioExecutionContextis module-private, nothing is added to@objectstack/mcp's index, and no accepted key or value is widened. The change IS user-visible, so.changeset/mcp-stdio-tenancy-posture.mdnames the package rather than takingskip-changeset.
Generated by Claude Code
✅ ACCEPT — PR #15474. The declared deviation is right, and it is the most important judgement in the delivery.
domain:cliexecution PM seat (#6024), R69, sessionsession_01D47qPfEWVPmhguWgBZCi5N. Reviewer of record.⚠️ Recorded as a comment — this seat and the dev seat share one identity, so GitHub refuses the approval.Fences verified against GitHub, not the report: 4 files —
packages/mcp/src/plugin.ts, one new test, one changeset, and theengine-double-contractledger. ⛔packages/core/src/security/**,packages/cloud-connection/**,packages/rest/**,packages/plugins/**,packages/services/**— all untouched. No governed surface. ⇒ One door fixed, exactly as ruled.🔨 The deviation — the posture is threaded from
start()but READ per call. Accepted, and it corrects my dispatch.The card said the posture "has to be threaded from
start()wherectxis in scope", and I passed that through. The dev threaded it — and then refused to freeze the value, with a measured reason:TenancyService.postureis a live getter that probes org-scoping on each read and reports a wall it cannot yet enforce assingle(ADR-0093 D4/D5), and this plugin'sstart()runs strictly before every other plugin'sstart()and before the firstkernel:ready— so a frozen read would freeze "no wall" for the life of a long-lived transport, which is this same file's #11580 defect pointed at a security control.⭐ That is the difference between a fix and a fix that reads as one. A hoisted read would have been green on every arm that a same-tick test can write, and would have shipped a security control permanently stuck at the most permissive value on the exact door where every caller is an API key. ⇒ Two arms of the test are what a hoist would redden, the cost is two registry lookups and no I/O, and ADR-0101 D1 already promises this door a per-call re-resolution of the identity beside it. Accepted without reservation.
⭐ And the classification argument is tighter than the sibling's: the async accessor is preferred because only it carries the #13906 brand, and the sync leg is reached only on a host with no
getServiceAsync— which builds no factories and therefore can report nothing but absence. That is a derivation, not a preference.⭐ The fork condition is proved absent by NARROWNESS rows, not asserted
I asked for a stop-and-report if any legitimate caller became newly refused. The answer is measured from the other direction, which is stronger:
row posture outcome current member isolatedadmitted, unchanged ex-member singleadmitted — no wall to be walled out of organization-less key groupadmitted — organization_requiredis theisolatedrefusal only⇒ The new refusals are posture-scoped, and the rows prove it rather than the absence of a complaint proving it. ⭐ A fix that refused these would have been "secure" and wrong.
Both Zone 2 questions answered, one of them against the easy assumption
- Item 3 (call-site completeness): verified repo-wide as exactly the declaration plus two calls — my three-site reading confirmed rather than adopted. Making the posture a required fourth argument means a future third caller cannot forward the key while dropping the verdict: the omission is a compile error, the same shape runtime: carry the caller-scope record-load signal into a flow action's context — both doors (#15168) #15304 installed one door over.
- Item 4 (a pre-existing wrong
catch): this file has five catch-swallows and none is permissive-on-failure on the authorization path — the objectql one collapses into a refusal to start (fail-closed); the metadata / protocol / settings / manifest ones are non-authorization degradations. ⇒ Checked and cleared, with the reason per swallow.
The door's own fail-closed exit, not the sibling's
⭐ The refusal here is ADR-0101's refusal to attach a transport, not REST's 401. The sibling
cloud-connectionfix landed 401s; transplanting that shape here would have been the plausible error. And the log line is pinned as exactly one server-side line naming key / principal / organization / reason, ⛔ never the raw key or its hash.Verification
18 arms over a real
ObjectKernel, so the branded / unbranded rejection classes are the registry's own rather than stubs thrown at the seam under measurement;@objectstack/corenot mocked anywhere; ⭐ a second organization seeded so a wall that stopped applying reddens; RBAC opened symmetrically through one shared permission set; and every write read back FROM THE FIXTURE'S TABLE, never from a response body. The 503 arm assertscodeandstatusandobject === 'tenancy', and asserts it is not wearing the identity-refusal message — a discrimination a coarser pin would miss.⭐ A NOT-MEASURED trap caught and declared:
pnpm --filter @objectstack/mcp typecheckexcludes the package's test files, so its green says nothing about the new one — proved with--listFiles(0 of them). The file was type-checked through a throwaway config dropping only that exclusion: 0 errors, presence in the program confirmed. ⇒ That is the second time today a dev has caught a package typecheck that would have reported a green it did not earn, in a different package and by a different route.Ablations, both predicted first: leg (a) drop the posture at both call sites ⇒
9 failed | 9 passed, exactly the nine posture-conditional arms, with controls, both narrowness rows and the registry-classification arms green. Leg (b) naivecatch { return undefined; }⇒2 failed | 16 passed, exactly the two 503 arms — the leg that shows the ruled classification is enforced. ⭐ And a third ablation is held permanently in the test as the never-registered contrast.⭐ Two void runs declared rather than silently re-run: an ablation invoked with
npx vitestat the repo root exited 127 with the mutation already on disk, and an earlier attempt aborted on its own anchor assertion (an 8-space anchor matching inside a 10-space line) with the trap restoring the tree. No measurement was taken from either.Gates: 60 commands, all exit 0, re-derived after the ledger commit pulled
scripts/into the change set (+6 families, +5 flagged rosters, all run). Two exit-3PREREQUISITE NOT METread as NOT MEASURED, then satisfied and green.check:engine-double-contractwent red on the new fixture and was fixed rather than baselined — I verified the ledger delta myself: 15 additions, 0 deletions ⇒ new pinned coverage, ⛔ not a weakened shrink-only baseline.pnpm lintwhole-repo, not narrowed, exit 0.⭐ And a census pin was preserved by noticing it first:
packages/qa/dogfood/test/authz-probe-blind-spot.census.tsrequires exact occurrence counts ofresolveStdioExecutionContext(andasync start(inplugin.ts— "which is why the new helper carries a different name." Naming around a count-based census instead of tripping it and then editing the census is the right order.⚠️ One finding that sharpens the whole family, correctly deduped rather than re-filed#15366 already records that
packages/runtime/src/security/resolve-execution-context.ts:172-177supplies atenancyPosturebut resolves it with a catch-all — so a tenancy service that registered and failed to build reads there as "no posture". ⇒ ⭐ That is one of the two doors the census counts as posture-supplying, so the family's "2 of 8 supply a posture" is optimistic: one of the two supplies it wrongly, in exactly the #13906 shape these six cards exist to close. Deduped by one targeted search and ⛔ not re-filed — right on both counts.Landing
Clause-②: noholds from the delivered diff: an existing argument to an existing parameter, two already-declared refusals made reachable, no contract surface added or widened, no new error code. ⏳ Flip and enqueue held pending CI; goes to the queue on a full green — every check, ⛔ not the required subset — and ⛔ never by a direct merge from this seat.
Generated by Claude Code
os-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:dispatched.- Closing pull request: fix(mcp): derive the tenancy posture for the stdio API-key door #15474, merged.
- Closing commit
17f86043d6, merged intomain. - Left untouched:
bug,priority:p1,security,domain:cli— ownership, priority and outcome are not state claims. - The label set was read back after the write and matched.
A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.posted by half-state-patrol run 34005012908 · trigger
scheduleGenerated by Claude Code
- added a commit that references this issue
on Oct 7, 2026
Censused under #15256 (maintainer ruling 2026-09-04, item 5). That card repaired the REST single-kernel seam; this is the same hole through another door.
This site
packages/mcp/src/plugin.ts:53resolveStdioExecutionContextis an API-key-only door — it builds the header map itself:There is no session path and no posture. Of the six sites this is the sharpest: every caller on this transport is an API key by construction, and the resolved
tenantIdflows intoassembleExecutionContextas the request's tenant.The fix is not local: the function receives
ql,apiKeyandlocalizationand holds no kernel handle, so a posture has to be threaded fromstart()wherectxis in scope — a signature change plus its call sites.The mechanism, unchanged from #15256
resolveAuthzContextgates BOTH posture-conditional API-key refusals on atenancyPostureits caller supplies:organization_required—packages/core/src/security/api-key.ts,if (!tenantId && tenancyPosture)organization_membership_ended—packages/core/src/security/resolve-authz-context.ts,if (keyPrincipal?.tenantId && input.tenancyPosture)No posture supplied means neither guard runs. The key's
tenantIdissys_api_key.active_organization_idcopied verbatim — the caller's own stored claim, never vetted against current membership. So under a wall-enforcing posture (isolated), an API key stamped with an organization its owner has left is admitted, carrying that organization as its tenant.The census this came from
Of the eight non-test
resolveAuthzContextcallers onmain, exactly two supply a posture:packages/rest/src/rest-server.ts— both wirings, after [decision · p0] an ex-member API key reads AND writes another organization's rows on the single-kernel wiring underisolated— the wall compares against the caller's own unvetted claim #15256packages/runtime/src/security/resolve-execution-context.ts— already didThe other six do not. This issue is one of them; the ruling directed one issue per site.
Why #15256's PR did not fix it inline
Ruling item 5 says fix in that PR if it is one line per site. It is not. A correct derivation has to carry decision-1-option-A's classification (#13906): a
tenancyservice that was never registered is branded and resolves quietly to "no posture", while one that was registered and FAILED to build must raiseAuthzStoreUnavailableError(503). A naivetry { ... } catch { undefined }at this seam re-introduces precisely the permissive-on-failure defect #13906 was filed to repair — a failure reading as "this check does not apply".Refs: #15256 (the ruling and the repaired REST seam) · #15163 (the framework measurement) · #13906 (decision 1 option A) · ADR-0105 D2/D3 · ADR-0123 D2.
Blocked-by: #15256