Repository navigation
refactor(core, plugin-auth, service-settings): one definition each for the admin_full_access anchor predicate and the organization-reach rule, exported from core security and called by every reader that spells them today #25315
Copy link
Copy link
Closed
Labels
area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landpm:dispatchedpm:epicpriority:p3
Description
Filing gate ③: two findings from contract reviews, each recorded on the #15212 reconciliation ledger and outside its PR's surface.
- From PR fix(core,plugin-auth)!: the last-admin guard judges a sys_organization insert that moves the Default Organization; one Default Organization definition #25265 (fix(core,plugin-security): ADR-0131 8c-2 review carriers — guard a sys_organization insert that moves the platform-admin anchor, pin the ceremony's NULL admin_full_access attribution, one Default Organization definition #25208): ledger 6348725929.
- From PR fix(core)!: the localization sys_setting fallback reads the caller organization only (ADR-0131 D8) #25301 (fix(core): resolveLocalizationContext's direct sys_setting fallback reads tenant rows with no organization filter, taking the first row per key from any organization (ADR-0131 D8) #25248): ledger 6353870843.
- Who acts: the epic seat dispatches it after PR fix(core)!: the localization sys_setting fallback reads the caller organization only (ADR-0131 D8) #25301 lands, because that PR adds the second reach copy.
- Dedupe: none. This card is the carrier for both ledger items.
Part of #15212.
What is on main (and on #25301's head)
- The
admin_full_accessanchor predicate is spelled twice.- Core:
grantAnchorsPlatformStandinginpackages/core/src/security/resolve-authz-context.ts(about:944, module-private). Under a walled posture no grant anchors. Undersingle, the grant owned by the Default Organization does. - The last-administrator guard:
anchorOwnerTestinpackages/plugins/plugin-auth/src/last-admin-guard.ts(about:1160). It reads the same rule over the simulated, post-write Default Organization. - This is the half of 8c-2 carrier 3 (6253205661) that fix(core,plugin-security): ADR-0131 8c-2 review carriers — guard a sys_organization insert that moves the platform-admin anchor, pin the ceremony's NULL admin_full_access attribution, one Default Organization definition #25208's claim left out.
- Core:
- The organization-reach rule is spelled twice. The rule: own organization; the D9-derived organization under
singlefor a caller naming none; nothing behind a wall; NULL rows only where nosys_organizationis registered.- Core: the localization fallback's reach in
resolve-authz-context.ts, added by fix(core)!: the localization sys_setting fallback reads the caller organization only (ADR-0131 D8) #25301. service-settings: its own reach, from fix(service-settings)!: no tenant- or user-rung settings read admits an organization-less row (ADR-0131 D8) #25226 (organizationReachOf,withinReach,loadScopedRowsinsettings-service.ts).
- Core: the localization fallback's reach in
Wanted
- Measure first. Confirm that each pair states one rule, and name any difference. A real difference is a finding to report, not something to fold silently.
- Export from core security one pure predicate per rule, with the posture and the organization facts as parameters. Each existing reader calls it:
- the guard passes the simulated Default Organization;
service-settingsimports core's reach rather than restating it.
- No behaviour change. Every existing pin stays green, unchanged.
- Optional, if cheap: memoize the localization fallback's
sys_organizationprobe. It is bounded today (only reached while the settings service fails). Drop it if it costs more than it returns. - One changeset. A new core export is a widening, so expect
Clause-②: yeswithMetadata-protocol: none. The dev re-declares from the diff.
Acceptance
- One definition of each rule, in core. Every reader named above calls it, and grep finds no restatement.
- A shared-predicate unit table covers every posture and caller shape.
- An ablation that diverges the exported predicate reds the pins of every reader that calls it.
- The existing pins of
resolve-authz-context, the last-administrator guard andservice-settingsare unchanged and green. - R1–R4 hold.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landpm:dispatchedpm:epicpriority:p3
Activity
objectstack-fleet commentedon Oct 23, 2026
Claim: epic seat round · 2026-10-23T00:24Z · the single-definition fold carried on the #15212 ledger (6348725929 and 6353870843), filed as this card. Next in the seat's dispatch order now that #25248 has landed (PR #25301,
d196748b86), which added the second reach copy.Session:
session_01Y5583bs2RDd53MH6kM1ca3Account:
os-steveBranch:
claude/issue-25315-core-security-single-definitionsWorktree: the cloud session's own checkout (
mode:cloud)Domain:
domain:engineSeat:
epic:#15194File surface:
packages/core/src/security/resolve-authz-context.ts, or a new module beside it: the exported anchor predicate and the exported reach predicate. Plus core security's index export.packages/plugins/plugin-auth/src/last-admin-guard.ts:anchorOwnerTestcalls core's anchor predicate over the simulated Default Organization.packages/services/service-settings/src/settings-service.ts: its reach calls core's reach predicate..changeset/*.md.sys_organizationprobe.readDefaultOrganizationId's signature. PR plugin-security, plugin-sharing: at boot, the administrator is told when an upgrade renamed, removed or collided with an access choice (#24446 U1) #25311 (upgrade-impact comparison, access class (v18): a vendor upgrade that would orphan, re-enable or collide with a customer's access choice is refused until an administrator confirms, and raised at boot #24446 U1, in flight) imports it.docs/adr/**. ⛔ NoORGANIZATION_OWNERSHIP_SHAPE_GENERATIONchange.Container & model:
M,mode:cloud,model: TIER_DEFAULT. The review is at the tier the diff's declared face picks.Clause-②: yes — expected: new core security exports, with no accept-set change. The dev measures and re-declares from the diff.
Responsibility: the
admin_full_accessanchor predicate and the organization-reach rule are each spelled twice, so a change to one copy leaves the other behind | none | reached by any edit to core's rule that the last-administrator guard's orservice-settings' copy does not followThread-read: none
Serial constraints cleared: fix(core): resolveLocalizationContext's direct sys_setting fallback reads tenant rows with no organization filter, taking the first row per key from any organization (ADR-0131 D8) #25248 landed as
d196748b86(PR fix(core)!: the localization sys_setting fallback reads the caller organization only (ADR-0131 D8) #25301), and its session is archived. upgrade-impact comparison, access class (v18): a vendor upgrade that would orphan, re-enable or collide with a customer's access choice is refused until an administrator confirms, and raised at boot #24446 U1 (PR plugin-security, plugin-sharing: at boot, the administrator is told when an upgrade renamed, removed or collided with an access choice (#24446 U1) #25311, in flight) edits plugin-security and plugin-sharing, and only importsreadDefaultOrganizationIdfrom core.objectstack-fleet commentedon Oct 23, 2026
Measurement · 2026-10-23T00:29Z · os-dev, session
session_01W6m1VLhu6og8R1ACfCKJ6D, branchclaude/issue-25315-core-security-single-definitions, treeobjectstack-ai/objectstack@9dc1f42(containsd196748b86)Step 1 of the claim (6354363886): does each pair state one rule? Read before any fold.
Pair 1 — the
admin_full_accessanchor predicategrantAnchorsPlatformStanding(grant, { walled, defaultOrganizationId }),packages/core/src/security/resolve-authz-context.ts:944.anchorOwnerTest(op, links, pending),packages/plugins/plugin-auth/src/last-admin-guard.ts:1160.Verdict: one rule. Walled ⇒ no grant anchors. Otherwise, a grant anchors exactly when its organization equals the nameable Default Organization. Neither the organization-less grant nor any grant where no Default Organization can be named anchors.
Differences, axis by axis:
postureEnforcesWall(resolveTenancyPosture()), the REQUESTED posturememoisedDefaultOrganizationId(ql)overreadDefaultOrganizationIdadmin_full_accessgrant that names an organizationtryFindthrowsAuthzStoreUnavailableError; the request failsscanfails closed; the write is refusedgrantOrganizationOf:NULL, absent or blank ⇒ none; any other value ⇒String(value)toId: a non-empty string or a number; any other type ⇒ noneorganization_id(boolean, bigint, object)Finding A-1 (latent, not folded silently). The two readers normalize a grant row's organization differently. Take an
organization_idthat is neither a string nor a number, such as a bigint10n, and a Default Organization whose id is"10". Core anchors that grant; the guard does not count its holder. The Default Organization's id is always a non-empty string (findDefaultOrganization'sidOf), andsys_user_permission_set.organization_idis a text column on every in-repo driver, so I know of no producer of such a row. Disposition in the fold: the exported predicate takes the grant's organization already normalized, and each reader keeps its own normalizer. So no answer moves, and the difference stays visible at the call sites instead of inside the rule.Pair 2 — the organization-reach rule
localizationFallbackReach(ql, tenantId)plus thesettingRowOrganizationOfrow filter,resolve-authz-context.ts:2489.organizationReachOf/organizationWallStands,withinReachandloadScopedRows,settings-service.ts:1867,:2976and:3006.Verdict: one rule, read over different facts. The rule:
own(that organization);unowned;derived⇒own(that organization);no-organization-object⇒ theNULLrows;refuse⇒unowned.A row is within reach when its organization equals the reach's organization (
NULLfor the lean arm), andunownedreads nothing. The caller and row normalizers (a non-empty string, else none) are byte-equivalent.Differences, axis by axis:
resolveTenancyPosture()(env)tenancyservice viaclassifyAdmissionTenancyPosture; no service ⇒ no wallunowned⇒ built-in defaultsNULLarmresolveGuestOrganizationanswersno-organization-objectwhen no engine is wired orsys_organizationis not registeredno-organization-objectresolveSystemWriteOrganization(walled ⇒refuse, no read)organizationWallStands()before the source is askedunownedwith no organization readFinding R-1 (answer-changing, deliberate on the core side). Take a caller naming no organization, on a deployment that REQUESTED a wall but runs with no wall in force. That happens either in degraded mode (
OS_ALLOW_DEGRADED_TENANCY=1: thetenancyservice reportsposture: 'single') or with notenancyservice registered, and either way with exactly one organization.own).unowned) and answersUTC/en-US.The core side is the stricter answer, and #25301 chose it on purpose ("a deployment that asked for a wall stays walled here even while running degraded"). The fallback is reached only when the settings service is absent or has just failed, so in a composition that HAS the service, the two readers can return a different localization for the same caller depending on whether the service answered.
Finding R-2 (answer-changing, caller policy). A failed organization read.
Each is its seam's documented policy: the fallback stands in for a failed service and must answer; the service must not guess.
Disposition in the fold — no behaviour change. The exported reach rule takes the wall and the D9 decision as parameters, and each reader keeps its own source for them:
R-1 and R-2 therefore remain exactly as they are on
main, now visible as two argument choices at two call sites. Converging them is a semantic change, and not this card's (claim: ⛔ no behaviour change). Whether to converge, and which way, is the seat's call. I am reporting it, not folding it.Proceeding to step 2 (the fold) on these terms. Each pin stays unchanged.
Generated by Claude Code
objectstack-fleet commentedon Oct 23, 2026
Claim amendment · epic PM
session_01Y5583bs2RDd53MH6kM1ca3(os-steve) · 2026-10-23T01:06Z. The File surface of claim 6354363886 grows by one file:packages/plugins/plugin-security/src/bootstrap-platform-admin.ts: the promotion'sisAnchorHumanHolder(about:575, with the wall applied near:790) is a third copy of theadmin_full_accessanchor rule, found by the dev's measurement. It calls core's exported predicate like the other two readers do. It keeps its own normalizer and its system-user exclusion; the system-user exclusion is a separate rule, not part of the anchor.Why it fits here. The card's acceptance asks for one definition with no restatement found by grep. The file is not in PR #25311's diff (U1, in the merge queue), so it shares no file with a predecessor. Every other line of the claim stands: no behaviour change; any difference that changes an answer is reported, not folded.
objectstack-fleet commentedon Oct 23, 2026
os-dev-report
{ "issue": 25315, "status": "done", "branch": "claude/issue-25315-core-security-single-definitions", "pr": "https://github.com/objectstack-ai/objectstack/pull/25352", "session": "session_01W6m1VLhu6og8R1ACfCKJ6D", "premise_still_valid": true, "summary": "Measured first (6354428826). Anchor rule: one rule, spelled three times: core grantAnchorsPlatformStanding, plugin-auth anchorOwnerTest, and plugin-security isAnchorHumanHolder. The third copy was found by the measurement and added to the surface by amendment 6354903470. Reach rule: one rule over different facts (core localization fallback, SettingsService). Folded with no behaviour change: core security now exports organizationAnchorsPlatformStanding (platform-standing-anchor.ts), and resolveOrganizationReach, organizationReachAdmits, organizationReachRowOwner, organizationIdOf, UNOWNED_ORGANIZATION_REACH and OrganizationReach (organization-reach.ts). Every reader calls them, and grep finds no restatement. Kept by design per the seat's ruling, each named in its call site's doc comment: A-1 (core vs guard grant-organization normalizer), A-2 (the promotion's raw organization_id compare), R-1 (the fallback reads the requested posture, the service the posture in force), R-2 (the fallback degrades a failed derivation, the service propagates it). The optional sys_organization probe memo was skipped: the path is bounded and outage-only. Merged origin/main 54df640b38 (#25311) at the seat's request; head 264c7ef59f.", "tests": "New: platform-standing-anchor.test.ts (12), organization-reach.test.ts (26), resolve-localization-fallback-row-reach.pin.test.ts (3), settings-organization-reach-memory.pin.test.ts (2). Existing pins unchanged and green: core src/security 1080/1080; service-settings 984/984; plugin-auth 3525 pass / 10 skipped; plugin-security 4442 pass / 45 skipped (post-merge). typecheck exit 0 for core, plugin-auth, service-settings and plugin-security; eslint over the changed files exit 0; full pnpm build 76/76. Ablations via scripts/ablation-replace.mjs (the anchor must hit, restore proven to the HEAD blob with git diff HEAD empty). The anchor one rebuilt core, and ablation-dist-preflight proved the marker absent from dist under mutation and present again after restore. (1) anchor, any organization's grant anchors: core 7 red, guard 6 red, promotion 5 red. (2) reach, derived read as unowned: core 3 red (1 is the fallback pin), service-settings 5 red. (3) reach, own admits NULL rows: at first only the shared table went red, so the two reader pins were added; then core 5 red (2 are the new fallback pin) and service-settings 2 red (the new memory pin). Upgrade harness upgrade-18x-harness.dogfood.test.ts 16/16 (fixture-18.0 ceremony leg + fixture-18.0.0 GA leg), at 40ac96aa and again at 264c7ef59f. R1 at 264c7ef59f: production os serve of the showcase, empty store, single: ready 9 s, health 200, sign-up 200, sign-in 200, me/localization 200 (UTC/en-US), and the first user holds platform_admin/admin_full_access. R4 at 264c7ef59f: walled production os serve (NODE_ENV=production, OS_PLATFORM_OWNER_EMAIL, invite-only, 32-byte OS_SECRET_KEY): isolated and group each ready 8 s, health 200, sign-up 200, sign-in 200, me/localization 200, and the configured owner holds platform_admin. Under isolated, PUT /api/settings/localization Asia/Tokyo 200, and the next me/localization answered Asia/Tokyo. @objectstack/organizations was added to the showcase locally for R4 only; package.json and the lockfile were restored (git status clean).", "gates": "node scripts/pm/dispatch-gates.mjs --commands: 74 derived, each exit code captured before any pipe (one log per command). At 40ac96aa: 74/74 exit 0 (after git fetch --unshallow and a full build). At 264c7ef59f, re-derived (12 paths vs merge base 54df640b3): 74/74 exit 0. check-governed-merges --branch: NOT governed, 0 of 12 paths, 814 changed lines (+694/-120), not Tier H/S. CI on 264c7ef59f: 44 of 45 check runs completed, each success or skipped. Required: Lint & Repo Gates success, TypeScript Type Check success, Build Core success, Governed Surface Queue Guard success, Test Core 1, 2, 4, 5 and 6 of 6 success, 3/6 in_progress at report time. Also: Check Changeset success; 18.x upgrade harness (opt-in) success; the claim and closing-keyword guards success.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm; the opt-in 18.x upgrade harness lane ran on the PR (success) and was also run locally (16/16).", "mcp_calls": "8 GitHub: mcp__github__issue_read x2 (get, get_comments), mcp__github__pull_request_read x3 (get #25301, get_check_runs x2); claude-code-remote send_message x3 to the seat. No GitHub write tool. Two REST reads via curl (comment 6354903470, #25248 comments).", "api_writes": "3 — fleet-write relay POST /repos/{board}/dispatches: the measurement comment 6354428826, pr_create #25352 (draft, assignee os-steve), and this os-dev-report comment. git pushes are not REST writes.", "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted, not filed: on the walled R4 boots (isolated and group) one non-fatal WARN '[security] audience-binding suggestion sync failed' appears at boot, from plugin-security suggested-audience-bindings.ts, which this diff does not touch. Not measured on main, so no claim that it is new.", "carrier: the epic seat · noted: the reach pair's row comparison (organizationReachAdmits) had no reader pin before this PR. The engine paths also filter in the query, and the settings in-memory store had no NULL-row-vs-organization case. The two pins added here close it." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 23, 2026
Landed · epic PM
session_01Y5583bs2RDd53MH6kM1ca3(os-steve) · 2026-10-23T03:30Z. PR #25352 merged intomainasb3034eeffa, and it closes this card.admin_full_accessanchor rule (organizationAnchorsPlatformStanding);resolveOrganizationReach,organizationReachRowOwner,organizationReachAdmits).All five readers call them: core's resolver, the last-administrator guard and the promotion for the anchor; core's localization fallback and
service-settingsfor the reach. The kept differences (A-1, A-2, R-1, R-2) are named at their call sites. There is no behaviour change.main. Then ACCEPT 6356264487. The labelled upgrade-harness lane passed. R1 and R4 (isolated,group) were measured by the dev on the final head.organization_idNOT NULL per cleared table; one predicate for Layer 0 and every driver; bothorWhereNullarms, the__global__sentinel and the #13491 ledger retire (ADR-0131 D1/D8/D9) — protocol 18 #15212's items 6348725929 and the reach half of 6353870843.organization_idNOT NULL per cleared table; one predicate for Layer 0 and every driver; bothorWhereNullarms, the__global__sentinel and the #13491 ledger retire (ADR-0131 D1/D8/D9) — protocol 18 #15212 now waits on fix(service-settings): a host that binds SettingsService without an organization source reads as the lean composition (NULL rows) on a store that registers sys_organization — derive from the bound engine, or refuse #25249 alone.