Skip to content

fix(service-settings): the tenant and user rungs stop reading organization-less sys_setting rows as every organization's fallback (ADR-0131 D8) — measure first #25202

Closed

Description

@objectstack-fleet

Filing gate ③: owed scope of #15212, found by the epic seat's reconciliation against main at 2919ddb875. ADR-0131 D8 says no layer has a NULL arm. The D8 note (6325596117) records only the storage organizationOutOfWriteReach as the remaining organization-less answer. Step 7 did not touch the settings service.

  • Who acts: the epic seat dispatches it.
  • Dedupe: none ("settings organization-less sys_setting fallback tenant rung" → no carrier).

Part of #15212.

What is on main

packages/services/service-settings/src/settings-service.ts:

  • OrganizationReach (:150-170): for a caller that names an organization (own), the tenant and user rungs read that organization's rows plus the rows written with no organization, which act as a fallback for every organization.
  • The organization-less and unwalled kinds read organization-less rows too.
  • More readers: :1822-1833, :2935-2940 and :2984-2994.

The global rung moved to its own deployment-level sibling in step 2 (#24590). So an organization-less tenant- or user-rung row is a D8 NULL reading, unless a ruling keeps it.

Wanted

  1. Measure first.
    • Can such a row still be written on any composition this release serves? Check fresh single, walled, an 18.0.0-born store (nullable columns, the ceremony voluntary), and lean embedding.
    • Which callers read through unwalled (the email brand name, the boot-time locale)?
    • If a ruling or ADR passage keeps the fallback, stop and report it with the citation.
  2. If owed:
    • Retire the organization-less reading on the tenant and user rungs.
    • Process-wide readers read the global sibling or name the one organization.
    • The ceremony's inventory covers any existing organization-less sys_setting rows (check organization-ownership-inventory.ts).
  • Clause-②: measured by the dev. A setting that stops answering for some caller is a narrowing.

Acceptance

  • No tenant- or user-rung read admits a NULL-organization row. This is pinned for own, walled and single.
  • An 18.0.0-born store keeps booting, and its settings keep answering (the upgrade harness lane).
  • R1–R4 hold.

Activity

  1. added
    priority:p1High: required for production / M2
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    on Oct 22, 2026
  2. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: epic seat round · 2026-10-22T11:37Z · owed scope of #15212, found by the reconciliation note 6339761793 (ADR-0131 D8; the D8 note 6325596117 names only storage's organizationOutOfWriteReach as remaining)
    Session: session_01Y5583bs2RDd53MH6kM1ca3
    Account: os-steve
    Branch: claude/issue-25202-settings-no-organization-less-rung
    Worktree: the cloud session's own checkout (mode:cloud)
    Domain: domain:services
    Seat: epic:#15194
    File surface:

    • packages/services/service-settings/src/** (OrganizationReach and its readers), and the callers that read through the unwalled kind, if they must name an organization or read the global sibling.
    • packages/cli/src/utils/organization-ownership-inventory.ts, only if existing organization-less sys_setting tenant- or user-rung rows need a fate. If touched, the PR takes needs:upgrade-harness.
    • The tests, and one .changeset/*.md.
    • ⛔ Not docs/adr/**.
    • ⛔ No ORGANIZATION_OWNERSHIP_SHAPE_GENERATION change.
    • Measure first. If a ruling or ADR passage keeps the fallback, stop and report it with the citation; do not build.
      Container & model: M, mode:cloud, model: TIER_DEFAULT. The review is at the tier the diff's declared face picks.
      Clause-②: no (narrowing) — expected: a tenant- or user-rung read stops admitting an organization-less row; the dev measures first and re-declares from the diff
      Responsibility: an organization-less sys_setting tenant- or user-rung row answers every organization's read as its fallback (settings-service.ts:150-170) | none | reached by every settings read on a store that holds such a row
      Thread-read: none
      Serial constraints cleared: no open PR on the line touches these files. refactor(objectql)!: retire the PLATFORM_OBJECT_TENANCY ledger (ADR-0131 D13) — the bypassTenantAudit mute becomes structural, and the [tenant-audit] warning stops telling a wall-exempt write to pass a tenantId #25201 (the objectql ledger) runs in parallel and shares no file.
  3. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 25202,
    "status": "in_progress",
    "branch": "claude/issue-25202-settings-no-organization-less-rung",
    "pr": "#25226",
    "session": "session_01CUy2hdPMToZMsUtnAoshdP (dispatched by epic:#15194, session_01Y5583bs2RDd53MH6kM1ca3)",
    "premise_still_valid": true,
    "summary": "Measured OWED on main 9c431ba: no ruling keeps the tenant/user-rung organization-less fallback (ADR-0131 D1 as amended by 6328195425; D8; D8 note 6325596117 names only storage's organizationOutOfWriteReach). Built on head 576b828, draft PR #25226. Each reach now owns one organization: own(X) reads X only; a caller naming none reads/writes as the organization ADR-0131 D9 derives (resolveGuestOrganization, wired by SettingsServicePlugin), gets no tenant/user rung where none is derivable or behind a wall (tenant write refused), and a composition with no organization object keeps the lean NULL path (#25133 A). Same cause fixed: under single an organization-less second save inserted a duplicate row and was lost. Ceremony inventory unchanged (sys_setting already carries the attribution fate), so no needs:upgrade-harness. Clause-②: yes (narrowing). Metadata-protocol: none. CI pending.",
    "measurement": {
    "reach_admits (settings-service.ts:150-171, preferredRow :1822-1833, withinReach :2935-2940, loadScopedRows :2984-2994)": {
    "own (caller names org X, every posture)": "query: each rung crossed with organization_id IN {X, NULL}. preferredRow: X's row, else the NULL row. A NULL row answers every organization that has no row of its own (the shared-owner fallback).",
    "organization-less (no org, group/isolated)": "query: organization_id = NULL only. preferredRow: the NULL row. The caller reads exactly the organization-less rows.",
    "unwalled (no org, single or no posture source)": "query: no organization filter at all. preferredRow: the first organization-stamped row, else the NULL row (candidates[0])."
    },
    "can an organization-less tenant/user row be written (probe through the real ObjectQL engine + wrapEngineAsSettingsEngine; driver stamps options.tenantId like injectTenantOnInsert)": {
    "fresh single (sys_organization registered, one org)": "NO. setMany carries organization_id NULL. The engine's resolveSystemInsertOrganization derives the Default Organization, and the row lands stamped.",
    "walled group / isolated": "NO. A tenant key is refused at the service pre-flight (SETTINGS_VALIDATION, constraint scope tenant). A user key passes the service and the engine refuses it: ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED (walled-posture). Rows: [] for both postures.",
    "18.0.0-born store (nullable columns, voluntary ceremony)": "NO on a served composition: same write path as above. A nullable column matters only for the lean/--dev-without-auth path below. fixture-18.0.0/database.sql holds 2 sys_setting rows (localization.timezone and branding.workspace_name, both tenant, both org_mvgqf77lj6oc60fk), so 0 organization-less. fixture-18.0 also holds 2. The #24919 reading cited in the D1 amendment: 0 NULL organization rows on every store.",
    "lean embedding (no sys_organization)": "YES, by ruling (#25133 A). The probe stored [[tenant,null],[user,null]]. A driver-created table (7b-0) refuses at NOT NULL; the in-memory driver and a pre-7b-0 nullable table accept.",
    "--dev": "os dev composes auth, so it behaves as served single/walled. --dev with the auth tier off composes no organization object, so it takes the lean path (D9 note, boundary 3).",
    "v17 store after the ceremony": "the inventory already gives sys_setting a fate (organization-ownership-inventory.ts:660, 'attribution', D10_SINGLE): single attributes to the Default Organization; walled leaves the row NULL and REPORTED. A reported row is the one population today's 'own' fallback serves on a served store."
    },
    "unwalled_write_defect": "Probe, single + sys_organization + one org, organization-less caller: set(motto,'one') stores [org_default,'one']. set(motto,'two') then probes upsertRow's identity with organization_id NULL, misses the stamped row and INSERTS a second one: [[org_default,'one'],[org_default,'two']]. Both readers keep answering 'one', so the write is lost. On a SQL store the identity index (ensureSysSettingIdentityIndex; fixture-18.0.0 line 895, COALESCE(organization_id,'global'),namespace,key,scope,COALESCE(user_id,'')) refuses the second insert, so the save fails. The cause is the same one: the unwalled caller never names the one organization.",
    "callers reading through unwalled / organization-less": [
    "plugin-auth auth-plugin.ts:1102 get(branding.workspace_name, {}): email appName. Tenant key. Branding is offered only where no wall stands (UNWALLED_ONLY_NAMESPACES), so this read happens under single only.",
    "plugin-auth auth-plugin.ts:1155 get(localization.locale, {}): auth mail/SMS locale. Tenant key, read under EVERY posture. Under a wall it reads the organization-less rows only.",
    "cloud-connection runtime-config-plugin.ts:1172 getMany(branding.*, {}): the anonymous runtime-config door. Single only (listManifests gate).",
    "objectql lifecycle-service.ts:893-897 get(lifecycle.retention_overrides): a tenant key read without tenantId. The per-org read at :926 passes tenantId.",
    "core resolve-authz-context.ts:2498/2510 resolveLocalizationContext({tenantId?, userId}): a signed-in user with no active organization (REST request context, MCP, current-user endpoints, plugin-audit getLocale).",
    "the settings HTTP door: a signed-in caller with no active organization (verifiedContextFromRequest: tenantId from resolveRequestExecutionContext).",
    "every other organization-less caller reads a GLOBAL namespace (auth, mail, sms, storage, membership policy), which the reach does not touch."
    ],
    "what they read without the fallback": "Under single: the Default Organization's own row, once the reach NAMES the one organization. That is the row they already prefer today; nothing changes for them unless a NULL row is all a store holds, and the measured stores hold none. Under a wall: no tenant or user row, so the global rung (sys_platform_setting) or the default answers. Today they read only NULL rows, and a served walled store holds those only as ceremony-reported rows."
    },
    "tests": "576b8283: service-settings vitest 58 files / 982 tests pass; typecheck exit 0. New/rewritten pins: settings-organization-isolation.pin.test.ts (own, walled, single, lean; tenant + user rung; lock pre-flight), settings-organization-reach.pin.test.ts (real SettingsServicePlugin + ObjectKernel + ObjectQL: single with 1/0/2 orgs, group/isolated, lean). Upgrade harness upgrade-18x-harness.dogfood.test.ts 16/16 pass (both fixture legs + fresh store), incl. new 18.0.0 test: settings still answer via GET /api/settings/branding and for a reader naming no organization. Ablations from committed state: A1 own admits NULL -> 8 pins red; A2 plugin wires no organization source -> 4 plugin pins + the harness 18.0.0 settings test red; A3 write stores caller org not reach org -> 3 pins red. Downstream spot-check: plugin-auth 124, core 33, organizations 32 pass.",
    "gates": "node scripts/pm/dispatch-gates.mjs --commands at 576b828: 72 derived, 70 exit 0, 2 exit 3 NOT MEASURED (check-plugin-teardown-shape --self-test: shallow clone; check:dual-build-cjs-loads: partial build); --ran verdict ✓ 72 accounted. check-adr-0087-registration: ✓ not-required (no-migration-prescription). check-governed-merges --branch: 0/9 governed, 645 lines. R1 (fresh store boots and serves) and R2 via the harness; R4 walled covered by pins, walled dogfood boot NOT MEASURED locally. PR check-runs: pending.",
    "dogfood_battery": "NOT MEASURED locally beyond the upgrade harness (16/16); CI full battery pending",
    "mcp_calls": "read only: issue_read x5 (#25202, #15212, #15194)",
    "api_writes": "2: fleet-write pr_create (#25226, run 39787962628), this comment",
    "open_questions": [],
    "out_of_scope_findings": []
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-22T16:00Z. PR #25226 merged into main as 279fdcf469. It closes this card under ADR-0131 D8 and D1 as amended.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpm:dispatchedpm:epicpriority:p1High: required for production / M2security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions