Repository navigation
fix(core)!: the localization sys_setting fallback reads the caller organization only (ADR-0131 D8) - #25301
Conversation
…s organization (ADR-0131 D8) Claude-Session: https://claude.ai/code/session_01DydJpxUXfktYEyRLvwbAEF Co-authored-by: Claude <noreply@anthropic.com>
… fixture rows with the caller's organization Claude-Session: https://claude.ai/code/session_01DydJpxUXfktYEyRLvwbAEF Co-authored-by: Claude <noreply@anthropic.com>
…rrowing Claude-Session: https://claude.ai/code/session_01DydJpxUXfktYEyRLvwbAEF Co-authored-by: Claude <noreply@anthropic.com>
…he caller's organization Claude-Session: https://claude.ai/code/session_01DydJpxUXfktYEyRLvwbAEF Co-authored-by: Claude <noreply@anthropic.com>
… column Claude-Session: https://claude.ai/code/session_01DydJpxUXfktYEyRLvwbAEF Co-authored-by: Claude <noreply@anthropic.com>
…e limit gate Claude-Session: https://claude.ai/code/session_01DydJpxUXfktYEyRLvwbAEF Co-authored-by: Claude <noreply@anthropic.com>
… not implement Claude-Session: https://claude.ai/code/session_01DydJpxUXfktYEyRLvwbAEF Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 28 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 d3fc5b71d82c6bb0e1610c089fdf1e989f9ce316 && git checkout d3fc5b71d82c6bb0e1610c089fdf1e989f9ce316
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 09e07c259a633eb02fdb29bedfe911e808529f88 8e9e7d48fc79bf28989970391209dd0299045d6a && git checkout -B drift-repro 09e07c259a633eb02fdb29bedfe911e808529f88 && git merge --no-ff 8e9e7d48fc79bf28989970391209dd0299045d6a
node scripts/docs-audit/affected-docs.mjs --json 09e07c259a633eb02fdb29bedfe911e808529f88 |
…g to one organization Claude-Session: https://claude.ai/code/session_01DydJpxUXfktYEyRLvwbAEF Co-authored-by: Claude <noreply@anthropic.com>
…calization-fallback-org-scope
Contract reviewServed-tier: Inputs read: card #25248 body, its claim ( ① Derived judgments1. Which
2. No cross-organization read remains. Before (merge-base :2537-2545 as #25226 read it): 3. The posture input. The fallback reads the REQUESTED posture ( 4. No legitimate read is lost.
5. Callers. All four production callers hand a vetted organization: 6. The failure memo. A failed derivation does not populate the memo (:2502-2506): the memo exists for the DIRECT 7. Scope, not retire — sound. Retiring would drop what #10221's memo is keyed on and the bind-window answer (#11580's re-resolve replaces a value, not a built-in), and would leave 8. Tests edited beside the pins. The fixture rows gain the caller's organization and nothing else: 9. Gate verdicts on the head (43 runs): 13 ② Semver levelChangeset
③ Boundary flags
Implemented-by: VERDICT: FAIL |
…ept set Claude-Session: https://claude.ai/code/session_01DydJpxUXfktYEyRLvwbAEF Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs read: everything the first record on this PR read ( The delta. A correction to the first record's citations, not its judgments. Record ① Derived judgmentsCarried forward from 1. Which 2. No cross-organization read remains — right. The merge-base read had no organization and took the first row per key under 3. The posture input — right, with a recorded residual. The fallback reads the REQUESTED posture ( 4. No legitimate read is lost — right. Lean kept (1); a lean composition that nonetheless requests a wall reads nothing, and the engine's write side refuses in the same order (:115-122), so no such row exists to lose. A fresh 5. Callers — right. 6. The failure memo — right. A failed derivation does not populate it (:2502-2506); 7. Scope, not retire — sound. Retiring would drop #10221's memo subject, the bind-window answer #11580's re-resolve replaces, and the organization-less readers in 8. Tests — right. Fixture rows gain the caller's organization and nothing else; 9. Gate verdicts on this head (24 runs staged at 23:12Z, two minutes after the push): 9 ② Semver levelJudged afresh on both carriers as they now stand. Changeset
③ Boundary flagsItems 1-3 and 6-7 are carried forward from
Implemented-by: VERDICT: PASS |
|
Seat ACCEPT · epic PM Seat check
|
Fixes #25248
Part of #15212. Filed from the contract review of #25226 (record 6343868210, ③ 3a). Claim: 6350248910. Owed under ADR-0131 D8.
Measurement (step 1, on
mainat1eb1afae07)resolveLocalizationContextUncachedreadssys_settingdirectly when the settings service does not answer. Onmainthat read isscope: 'tenant'with no organization filter, and it takes the first row per key.main?sys_setting's own registrar is@objectstack/service-settings(manifest.ts), so without it the read fails ("object not found"), memoized as a backend fault, andUTC/en-USanswersys_settingitselfgetManythrows:localizationnamespace not registered yet (bind window), or a partial manifestgetManythrows: the organization source'sAuthzStoreUnavailableError(#25226, a caller that names no organization undersingle), or the posture source throwsUTC/en-USdefaults always answer, so the fallback is skippedsys_organization)single,groupandisolatedTwo organizations, each with a tenant
localization.timezonerow: can a user of one read the other's? Yes. A scratch probe (not committed) used #25226's rig: a realObjectKernel+ObjectQL+SettingsServicePlugin, with only the driver a Map.isolated,org_aandorg_b, the settings service throwing (namespace not bound), callerorg_a:mainanswersAsia/Tokyo(org_b's row). This branch answersEurope/Paris(org_a's).single, two organizations, the organization read failing, caller names none:mainanswersorg_b'sAsia/Tokyo. This branch answersUTC.UTC, and the fallback is not reached.The success cache can then hold a leaked answer for up to
OS_LOCALIZATION_CACHE_TTL_MS.What changes (step 2): scoped, not retired
The fallback now reads with the reach
SettingsServicereads with since #25226 (localizationFallbackReach):organization_id = Xsingle, D9 derives Xorganization_id = Xsingle, zero or several organizationsgroup/isolated)sys_organizationregistered (lean embedding, #25133 letter A)organization_idNULL onlyresolveGuestOrganization, the predicate in this file that the settings plugin also wires.resolveTenancyPosture()), read the same way this file's platform-standing anchor reads it: no query, and a deployment that asked for a wall stays walled while running degraded. The fallback takes no posture input from the service it stands in for.whereand is compared in memory, followingpreferredRow's rule.resolveLocalizationContext's 30s failure cache can answer a deliberate post-bind re-read with the pre-bind value #11877 unchanged).Why not retire it. Two things still use it. The #10221 failure memo is keyed on this direct read and keeps the "no such table" log line from repeating. And the bind-window answer is still the caller's own stored value, which the #11580 re-resolve then replaces. Scoping keeps both, with no other organization's row and no NULL row outside lean.
Clause-②: no (narrowing)
Metadata-protocol: none
A localization read stops admitting another organization's row, or a NULL row outside lean embedding. The changeset ships
@objectstack/coreaspatchwith the BREAKING banner, the FROM/TO and the remedy. ADR-0087 disposition:not-required (no-migration-prescription).Pins, and the ablation
resolve-localization-fallback-organization.pin.test.ts(new, 14 cases):AuthzStoreUnavailableError;singlewith one, several or no organizations;isolatedandgroup, for a caller naming none and a caller naming its own organization;Existing fixtures in
resolve-authz-context.test.ts,resolve-localization-cache.test.tsand plugin-hono-server'scurrent-user-endpoints-localization.test.tsgave their rows no organization while their caller namedo1/org_lang. Their rows now carry the caller's organization. Nothing else in them changed.Ablation, from the committed state, each restored with
git checkout HEAD --(git statusclean afterwards):main's fallback (no organization at all)isolated,group)Proof (head
0139af1fe3)@objectstack/core:pnpm typecheck: exit 0.pnpm typecheckexit 0.node scripts/pm/dispatch-gates.mjs --commands: 70 derived. Each exit code was captured before any pipe, and all 70 now exit 0.check:objectql-double-limitandcheck:where-matcherwere red on the new pin's engine double. They are fixed in8c59890e8fand0139af1fe3, and both now exit 0.check-plugin-teardown-shape --self-testexited 3 (shallow clone). Aftergit fetch --unshallowit exits 0.check:dual-build-cjs-loadsexited 3 (no full build). Afterpnpm buildit exits 0.check-governed-merges --branch: NOT governed (0 of 6 paths). 336 changed lines (+305 / −31). Not Tier H/S.upgrade-18x-harness.dogfood.test.ts, both fixture legs and the fresh store): 16 / 16 pass.os serveof the showcase on an empty store undersingle:GET /api/v1/auth/me/localization200 (UTC/en-US).os serve(NODE_ENV=production,OS_PLATFORM_OWNER_EMAIL,OS_AUTH_MEMBERSHIP_POLICY=invite-only, a 32-byteOS_SECRET_KEY), on an empty store:isolatedandgroup: each ready in 9 s, health 200, sign-up 200, sign-in 200,me/localization200.isolated:PUT /api/settings/localizationwithtimezone: Asia/Tokyoanswered 200, and the nextme/localizationread answeredAsia/Tokyo.@objectstack/organizations, and a walledos serverefuses without it. For R4 only, the dependency was added locally.package.jsonand the lockfile were restored afterwards. None of it is in the diff.Acceptance notes
mainmoved past1eb1afae07(tof47f7f222f). None of its commits touch these files, so no merge was taken (§10).session_01Y5583bs2RDd53MH6kM1ca3). The body was written at head0139af1fe3, and three facts above have aged:Clause-②is corrected fromyes (narrowing)tono (narrowing), per the contract review 6353329341: the diff widens no surface. The changeset line follows in the dev's next commit.417fe64988addspackages/runtime/src/security/resolve-execution-context.test.ts, a fixture whose rows now carry the caller's organization.mainwas merged in at83ff895f4b(origin/main09e07c259a), so the "no merge was taken" line above is superseded.🤖 Generated with Claude Code
https://claude.ai/code/session_01DydJpxUXfktYEyRLvwbAEF