fix(service-settings): supply the effective tenancy posture at the manifest gate - #16015
fix(service-settings): supply the effective tenancy posture at the manifest gate#16015claude[bot] wants to merge 3 commits into
Conversation
…nifest gate `SettingsServicePlugin`'s `verifiedContextFromRequest` called `resolveAuthzContext` with no `tenancyPosture`, so both posture-conditional API-key refusals stayed unreachable at the settings doors: under a wall-enforcing posture an ex-member's org-stamped key and an organization-less key were both admitted. This seam also returns `authz.tenantId` onward as the resolved settings tenant, so the key's own unvetted `active_organization_id` became the verdict the read/write path acted on. The posture is derived here through the kernel's async registry, carrying decision-1-option-A's classification: a `tenancy` service that was never registered resolves quietly to "no posture", one that was registered and failed to build raises `AuthzStoreUnavailableError`. The requested posture (`OS_TENANCY_POSTURE`) is deliberately not reused — under ADR-0093 D4/D5 it would refuse legitimate automation over a wall that is not in force. Deliberately not extracted into a shared helper: sibling repairs are in flight on this same seam, and this copies the precedent set by `@objectstack/cloud-connection`'s install-local door. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…on its void return Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…'s find `check:objectql-double-limit` graded the new fixture driver twice: first limit-blind, then touching rows outside the bound because the copy ran before the slice. The bound is now applied after the filter, by presence, and before any row-touching stage; `count` no longer derives a population size from a page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 7 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 cae0aae7b2612a3f856a2b1c2bc326a332580bc8 && git checkout cae0aae7b2612a3f856a2b1c2bc326a332580bc8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f7db8f4fd268a86a08c62ae4894cf7417720f8c9 d49ec41f7ffdbc321ae5ada98ad7f5e5ea024a03 && git checkout -B drift-repro f7db8f4fd268a86a08c62ae4894cf7417720f8c9 && git merge --no-ff d49ec41f7ffdbc321ae5ada98ad7f5e5ea024a03
node scripts/docs-audit/affected-docs.mjs --json f7db8f4fd268a86a08c62ae4894cf7417720f8c9
|
Docs Drift Check — verdict on the one page it named
Why it was named: the detector attributes a page to a top-level exported symbol, and What the page actually claims about that symbol. One mention, line 180, and it names the class only as the registrar of the That is the in-process, trusted path. This PR touches only The The one admission passage on the page is about a different seam. Lines 293-307 describe Measured, with a positive control on the same command and scope. Over that one file:
No page under Generated by Claude Code |
|
Standing down on the red
Mechanism (measured; my first summary of it was wrong and is corrected on #15978/#15998): the launcher does not fail to resolve. Not tree-dependent, and the cleanest proof is on Severity bound: false-RED only, never false-green — a poisoned store makes every artifact look Fix: PR #16002 (from the ⛔ No test skipped, disabled or quarantined; ⛔ no empty commit. Spending this PR's one permitted re-run, unspent. Generated by Claude Code |
|
Update, one minute later: ⛔ the re-run is NOT being spent. The fix has landed, so the remedy changed.
The comment above said this PR's one permitted re-run was being spent. It is not, and the reason is worth stating rather than quietly skipping:
So this PR's re-run stays unspent, and the next action here is a Generated by Claude Code |
Fixes #15351
SettingsServicePlugin'sverifiedContextFromRequest— the manifest gate behind every/api/settingsdoor — calledresolveAuthzContextwith notenancyPosture. Both posture-conditional API-key refusals are gated on a posture the caller supplies (organization_requiredinpackages/core/src/security/api-key.ts,organization_membership_endedinpackages/core/src/security/resolve-authz-context.ts), so neither ran here. An API key'stenantIdissys_api_key.active_organization_idcopied verbatim — the caller's own stored claim, never vetted against current membership — so under a wall-enforcing posture a key stamped with an organization its owner had left was admitted, and this seam then handed that organization onward asSettingsContext.tenantId, the resolved tenant the settings read/write path acts on.The posture is now derived at this door through the kernel's async registry, carrying decision-1-option-A's classification (#13906): a
tenancyservice that was never registered resolves quietly toundefined, one that was registered and failed to build raisesAuthzStoreUnavailableError. The requested posture (OS_TENANCY_POSTURE, viaresolveTenancyPosture()) is deliberately not reused — under ADR-0093 D4/D5 a deployment that asks forisolatedwithout the enterprise organizations runtime issinglein force, and the requested value would refuse legitimate automation over a wall that is not there.Measured, at
d49ec41f7Driven through the real
SettingsServicePlugin(init→start→kernel:ready), a realObjectQLengine over a memory driver, the real routes, and atenancyservice on a REALObjectKernelso the never-registered / registered-and-broken split comes from the registry's own rejections rather than a hand-branded stub.sys_setting/sys_setting_auditrows are real and are read back out of the driver's store, never off a response body.PUT /api/settings/:ns, ex-member's org-stamped keyGET /api/settings/:ns, same keyGET /api/settings(the listing)The wiring ablation, held permanently in the test
Three
mount()shapes differ in nothing but whether this seam can resolve a posture.no-async-registryandunregisteredre-admit the very caller theisolatedcase refuses, so the refusal is attributable to the posture supply and to nothing else; the CURRENT-member control proves the door still authenticates somebody. Both re-admitting shapes are ruled behaviours, not regressions: a host with notenancyservice has no wall, and aKernelBase/LiteKernelhost has no async registry at all (#15997, already filed — untouched here).The returned
tenantId, the half of the card that is not admissionA browser session whose stored
activeOrganizationIdis no longer backed by asys_memberrow now has that claim dropped here (#15409's ruled option B) instead of returned as the settings tenant. Pinned with its own ablation (no posture resolvable ⇒ the staleorg_Acomes back) and a current-member control in both the session and the API-key direction.Red-then-green, twice
Both ablations ran in a single shell with the mutation proven on disk (HEAD-blob hash comparison plus injected/removed marker counts) BEFORE any measurement, and the restore proven after (
git diff HEADempty, restored hash equal to the HEAD blob). Nodist/is involved: the suite imports the seam through a relative source specifier, so there is no built artifact to go stale.catch { undefined }([finding] two morecomputeExecCtxseams read "failed" and "not wired" as one value, and both feed authorization inputs — tenancy posture and the ADR-0069 auth gate #13906's permissive-on-failure defect): 3 failed / 15 passed — exactly the three ADR-0112 outage cases.Commands
pnpm --filter '@objectstack/service-settings^...' build— green (the closure, built before anything was judged).pnpm --filter @objectstack/service-settings typecheck— green. It really does cover the new test file: an earlier run failed withsettings-admission-tenancy-posture.test.ts(395,87): error TS2339before that line was fixed.pnpm --filter @objectstack/service-settings test— 31 files, 551 passed.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, 54 families) re-run atd49ec41f7: 52 green.check:objectql-double-limitfound the new fixture driver twice — limit-blind, then touching rows outside the bound — and both were repaired rather than baselined; it is green now.NOT MEASURED, declared
check:dual-build-cjs-loadsandcheck:type-check-debtboth exit 3 — PREREQUISITE NOT MET: they read built output and 54 packages have nodist/in this worktree. That is not a pass and not a failure; CI runs both after its own workspace build. Neither can plausibly move on this diff — nopackage.json, noexports, no ledgered-package boundary is touched, andcheck:type-check-coverage(which counts hidden tests) is green.Contract tier
Clause-②: no— re-derived from this diff, not inherited from the claim.api-surface/artifacts exist only forpackages/spec, so the published surface of this service package is itsfiles[]+types, i.e.dist/**. Measured by buildingdist/index.d.tson this branch and again with the one edited source file swapped back tof7db8f4fd, then diffing. Ignoring doc comments, the delta is exactly one line:Name-only, type-erased, not callable from outside.
SettingsServicePluginalready ships four private members and this file's shipped classes carry over forty; the byte-identical line already ships in a published sibling,@objectstack/cloud-connection(MarketplaceInstallLocalPluginis exported from its index and declares the same private member). No exported type, signature or accept surface is added or widened. Behaviourally the accept set narrows — a declared guard returns to enforced — which is the card'sBuggrading, not a contract widening.Deliberately not done
tenancyPosturetoresolveAuthzContext— an ex-member's org-stamped API key is admitted #15350 is in flight on this same seam and service-storage: the file-read authorizer supplies notenancyPosturetoresolveAuthzContext— an ex-member's org-stamped API key authenticates for downloads #15352 is queued behind it; the landed sibling fix(plugin-sharing): the share-link admission seam derives the tenancy posture — an ex-member's org-stamped API key no longer mints links into the organization it left #15996 wrote a private copy for the same reason. On the merits I do think a sharedresolveAdmissionTenancyPosture(ctx)in@objectstack/coreis the right end state — five doors now carry structurally identical copies — but that is a card of its own, not a rider on this one.packages/services/service-settings/src/settings-routes.tshas noisAuthzStoreUnavailableErrorarm, so the branded 503 this seam raises is flattened to500 INTERNAL_ERRORat every settings door. That flattening is pre-existing — [finding] a permission-store read failure resolves as an AUTHENTICATED caller holding ZERO capabilities — the package door answers 403 FORBIDDEN, byte-identical to a genuine capability denial #13279's permission-store re-raise already reached the sameelsebranch — and repairing it is a different defect from supplying the posture. It is pinned as MEASURED (not endorsed) in the test and filed separately.content/docs/releases/**untouched, and noFixes/Part ofkeyword is attached to any other card number in this body.Scope
Enterprise-surface: the wall-enforcing posture needs the
org-scopingservice, and nothing in this repository registers it (sole registrar is cloud-privatepackages/organizations). That shapes urgency, not correctness. The card body's census ("exactly two callers supply a posture") was already stale when it was written — measured on this tree it is four:packages/rest/src/rest-server.ts,packages/runtime/src/security/resolve-execution-context.ts,packages/mcp/src/plugin.tsandpackages/cloud-connection/src/marketplace-install-local-plugin.ts. With this PR it is five of the eight non-test callers; the three left arepackages/plugins/plugin-sharing/src/sharing-plugin.ts:847,packages/services/service-datasource/src/admin-routes.ts:387andpackages/services/service-storage/src/storage-service-plugin.ts:841.Generated by Claude Code