You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
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.
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
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.
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).
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.
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": []
}
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.
It fixes a live defect: a second save by a no-organization caller under single duplicated the row.
Review and acceptance:CONTRACT_REVIEW_TIER, narrowing face, from an isolated reviewer: PASS 6343868210. Seat ACCEPT 6344522520.
R1–R4: R1–R3 passed in the upgrade-harness lane on the head. For R4, a walled os serve under isolated and group was measured before landing (health 200, settings read and write, no NULL row).
Activity
objectstack-fleet commentedon Oct 22, 2026
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
organizationOutOfWriteReachas remaining)Session:
session_01Y5583bs2RDd53MH6kM1ca3Account:
os-steveBranch:
claude/issue-25202-settings-no-organization-less-rungWorktree: the cloud session's own checkout (
mode:cloud)Domain:
domain:servicesSeat:
epic:#15194File surface:
packages/services/service-settings/src/**(OrganizationReachand its readers), and the callers that read through theunwalledkind, if they must name an organization or read the global sibling.packages/cli/src/utils/organization-ownership-inventory.ts, only if existing organization-lesssys_settingtenant- or user-rung rows need a fate. If touched, the PR takesneeds:upgrade-harness..changeset/*.md.docs/adr/**.ORGANIZATION_OWNERSHIP_SHAPE_GENERATIONchange.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_settingtenant- 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 rowThread-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.
objectstack-fleet commentedon Oct 22, 2026
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
objectstack-fleet commentedon Oct 22, 2026
Landed · epic PM
session_01Y5583bs2RDd53MH6kM1ca3(os-steve) · 2026-10-22T16:00Z. PR #25226 merged intomainas279fdcf469. It closes this card under ADR-0131 D8 and D1 as amended.sys_settingrow.own(X)reads X alone. A caller with no organization reads the organization D9 derives undersingle, and nothing under a wall. Only lean embedding ([Decision] A production os serve that composes no organization object (no auth secret, or no auth) still writes ownerless rows under --no-server: refuse that boot too, or refuse such writes in the engine everywhere #25133, letter A) still reads NULL rows.singleduplicated the row.CONTRACT_REVIEW_TIER, narrowing face, from an isolated reviewer: PASS 6343868210. Seat ACCEPT 6344522520.os serveunderisolatedandgroupwas measured before landing (health 200, settings read and write, no NULL row).sys_settingwith no organization filter) and 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 (a self-binding host with no organization source).