Uh oh!
There was an error while loading. Please reload this page.
fix(rest,core): the single-kernel wiring derives the tenancy posture — an ex-member's org-stamped API key no longer reads or writes another organization's rows - #15365
Conversation
…tate the corrected comment as prose The dead dispatch left the 1A/2A/3A implementation committed and its pins UNCOMMITTED, so a read of the pushed branch found them absent. This lands them and repairs the one measured contradiction between the two halves. `rest-server.ts` had "corrected" its stale comment by quoting the withdrawn B' claim verbatim under a SUPERSEDED banner. That kept the false sentence answering every grep for it — it had already caused this seam to be re-read as unrepaired after the fix — and it directly contradicted the pin that forbids the phrase (1 failed / 41 passed before this change). The correction is now stated as prose, and the forbidden phrase occurs exactly once in the repository: inside the regex that forbids it. Also narrows that pin's sibling assertion off the words "boot refusal", which are how both files' own no-boot-refusal notes are worded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…or the single-kernel posture seam Sized as the maintainer ruled (item 6): a fail-closed bug fix, no accept-set change, no new public surface, Clause-2 no. Names the observable change — an organization-less or ex-member API key on a walled single-kernel deployment now answers 401 where it answered 200. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ngle-kernel-posture-provider
… after the rest-server insertions Pure line rot, produced by `node scripts/check-system-context-census.mjs --fix`: this card's additions to `rest-server.ts` shifted the cited lines by +24 before the posture block and +79 after it. No prose and no row semantics change — only the line numbers the page cites. check-system-context-census: OK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 30 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 30 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 11f772946146d33c668bc4fad0104fe695b81d17 && git checkout 11f772946146d33c668bc4fad0104fe695b81d17
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dc46c4ec133eb946c50805a851e4f6218058f366 4cfedfe01e2fac3aff9525eacf54be1b24cb6624 && git checkout -B drift-repro dc46c4ec133eb946c50805a851e4f6218058f366 && git merge --no-ff 4cfedfe01e2fac3aff9525eacf54be1b24cb6624
node scripts/docs-audit/affected-docs.mjs --json dc46c4ec133eb946c50805a851e4f6218058f366
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#15256
Implements the maintainer's 2026-09-04 ruling (1A · 2A · 3A, decision batch #33) on the seam that let an ex-member's organization-stamped API key read and write another organization's rows on the wiring the open core actually builds.
⭐ This is a takeover of a dispatch that died mid-run. The implementation was committed on the branch and its pins were left uncommitted, which is why a read of the pushed branch found them absent. Every surviving line was re-read against the ruling and re-driven here; one measured contradiction between the two halves is repaired below.
1A — the single-kernel branch derives the posture
rest-api-plugin.tswires atenancyServiceProviderto the lone local kernel'stenancyservice, in the same shape as theauthServiceProviderbeside it, withobjectQLProvider's classification — because decision 1 option A governs what a fault at this seam means:undefined, no posture-conditional refusal (the supported no-tenancy composition)computeExecCtxanswersAuthzStoreUnavailableError(503)⛔ B′ (a boot refusal) stays withdrawn; none was added in any form.
The stale comment, and why it is paraphrased rather than quoted
rest-server.tsdocumented this seam as covered by a startup refusal inrest-api-plugin.tsthat was withdrawn and that the plugin never carried — a p0 seam documenting itself as covered when nothing covered it.The dead dispatch had "corrected" it by quoting the false sentence verbatim under a SUPERSEDED banner. That kept it answering every grep for the withdrawn remedy — it had already caused this seam to be re-read as unrepaired after the fix — and it contradicted the pin that forbids the phrase. Measured:
1 failed | 41 passedbefore the repair, the single failure being that pin.The correction is now prose, and the forbidden phrase occurs exactly once in the repository — inside the regex that forbids it:
2A — the refusal is said out loud, server-side; the wire is unchanged
One
warnper refusal, at the point each is decided —organization_requiredat admission,organization_membership_endedafter grants — naming thesys_api_keyrow id, the principal, the organization and the reason. ⛔ Never the raw key, never its at-rest hash. ⛔ Not duplicated insideapi-key.ts's admission path.Real output from the run:
The response body is unchanged, and that is asserted rather than asserted-about:
ResolvedAuthzContext.authRefusalis cross-referenced only — #14273's A1 ruling removes it in its own PR. ⛔ Not removed here.3A — the admit-on-unknown default stays; only its justification changes
api-key.tskeeps admitting an organization-less key when no posture is supplied. No behaviour change. The docblock's false premise (a "transport not yet wired" residue — in fact every deployment the open core builds) is replaced by the one legitimate case: a host that registers notenancyservice. Atenancyservice that was registered and failed to build is explicitly named as not in that set.The measurement, and the ablation
REST-level, on the single-kernel wiring under a live
isolatedposture, Layer 0 modelled as the hard equalitytenant-layer.tscomputes. Writes are read back from the store, never from the response body, and a second organization is seeded so "the wall is live" is a control rather than an assumption.org_alphaorg_alphaAblation — remove the provider, the leak returns
The only variable is the provider this card wired; the tenancy service, engine, keys, routes and store are byte-identical to the subject arm. A pin that cannot go red has measured nothing.
before (provider absent) — GET 200 total 2, POST 201 with the row landing
organization_id: org_alpha, created_by: u_exmember, organization-less key back to 200 total 0, and no refusal line at all because no refusal was decided.after (provider wired) — GET 401, POST 401, nothing in the store, one
warnper refusal.The member control is asserted unchanged by the ablation, so what moved is the provider and not the fixture.
setup({ omitTenancyProvider: true })omits exactly the one argument this card added. There is no mutated tree to restore, so no restore leg can silently fail to run.Pin (a) — every branch, not just the one that leaked
The mechanism was "one branch was never asked", so the pin is over the branch set: all three
computeExecCtxwirings (scoped kernelManager, default-environment kernelManager, single-kernel providers) are driven, each required to have consulted a registered tenancy service and to have fired the refusal — with an anti-vacuity control first (a healthy member must get 200 on that same branch, so a wiring that authenticated nobody cannot "pass" by 401-ing for the wrong reason). A fourthauthServiceassignment reds the completeness assertion.Census — ruling item 5
Eight non-test
resolveAuthzContextcallers. Two supply a posture:rest-server.ts(both wirings, after this PR) andruntime/src/security/resolve-execution-context.ts(already did). The other six do not, and all six take real request headers, so all six acceptx-api-key.Not one line per site: a correct derivation must carry decision-1-option-A's classification, and a naive
try { … } catch { undefined }re-introduces exactly the permissive-on-failure defect #13906 repaired. So, per the ruling, filed per site withBlocked-by: #15256— #15348 (mcp stdio, an API-key-only door), #15349 (plugin-sharing), #15350 (service-datasource admin routes), #15351 (service-settings, which returns the unvettedtenantIdonward), #15352 (service-storage), #15353 (cloud-connection marketplace-install). None is addressed here.Verification
Gate union re-derived from the real diff and run on the final commit
4cfedfe01:The family grew from 45 to 73 after the docs re-anchor below entered the diff — the derivation was re-run against the actual change set rather than the first answer.
pnpm --filter @objectstack/rest test— 177 files / 3011 tests passedpnpm --filter @objectstack/core test— 49 files / 1189 tests passedtypecheckboth packages green;check:test-typecheckcompiles the test layer separately (rest: 0 files / 0 errors in debt), so the new test files are genuinely covered rather than excludedkeyId/userId/organizationId), so there is no narrowing to reverse-verify; the ablation is the reverse verification this card owes.check:dual-build-cjs-loadsandcheck:type-check-debtfirst answered exit 3, PREREQUISITE NOT MET on an unbuilt worktree — read as NOT MEASURED, not as a pass and not as a failure. Both re-ran green afterturbo run buildacross the workspace.content/docs/permissions/system-context.mdxis a mechanical re-anchor (check-system-context-census.mjs --fix): this card's insertions shifted the citedrest-server.tslines by +24 before the posture block and +79 after it. Line numbers only — no prose, no row semantics.flock, so the shared verify lock was never taken and nothing was serialized against sibling work in the container. Everyos-verify-lock.shVERDICT line above says so.Scope
⛔ Not in this PR, per the ruling: cloud's ARM C measurement; #14273's field removal;
os serve --devhost-config paths beyond the provider. The changeset is sized as ruled —@objectstack/restpatch +@objectstack/corepatch, Clause-② no — and was not re-priced upward on account of 17.3.0 having shipped with the leak. The release vehicle is the maintainer's call, in chat, and is not decided here.Generated by Claude Code