From 5d1b91e9b2c13f9e35a7a0270db575bdc2100578 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 02:57:34 +0000 Subject: [PATCH 01/11] =?UTF-8?q?test:=20stamp=20incidental=20organization?= =?UTF-8?q?-less=20grant=20fixtures=20with=20the=20organization=20their=20?= =?UTF-8?q?context=20reads=20under=20(C8=20stage=208c-T-=CE=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four grant rows whose test reads them under an active organization and whose assertions never read the organization-less arm now carry that organization, so they hold under today's resolver and once the NULL grant arm retires. Assertions unchanged. Claude-Session: https://claude.ai/code/session_01ATWiuWCaTBk1PD4VPimDsq Co-authored-by: Claude --- packages/core/src/security/resolve-authz-context.test.ts | 4 ++-- .../src/current-user-endpoints-position-grants.test.ts | 2 +- .../security/request-execution-context.conformance.test.ts | 2 +- .../runtime/src/security/resolve-execution-context.test.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/src/security/resolve-authz-context.test.ts b/packages/core/src/security/resolve-authz-context.test.ts index ccafb7d2e06..344f1ba51a8 100644 --- a/packages/core/src/security/resolve-authz-context.test.ts +++ b/packages/core/src/security/resolve-authz-context.test.ts @@ -1090,8 +1090,8 @@ describe('resolveUserAuthzGrants — userId-driven authz for non-HTTP surfaces ( const ql = makeQl({ sys_user: [{ id: 'u1', email: 'ada@x.com' }], sys_member: [{ user_id: 'u1', role: 'admin', organization_id: 'o1' }], - sys_user_position: [{ user_id: 'u1', position: 'approver', organization_id: null }], - sys_user_permission_set: [{ user_id: 'u1', permission_set_id: 'psA', permission_set: 'ehr_all', organization_id: null }], + sys_user_position: [{ user_id: 'u1', position: 'approver', organization_id: 'o1' }], + sys_user_permission_set: [{ user_id: 'u1', permission_set_id: 'psA', permission_set: 'ehr_all', organization_id: 'o1' }], sys_permission_set: [{ id: 'psA', name: 'ehr_all', system_permissions: ['cap_ehr'] }], }); const grants = await resolveUserAuthzGrants(ql, 'u1', { tenantId: 'o1' }); diff --git a/packages/plugins/plugin-hono-server/src/current-user-endpoints-position-grants.test.ts b/packages/plugins/plugin-hono-server/src/current-user-endpoints-position-grants.test.ts index 8e37c51a1ca..3f7b5da4f06 100644 --- a/packages/plugins/plugin-hono-server/src/current-user-endpoints-position-grants.test.ts +++ b/packages/plugins/plugin-hono-server/src/current-user-endpoints-position-grants.test.ts @@ -226,7 +226,7 @@ describe('position-bound permission sets reach /auth/me/permissions (#6334)', () const app = mount({ tables: baseTables({ sys_user_permission_set: [ - { id: 'ups1', user_id: USER, permission_set_id: 'ps_direct', permission_set: 'showcase_direct', organization_id: null }, + { id: 'ups1', user_id: USER, permission_set_id: 'ps_direct', permission_set: 'showcase_direct', organization_id: ACTIVE_ORG }, ], }), }); diff --git a/packages/runtime/src/security/request-execution-context.conformance.test.ts b/packages/runtime/src/security/request-execution-context.conformance.test.ts index cc8033c13b0..888c4ed44bb 100644 --- a/packages/runtime/src/security/request-execution-context.conformance.test.ts +++ b/packages/runtime/src/security/request-execution-context.conformance.test.ts @@ -139,7 +139,7 @@ function world(opts: { auth: boolean; gateActive?: boolean; sessionApi?: boolean { user_id: 'u_ses', organization_id: ORG, role: 'admin' }, ], sys_user_position: [{ user_id: 'u_ses', position: 'sales_rep', organization_id: ORG }], - sys_user_permission_set: [{ user_id: 'u_key', permission_set: 'contact_reader', organization_id: null }], + sys_user_permission_set: [{ user_id: 'u_key', permission_set: 'contact_reader', organization_id: ORG }], sys_permission_set: [ { id: 'ps_sales', name: 'sales_user', object_permissions: '{}' }, { id: 'ps_contact', name: 'contact_reader', system_permissions: '["export_reports"]', object_permissions: '{}' }, diff --git a/packages/runtime/src/security/resolve-execution-context.test.ts b/packages/runtime/src/security/resolve-execution-context.test.ts index 8ed5ab0105f..a5d64d8aa4c 100644 --- a/packages/runtime/src/security/resolve-execution-context.test.ts +++ b/packages/runtime/src/security/resolve-execution-context.test.ts @@ -415,7 +415,7 @@ describe('resolveExecutionContext — posture plumbing (#2947)', () => { it('carries MEMBER for an ordinary principal (no admin / org-admin capability)', async () => { const ql = makeQlFor( [{ id: 'ps_basic', name: 'sales_rep', system_permissions: '[]', object_permissions: '{}' }], - [{ id: 'ups1', user_id: 'u1', permission_set_id: 'ps_basic', permission_set: 'sales_rep', organization_id: null }], + [{ id: 'ups1', user_id: 'u1', permission_set_id: 'ps_basic', permission_set: 'sales_rep', organization_id: 'orgA' }], ); const ctx = await resolveExecutionContext(opts(ql)); expect(ctx.userId).toBe('u1'); From 49b63a4795067b46be80d3ae93a17f01e5957f0d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 04:53:54 +0000 Subject: [PATCH 02/11] wip: lift the platform-object system-insert exemption under single (C8 stage 8b) --- packages/objectql/src/engine.ts | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/objectql/src/engine.ts b/packages/objectql/src/engine.ts index 7f8171017e0..b768ac2d550 100644 --- a/packages/objectql/src/engine.ts +++ b/packages/objectql/src/engine.ts @@ -7173,16 +7173,20 @@ export class ObjectQL implements IObjectQLEngine { // system write that threaded one. Nothing to resolve; this is the shape the // ruling asks every system write to reach. if (carriesOrganization(execCtx?.tenantId)) return undefined; - // [#13491] Platform-namespace objects are excluded PER OBJECT, not - // wholesale. The 2026-08-31 ruling withdrew the blanket - // `isPlatformNamespaceObject(object)` exemption that used to stand here: - // commit ff08691e6's "an org-less row is defensible for `sys_permission_set`" - // inherits per object, and the namespace also holds objects whose org-less - // rows are a defect — five instances of that class, all found by hand. - // `unclassified` keeps the old exclusion so an unadjudicated object's - // behaviour does not move; see `platform-object-tenancy.ts` for the - // inventory and why the classification cannot be read off the schema. - if (isPlatformObjectOutOfTenantAuditScope(object)) return undefined; + // [ADR-0131 D9, C8 stage 8b] Under `single` a platform object has no + // exemption: with a tenant column it is in scope like any other object, so + // its organization-less system insert is stamped with the Default + // Organization or refused below — never left NULL. + // + // ⚠️ WALLED POSTURES ONLY: #13491's per-object exemption still stands under + // `group` / `isolated` (epic ruling record 6259899486 on #15194, item 4). + // 18.0 refuses a walled posture at the operator's boot path, and the walled + // half of the lift lands in 18.x, after the stages that give the walled + // writers an organization of their own. The cut is a POSTURE condition on + // purpose — ⛔ never re-narrow it with a per-table list. + if (isPlatformObjectOutOfTenantAuditScope(object) && this.resolveEnginePosture() !== 'single') { + return undefined; + } const objectSchema = this._registry.getObject(object) as any; // A federated object's schema is the REMOTE's (ADR-0015); the platform's // injected column says nothing about it, which is the same reason From a00e9cda0e6344b2a88baf4271d4223aff0ca650 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 05:14:40 +0000 Subject: [PATCH 03/11] feat(objectql)!: under `single`, a platform object's organization-less system insert is stamped or refused (C8 stage 8b) `resolveSystemInsertOrganization` returned before the posture was read for every in-reach platform object outside the 9-entry admit list, so those inserts landed with a NULL organization_id under every posture. Under `single` that exemption is gone: the insert takes the Default Organization, or is refused at zero or several organizations (ADR-0131 D9). Under a walled posture the #13491 exemption stands in 18.0, as a named posture condition (epic ruling record 6259899486, item 4). The ledger keeps two readers (the walled exemption and the diagnostics-only isSystem mute), so it does not retire here; its header says so. Claude-Session: https://claude.ai/code/session_01AZMXDVkiaxSpJkGXACuVwr Co-authored-by: Claude --- .../src/system-write-organization.test.ts | 37 +++++++++----- .../tenancy-by-object-classification.test.ts | 51 +++++++++++++++---- .../src/tenancy/platform-object-tenancy.ts | 29 +++++++++-- .../src/tenancy/system-write-organization.ts | 15 ++++-- 4 files changed, 99 insertions(+), 33 deletions(-) diff --git a/packages/objectql/src/system-write-organization.test.ts b/packages/objectql/src/system-write-organization.test.ts index 5faf70bad3f..15992a8ed29 100644 --- a/packages/objectql/src/system-write-organization.test.ts +++ b/packages/objectql/src/system-write-organization.test.ts @@ -124,12 +124,11 @@ const DISPATCH_ORDER = { /** * A platform-namespace object the #13491 inventory did NOT adjudicate. * - * ⚠️ Its exclusion below is `unclassified`, not `global`. Until the 2026-08-31 - * ruling this file read it as "platform namespace ⇒ deliberately org-less - * (…)"; that wholesale reading was withdrawn, and what these cases now pin - * is that an UNADJUDICATED object's behaviour did not move — which is what - * bounds the reclassification's blast radius to the admitted list. The admitted - * and `global` sides are pinned in `tenancy-by-object-classification.test.ts`. + * ⚠️ Its exclusion below is `unclassified`, not `global`, and since ADR-0131 + * C8 stage 8b it holds under a WALLED posture only (epic ruling record + * 6259899486 on #15194, item 4). Under `single` the object is judged like any + * other — derived or refused. The admitted and `global` sides are pinned in + * `tenancy-by-object-classification.test.ts`. */ const SYS_LEDGER = { name: 'sys_audit_entry', @@ -389,20 +388,28 @@ describe('#8844 multi-organization — the write is REFUSED, never defaulted', ( }); describe('#8844 the exclusions — populations the refusal must not touch', () => { - it.each(['isolated', 'single'] as const)( - 'a platform-namespace object stays org-less on the %s posture', + it.each(['isolated', 'group'] as const)( + 'a platform-namespace object stays org-less on the %s posture — the walled exemption stands in 18.0', async (posture) => { - // ⚠️ [#13491] Reads as an UNCLASSIFIED verdict now, not a namespace one. - // The card commit ff08691e6 cites measured this primitive on `sys_permission_set` and filed it as an - // observation because an org-less row is defensible there. The #8844 - // ruling confirms that reasoning holds for platform objects and does NOT - // generalize to application objects — which is exactly the boundary here. + // ⚠️ [#13491] Reads as an UNCLASSIFIED verdict, not a namespace one, and + // [C8 stage 8b] holds under a wall only: 18.0 refuses a walled posture at + // the operator's boot path, and the walled half of the lift is 18.x's + // (epic ruling record 6259899486 on #15194, item 4). const { engine, observed } = await makeEngine({ posture }); await engine.insert('sys_audit_entry', { subject: 'e1' }, { context: SYSTEM_CTX } as any); expect(lastWrite(observed, 'sys_audit_entry')?.options?.tenantId).toBeUndefined(); }, ); + it('[C8 stage 8b] single posture: a platform-namespace object is no longer excluded — it takes the sole organization', async () => { + // ADR-0131 D9 / D11: `single` is never a reason for a row to be org-less. + // The walled case above is the control: same object, same write, only the + // posture differs. + const { engine, observed } = await makeEngine({ posture: 'single', organizations: [ORG_ID] }); + await engine.insert('sys_audit_entry', { subject: 'e1' }, { context: SYSTEM_CTX } as any); + expect(lastWrite(observed, 'sys_audit_entry')?.options?.tenantId).toBe(ORG_ID); + }); + it.each(['isolated', 'single'] as const)( 'an object declaring `tenancy.enabled: false` stays org-less on the %s posture', async (posture) => { @@ -418,8 +425,10 @@ describe('#8844 the exclusions — populations the refusal must not touch', () = it('never probes `sys_organization` for a write it does not judge', async () => { // The cost half of the exclusions: an ordinary write must pay two property // reads and a regexp, not a query. + // [C8 stage 8b] A platform object is judged under `single` now, so it is + // not in this population: only the walled exemption skips its probe, and + // a walled refusal never probes anyway. const { engine, observed } = await makeEngine({ organizations: [ORG_ID] }); - await engine.insert('sys_audit_entry', { subject: 'e1' }, { context: SYSTEM_CTX } as any); await engine.insert('billing_license', { subject: 'lic' }, { context: SYSTEM_CTX } as any); await engine.insert('dispatch_order', { subject: 'session' }, { context: SESSION_CTX } as any); expect(observed.filter((c) => c.object === 'sys_organization')).toEqual([]); diff --git a/packages/objectql/src/tenancy-by-object-classification.test.ts b/packages/objectql/src/tenancy-by-object-classification.test.ts index 90c610a20d7..9703830ec12 100644 --- a/packages/objectql/src/tenancy-by-object-classification.test.ts +++ b/packages/objectql/src/tenancy-by-object-classification.test.ts @@ -196,17 +196,48 @@ describe('#13491 gate 1 — an admitted platform object reaches #8844 derive/ref expect(lastWrite(observed, 'sys_file')?.options?.tenantId).toBe(ORG_ID); }); - it('single posture: a GLOBAL platform object still stays org-less — the control', async () => { - const { engine, observed } = await makeEngine({ posture: 'single' }); - await engine.insert('sys_permission_set', { label: 'Admin' }, { context: SYSTEM_CTX } as any); - expect(lastWrite(observed, 'sys_permission_set')?.options?.tenantId).toBeUndefined(); - }); + // [ADR-0131 C8 stage 8b] Under `single` the classification no longer decides + // the write: every platform object with a tenant column is judged, admitted + // or not. The walled cases below are the control — there the exemption + // stands in 18.0 (epic ruling record 6259899486 on #15194, item 4). + it.each([ + ['sys_permission_set', { label: 'Admin' }], + ['sys_audit_entry', { subject: 'e1' }], + ] as const)( + 'single posture, one organization: the un-admitted platform object %s now DERIVES it', + async (object, row) => { + const { engine, observed } = await makeEngine({ posture: 'single' }); + await engine.insert(object, { ...row }, { context: SYSTEM_CTX } as any); + expect(lastWrite(observed, object)?.options?.tenantId).toBe(ORG_ID); + }, + ); - it('single posture: an UNCLASSIFIED platform object stays org-less — behaviour did not move', async () => { - const { engine, observed } = await makeEngine({ posture: 'single' }); - await engine.insert('sys_audit_entry', { subject: 'e1' }, { context: SYSTEM_CTX } as any); - expect(lastWrite(observed, 'sys_audit_entry')?.options?.tenantId).toBeUndefined(); - }); + it.each([ + ['no organization', [] as string[], 0], + ['two organizations', [ORG_ID, 'org_second'], 2], + ] as const)( + 'single posture, %s: an org-less system insert on an un-admitted platform object is REFUSED, loudly, and never written', + async (_label, organizations, count) => { + const { engine, observed } = await makeEngine({ posture: 'single', organizations: [...organizations] }); + await expect( + engine.insert('sys_audit_entry', { subject: 'e1' }, { context: SYSTEM_CTX } as any), + ).rejects.toMatchObject({ + code: 'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED', + status: 500, + organizationCount: count, + }); + expect(lastWrite(observed, 'sys_audit_entry')).toBeUndefined(); + }, + ); + + it.each(['isolated', 'group'] as const)( + '%s posture: an UNCLASSIFIED platform object still stays org-less — the walled exemption stands in 18.0', + async (posture) => { + const { engine, observed } = await makeEngine({ posture }); + await engine.insert('sys_audit_entry', { subject: 'e1' }, { context: SYSTEM_CTX } as any); + expect(lastWrite(observed, 'sys_audit_entry')?.options?.tenantId).toBeUndefined(); + }, + ); it.each(['isolated', 'group'] as const)( '%s posture: an org-less system write on an admitted object is REFUSED, loudly', diff --git a/packages/objectql/src/tenancy/platform-object-tenancy.ts b/packages/objectql/src/tenancy/platform-object-tenancy.ts index 3965872d8f6..cc76ce2a757 100644 --- a/packages/objectql/src/tenancy/platform-object-tenancy.ts +++ b/packages/objectql/src/tenancy/platform-object-tenancy.ts @@ -3,6 +3,26 @@ /** * [#13491] Per-object tenancy classification for the platform namespaces. * + * ## ⚠️ What still reads this ledger (ADR-0131 C8 stage 8b) + * + * Under the `single` posture the ledger no longer decides any write: ADR-0131 + * D9 made the system-insert rule structural there — a platform object with a + * tenant column is in scope like any other, so its organization-less system + * insert takes the Default Organization or is refused + * (`Engine.resolveSystemInsertOrganization`). Two readers remain, and they + * are why the ledger has not retired yet (D13): + * + * 1. the system-insert exemption under a WALLED posture (`group` / + * `isolated`), which stands in 18.0 by epic ruling record 6259899486 on + * #15194, item 4 — the walled half of the lift lands in 18.x; + * 2. the engine's `bypassTenantAudit` isSystem mute + * (`Engine.buildDriverOptions`), a diagnostics-only reading in every + * posture that changes nothing a write touches. + * + * ⛔ Do not add an entry to make a `single` write behave: under `single` an + * entry changes nothing but the mute. The ledger retires with its walled + * reader. + * * ## The ruling this implements * * Maintainer, 2026-08-31 (总监席第 5 场, 联案 #13491 + #13497, verbatim 「同意」): @@ -78,10 +98,11 @@ * ## What admission actually changes, per object * * A `tenant-scoped` classification lets the object reach - * `resolveSystemInsertOrganization` (#8844): on a `single` posture with exactly - * one organization the write DERIVES it, and on a walled posture - * (`group` / `isolated`) an org-less write is REFUSED loudly - * ({@link SystemWriteOrganizationRequiredError}). It also stops the engine + * `resolveSystemInsertOrganization` (#8844) on a walled posture + * (`group` / `isolated`), where an org-less write is REFUSED loudly + * ({@link SystemWriteOrganizationRequiredError}). Under `single` every + * platform object with a tenant column reaches it whatever its entry says + * (stage 8b, above). It also stops the engine * auto-muting the driver's tenant-audit warning for elevated writes on it. * Both directions are the ruled one — a refusal or a warning, never a silent * rewrite of what the write touches (execution point 3). diff --git a/packages/objectql/src/tenancy/system-write-organization.ts b/packages/objectql/src/tenancy/system-write-organization.ts index 8a1c6000ac0..7b0ebb0dcde 100644 --- a/packages/objectql/src/tenancy/system-write-organization.ts +++ b/packages/objectql/src/tenancy/system-write-organization.ts @@ -71,8 +71,11 @@ * org-less rows is `tenancy: { enabled: false }` — a metadata declaration, * loud and checkable — never a per-write bypass flag, which is exactly the * lenient-consumer accommodation Prime Directive #12 forbids. - * - **Platform-namespace objects the inventory did not admit** ({@link - * isPlatformObjectOutOfTenantAuditScope}, `platform-object-tenancy.ts`). + * - **Under a WALLED posture only, platform-namespace objects the inventory + * did not admit** ({@link isPlatformObjectOutOfTenantAuditScope}, + * `platform-object-tenancy.ts`). Under `single` this exclusion is gone + * (ADR-0131 C8 stage 8b): a platform object with a tenant column takes the + * Default Organization or is refused, like every other object. * * ⚠️ This exclusion used to be the whole `sys_` / `cloud_` / `ai_` * NAMESPACE, on the reasoning commit ff08691e6 recorded, that "an org-less row is defensible for @@ -84,8 +87,9 @@ * none by this control. So the exclusion is now a per-object verdict, and an * object nobody has adjudicated stays excluded and stays ON THE LIST. * - * ⚠️ This per-object exclusion STAYS until ADR-0131 C8, which retires it - * together with the ledger (D13); C1 withdrew only the seed loader's + * ⚠️ C8 stage 8b lifted this exclusion under `single` only. Under a wall it + * stands in 18.0 (epic ruling record 6259899486 on #15194, item 4) and + * retires with the ledger in 18.x (D13). C1 withdrew the seed loader's * namespace exemption. #8686's backfill (`seed-tenancy-backfill.ts`) still * carries the NAMESPACE form; its residue attribution is C7's. Read the * three together before changing any of them. @@ -156,7 +160,8 @@ export const DEFAULT_TENANT_FIELD = 'organization_id'; * #13491's re-ruling (2026-08-31, 联案 #13497) withdrew the wholesale namespace * reading — but its scope was the RUNTIME control only. So today: * - * - **the runtime write path** cuts PER OBJECT, by the hand-adjudicated + * - **the runtime write path** no longer cuts under `single` (C8 stage 8b). + * Under a walled posture it cuts PER OBJECT, by the hand-adjudicated * classification in `platform-object-tenancy.ts` — the * `PLATFORM_OBJECT_TENANCY` ledger, read through * {@link classifyPlatformObjectTenancy} and From 5cddf4fbc2b63f424e7d5c8555ff0dc869350aaa Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 05:25:53 +0000 Subject: [PATCH 04/11] =?UTF-8?q?chore(changeset):=20C8=20stage=208b=20?= =?UTF-8?q?=E2=80=94=20single-posture=20platform-object=20system=20inserts?= =?UTF-8?q?=20stamped=20or=20refused?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01AZMXDVkiaxSpJkGXACuVwr Co-authored-by: Claude --- ...15212-8b-single-platform-insert-stamped.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .changeset/15212-8b-single-platform-insert-stamped.md diff --git a/.changeset/15212-8b-single-platform-insert-stamped.md b/.changeset/15212-8b-single-platform-insert-stamped.md new file mode 100644 index 00000000000..f70c659c59d --- /dev/null +++ b/.changeset/15212-8b-single-platform-insert-stamped.md @@ -0,0 +1,19 @@ +--- +'@objectstack/objectql': minor +--- + +feat(objectql)!: under the `single` posture, a system insert into a platform object that names no organization takes the Default Organization, or is refused; it no longer lands with `organization_id` NULL + +Clause-②: no (narrowing) + + + +**BREAKING** accept-set narrowing, shipped as `minor` under the repo's launch-window convention for breaking changes (ADR-0131 D9, D11). + +- **Before.** A system-context insert (`isSystem`, no `tenantId`) into a `sys_` / `cloud_` / `ai_` object that carries an organization column, but that the tenancy inventory had not admitted, skipped the organization rule entirely, under every posture. The row landed with `organization_id` NULL. Thirty-five platform objects in this repository were in that population, among them `sys_business_unit`, `sys_business_unit_member`, `sys_member`, `sys_team`, `sys_user_permission_set`, `sys_user_position`, `sys_sharing_rule`, `sys_share_link`, `sys_comment`, `sys_attachment`, `sys_activity`, `sys_email`, `sys_inbox_message`, `sys_secret`, `sys_setting`, `sys_setting_audit`, `sys_webhook` and `sys_flow_credential`. +- **Now, under `single`.** That insert goes through the same decision every application object already gets. With exactly one organization (the Default Organization), the row is stamped with it. With none, or with several, the insert is refused with `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` (status 500), and the message names the missing organization. Nothing is written. +- **Unchanged.** Under a walled posture (`group` / `isolated`) the per-object exclusion stands as before; the walled half of this change ships in a later 18.x release. A write that carries an organization, on the execution context or on the record, is never re-stamped or refused. Objects with no organization column, objects declaring `tenancy: { enabled: false }`, and federated objects are outside the rule. Updates are untouched. The engine's diagnostics-only tenant-audit mute is unchanged. + +**FROM → TO.** A system insert into an un-admitted platform object under `single`: FROM `organization_id` NULL, silently, TO the Default Organization's id, or a refusal when the install holds zero or several organizations. + +**The one-line fix.** If a refusal names your insert, carry the organization on the write: `ql.insert(object, { ...row, organization_id: orgId }, { context: { isSystem: true } })`, or `{ context: { isSystem: true, tenantId: orgId } }`. On a fresh `single` deployment the auth plugin creates the Default Organization at boot, so a refusal reading `no-organization` means the write ran before it, or in a composition without the auth plugin's identity objects. From 9c99c53a47379122b4f6b777cfb562758f47a93f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 06:21:35 +0000 Subject: [PATCH 05/11] test: fixtures 8b breaks declare sys_organization as the platform does, or carry their organization Claude-Session: https://claude.ai/code/session_01AZMXDVkiaxSpJkGXACuVwr Co-authored-by: Claude --- .../src/identity-objects-feeds-off.test.ts | 6 +- ...nsert-default-organization.dogfood.test.ts | 106 ++++++++++++++++++ ...nancy-autonumber-split.integration.test.ts | 11 +- ...nancy-autonumber-split.integration.test.ts | 6 +- ...nization-follows-event.integration.test.ts | 7 +- 5 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 packages/qa/dogfood/test/showcase-single-platform-insert-default-organization.dogfood.test.ts diff --git a/packages/plugins/plugin-audit/src/identity-objects-feeds-off.test.ts b/packages/plugins/plugin-audit/src/identity-objects-feeds-off.test.ts index 4ac58e197c5..87568ee8a8b 100644 --- a/packages/plugins/plugin-audit/src/identity-objects-feeds-off.test.ts +++ b/packages/plugins/plugin-audit/src/identity-objects-feeds-off.test.ts @@ -236,7 +236,11 @@ describe('Setup identity objects declare enable.feeds: false — a comment creat const { engine, storeFor } = await boot(); await expect( - engine.insert('sys_comment', { thread_id: 'crm_account:acc-1', body: 'hello' } as any), + // [C8 stage 8b] The comment carries its organization: under `single` an + // organization-less system insert into a platform object is refused when + // no organization exists, and this harness registers the organization + // object with none of its rows. + engine.insert('sys_comment', { thread_id: 'crm_account:acc-1', body: 'hello', organization_id: 'org_fixture' } as any), ).resolves.toBeTruthy(); expect(Array.from(storeFor('sys_comment').values()).map((r) => r.thread_id)).toEqual(['crm_account:acc-1']); diff --git a/packages/qa/dogfood/test/showcase-single-platform-insert-default-organization.dogfood.test.ts b/packages/qa/dogfood/test/showcase-single-platform-insert-default-organization.dogfood.test.ts new file mode 100644 index 00000000000..64fa7b76a43 --- /dev/null +++ b/packages/qa/dogfood/test/showcase-single-platform-insert-default-organization.dogfood.test.ts @@ -0,0 +1,106 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// [ADR-0131 D9 / D11, C8 stage 8b] Under the `single` posture no platform +// object is exempt from the system-insert organization rule. +// +// Until this stage, `Engine.resolveSystemInsertOrganization` returned before it +// read the posture for every platform object (`sys_` / `cloud_` / `ai_`) with a +// tenant column that the #13491 inventory had not admitted, so a system insert +// that named no organization landed with `organization_id` NULL — under every +// posture. `sys_business_unit_member` was the measured instance (#14570): a +// bare `isSystem` membership insert stored NULL, and a strict organization +// screen then resolved that member to nobody. +// +// This file pins the lift on the real showcase composition, the one the +// operator boots: +// 1. a fresh `single` boot, the seeded admin's sign-in and an invitation +// leave NO organization-less row in any platform object with a tenant +// column — so no boot-time producer writes before the Default +// Organization exists, or writes around it; +// 2. a bare `isSystem` insert into un-admitted platform objects takes the +// Default Organization; +// 3. with a second organization present the same insert is REFUSED, loudly, +// and nothing is written — the refusal half of D9, never a guess. +// +// The walled half stays as it was in 18.0 (epic ruling record 6259899486 on +// #15194, item 4): the unit pins in `@objectstack/objectql` hold that control. + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { type VerifyStack } from '@objectstack/verify'; +import { resolveTenantFieldName } from '@objectstack/objectql'; +import { bootShowcase } from './showcase-boot.js'; + +const SYS = { isSystem: true } as const; +const PLATFORM_PREFIX = /^(sys_|cloud_|ai_)/; + +describe('showcase (single): a platform object system insert takes the Default Organization or is refused (C8 stage 8b)', () => { + let stack: VerifyStack; + let ql: any; + let orgId: string; + let userId: string; + + beforeAll(async () => { + stack = await bootShowcase(); + const adminToken = await stack.signIn(); + ql = await stack.kernel.getServiceAsync('objectql'); + const tenancy = await stack.kernel.getServiceAsync<{ defaultOrgId(): Promise }>('tenancy'); + const resolved = await tenancy.defaultOrgId(); + if (!resolved) throw new Error('fixture: the showcase boot has no Default Organization'); + orgId = resolved; + // An identity-mail producer and the invitation row, through the public door. + const invite = await stack.apiAs(adminToken, 'POST', '/auth/organization/invite-member', { + email: 'c8-8b-invitee@verify.test', + role: 'member', + }); + expect(invite.status, 'the admin invites a member').toBeLessThan(300); + userId = (await ql.findOne('sys_user', { where: { email: 'admin@objectos.ai' }, context: SYS }))?.id; + if (!userId) throw new Error('fixture: the seeded admin has no sys_user row'); + }); + + afterAll(async () => { await stack?.stop(); }); + + it('boot, sign-in and an invitation leave no organization-less row in any platform object with a tenant column', async () => { + const offenders: string[] = []; + let inScope = 0; + for (const def of ql.registry.getAllObjects() as Array<{ name: string }>) { + if (!PLATFORM_PREFIX.test(def.name)) continue; + const tenantField = resolveTenantFieldName(ql.registry.getObject(def.name)); + if (!tenantField) continue; + inScope++; + const rows = await ql.find(def.name, { + where: { [tenantField]: null }, + fields: ['id'], + limit: 3, + context: SYS, + }); + if (rows.length > 0) offenders.push(`${def.name} (${rows.length}+)`); + } + // The non-vacuity half: the walk really visited the in-reach population. + expect(inScope).toBeGreaterThan(30); + expect(offenders).toEqual([]); + }); + + it('a bare isSystem insert into an un-admitted platform object takes the Default Organization (#14570)', async () => { + await ql.insert('sys_business_unit', { id: 'bu_c8_8b', name: 'C8 8b', kind: 'department', active: true }, { context: SYS }); + await ql.insert('sys_business_unit_member', { id: 'bm_c8_8b', business_unit_id: 'bu_c8_8b', user_id: userId }, { context: SYS }); + const unit = await ql.findOne('sys_business_unit', { where: { id: 'bu_c8_8b' }, context: SYS }); + const member = await ql.findOne('sys_business_unit_member', { where: { id: 'bm_c8_8b' }, context: SYS }); + expect(unit?.organization_id).toBe(orgId); + expect(member?.organization_id).toBe(orgId); + }); + + it('with a second organization the same insert is REFUSED, loudly, and nothing is written', async () => { + await ql.insert('sys_organization', { id: 'org_c8_8b_second', name: 'Second', slug: 'c8-8b-second' }, { context: SYS }); + await expect( + ql.insert('sys_business_unit_member', { id: 'bm_c8_8b_refused', business_unit_id: 'bu_c8_8b', user_id: userId }, { context: SYS }), + ).rejects.toMatchObject({ + code: 'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED', + status: 500, + reason: 'ambiguous-organization', + }); + expect(await ql.findOne('sys_business_unit_member', { where: { id: 'bm_c8_8b_refused' }, context: SYS })).toBeFalsy(); + // Carrying the organization is the remedy the refusal names, and it lands. + await ql.insert('sys_business_unit_member', { id: 'bm_c8_8b_carried', business_unit_id: 'bu_c8_8b', user_id: userId, organization_id: orgId }, { context: SYS }); + expect((await ql.findOne('sys_business_unit_member', { where: { id: 'bm_c8_8b_carried' }, context: SYS }))?.organization_id).toBe(orgId); + }); +}); diff --git a/packages/runtime/src/seed-tenancy-autonumber-split.integration.test.ts b/packages/runtime/src/seed-tenancy-autonumber-split.integration.test.ts index 62a4d033bdd..0442ee042cd 100644 --- a/packages/runtime/src/seed-tenancy-autonumber-split.integration.test.ts +++ b/packages/runtime/src/seed-tenancy-autonumber-split.integration.test.ts @@ -83,7 +83,11 @@ const CASE_OBJECT = { }, } as any; -const ORG_OBJECT = { name: 'sys_organization', fields: { name: { type: 'text' } } } as any; +// [C8 stage 8b] Declared as the platform declares it (`sys-organization.object.ts`): +// better-auth owns the table, so the registration injects no organization column. +// A bare schema would gain one, and under `single` the first organization's own +// insert would then be refused for want of an organization. +const ORG_OBJECT = { name: 'sys_organization', managedBy: 'better-auth', fields: { name: { type: 'text' } } } as any; function createMetadata(objects: any[]) { const byName: Record = Object.fromEntries(objects.map((o) => [o.name, o])); @@ -548,8 +552,11 @@ describe('#8686 seed/API tenancy split — autonumber scope', () => { await driver.initObjects([sysObject, ORG_OBJECT]); // Land untenanted platform rows, then bring an organization into existence. + // [C8 stage 8b] Straight through the driver, as `landPreC1SeedResidue` does: + // under `single` the engine no longer writes an organization-less platform + // row, so these are the residue an older release left behind. for (let i = 0; i < 3; i++) { - await engine.insert('sys_audit_entry', { subject: `e${i}` }, { context: { isSystem: true } } as any); + await (driver as any).create('sys_audit_entry', { subject: `e${i}` }); } await createOrganization(engine); expect(await readSequences(driver)).toEqual([{ tenant: GLOBAL_TENANT, lastValue: 3 }]); diff --git a/packages/runtime/src/system-write-tenancy-autonumber-split.integration.test.ts b/packages/runtime/src/system-write-tenancy-autonumber-split.integration.test.ts index e5fd7ed7133..9b655d4fc51 100644 --- a/packages/runtime/src/system-write-tenancy-autonumber-split.integration.test.ts +++ b/packages/runtime/src/system-write-tenancy-autonumber-split.integration.test.ts @@ -68,7 +68,11 @@ const DISPATCH_ORDER = { }, } as any; -const ORG_OBJECT = { name: 'sys_organization', fields: { name: { type: 'text' } } } as any; +// [C8 stage 8b] Declared as the platform declares it (`sys-organization.object.ts`): +// better-auth owns the table, so the registration injects no organization column. +// A bare schema would gain one, and under `single` the first organization's own +// insert would then be refused for want of an organization. +const ORG_OBJECT = { name: 'sys_organization', managedBy: 'better-auth', fields: { name: { type: 'text' } } } as any; const openDrivers: SqlDriver[] = []; diff --git a/packages/services/service-messaging/src/notification-organization-follows-event.integration.test.ts b/packages/services/service-messaging/src/notification-organization-follows-event.integration.test.ts index d4d19d0b2d3..d69dc53c460 100644 --- a/packages/services/service-messaging/src/notification-organization-follows-event.integration.test.ts +++ b/packages/services/service-messaging/src/notification-organization-follows-event.integration.test.ts @@ -45,8 +45,11 @@ import { InboxMessage, NotificationDelivery, NotificationPreference, Notificatio const SYSTEM = { context: { isSystem: true } as ExecutionContext }; -const SYS_ORGANIZATION = { name: 'sys_organization', label: 'Organization', fields: { name: { type: 'text', label: 'Name' } } }; -const SYS_USER = { name: 'sys_user', label: 'User', fields: { name: { type: 'text', label: 'Name' } } }; +// [C8 stage 8b] Declared as the platform declares them: better-auth owns both tables, +// so the registration injects no organization column. A bare schema would gain one, +// and under `single` the first organization's own insert would then be refused. +const SYS_ORGANIZATION = { name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } }; +const SYS_USER = { name: 'sys_user', label: 'User', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } }; const silentLogger: any = { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }; silentLogger.child = () => silentLogger; From 9f0c026f8ec56ce339fa59935f4f0cd97bddd2c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 06:38:43 +0000 Subject: [PATCH 06/11] test: fixtures 8b breaks land organization-less legacy rows beneath the engine, or carry an organization Under single the engine now stamps or refuses an organization-less platform insert. Fixtures that model rows an older release left (global grants, the retired catalogue bucket, unscoped holders) write them through the driver; fixtures that only needed an owner carry one. Claude-Session: https://claude.ai/code/session_01AZMXDVkiaxSpJkGXACuVwr Co-authored-by: Claude --- .../status-mirror-cascade.integration.test.ts | 4 +++ .../__tests__/legacy-catalog-rows.testkit.ts | 12 ++++++-- .../src/credential-at-rest-posture.test.ts | 15 ++++++++++ .../driver-fault-auth-log-carriers.test.ts | 6 ++-- ...ipal-less-producers-system-context.test.ts | 4 ++- .../scim-deactivation-reconcile-user.test.ts | 7 +++++ .../src/scim-transaction-scope.test.ts | 7 +++++ .../signup-existing-address-refusal.test.ts | 6 ++-- .../sso-register-platform-admin-gate.test.ts | 29 +++++++++---------- .../__tests__/legacy-catalog-rows.testkit.ts | 12 ++++++-- ...latform-admin-existing-holder-scan.test.ts | 29 ++++++++++++------- .../src/builtin-positions.boot.test.ts | 14 +++++++-- .../src/catalog-reference-report.test.ts | 6 +++- ...lled-by-parent-chain-middle-master.test.ts | 11 +++++-- .../src/explain-asker-gate.test.ts | 21 +++++++++----- .../src/explain-enforce-parity.test.ts | 14 +++++++-- .../src/explain-principal-person.test.ts | 17 +++++++---- .../explain-removed-member-principal.test.ts | 8 +++-- .../src/granted-by-writer-provenance.test.ts | 10 +++++-- .../src/orgless-position-name-fold.test.ts | 25 ++++++++++++---- .../src/sys-audit-log-row-scope.test.ts | 5 +++- 21 files changed, 193 insertions(+), 69 deletions(-) diff --git a/packages/plugins/plugin-approvals/src/status-mirror-cascade.integration.test.ts b/packages/plugins/plugin-approvals/src/status-mirror-cascade.integration.test.ts index 70d65c16188..76f82cfa899 100644 --- a/packages/plugins/plugin-approvals/src/status-mirror-cascade.integration.test.ts +++ b/packages/plugins/plugin-approvals/src/status-mirror-cascade.integration.test.ts @@ -327,6 +327,10 @@ describe('an approval decision cascades as the deciding user (#3783)', () => { { name: 'sys_organization', label: 'Organization', + // [C8 stage 8b] better-auth owns the table, as `sys-organization.object.ts` + // declares: no injected organization column, so the Default Organization's + // own insert below is not judged as an organization-less platform write. + managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' }, slug: { type: 'text', label: 'Slug' } }, } as any, '@objectstack/plugin-auth', diff --git a/packages/plugins/plugin-auth/src/__tests__/legacy-catalog-rows.testkit.ts b/packages/plugins/plugin-auth/src/__tests__/legacy-catalog-rows.testkit.ts index 2da6b0c4017..29b15161ae6 100644 --- a/packages/plugins/plugin-auth/src/__tests__/legacy-catalog-rows.testkit.ts +++ b/packages/plugins/plugin-auth/src/__tests__/legacy-catalog-rows.testkit.ts @@ -41,7 +41,15 @@ export async function seedLegacyPlatformBucket( continue; } const id = legacyBucketRowId(ps.name); - await engine.insert('sys_permission_set', { + // [C8 stage 8b] Written beneath the engine when it has a driver to reach: under + // `single` the engine stamps an organization-less platform insert with the + // Default Organization (ADR-0131 D9), and these rows ARE the organization-less + // residue an earlier release left. An engine double without a driver keeps + // the engine write, where nothing stamps it. + const driver = (engine as { getDriver?: (object: string) => any }).getDriver?.('sys_permission_set'); + const write = (row: Record) => + driver ? driver.create('sys_permission_set', row) : engine.insert('sys_permission_set', row, { context: SYS }); + await write({ id, name: ps.name, label: ps.label ?? ps.name, @@ -54,7 +62,7 @@ export async function seedLegacyPlatformBucket( admin_scope: (ps as any).adminScope ? JSON.stringify((ps as any).adminScope) : null, active: true, managed_by: 'platform', - }, { context: SYS }); + }); ids[ps.name] = id; } return ids; diff --git a/packages/plugins/plugin-auth/src/credential-at-rest-posture.test.ts b/packages/plugins/plugin-auth/src/credential-at-rest-posture.test.ts index 49d3ac4d542..1d22d573dc4 100644 --- a/packages/plugins/plugin-auth/src/credential-at-rest-posture.test.ts +++ b/packages/plugins/plugin-auth/src/credential-at-rest-posture.test.ts @@ -232,8 +232,15 @@ describe('[#8192/#3653] sys_scim_connection_credential.token_digest is a keyed o const manager = makeManager(engine); const send = (request: Request) => manager.handleRequest(request); + // [C8 stage 8b] It carries an organization: under `single` an organization-less + + // system insert is refused while the install holds none, and this rig holds none. + + // The verifier reads the digest alone, so the organization changes nothing it answers. + const { token } = await mintScimConnectionCredential(engine as never, SECRET, { connectionId: 'okta-probe', + organizationId: 'org_scim_8b', }); const ok = await send( @@ -267,8 +274,12 @@ describe('[#8192/#3653] sys_scim_connection_credential.token_digest is a keyed o const send = (request: Request) => manager.handleRequest(request); // Revoked: minted, then switched inactive — refused. + // [C8 stage 8b] It carries an organization: under `single` an organization-less + // system insert is refused while the install holds none, and this rig holds none. + // The verifier reads the digest alone, so the organization changes nothing it answers. const revoked = await mintScimConnectionCredential(engine as never, SECRET, { connectionId: 'okta-revoked', + organizationId: 'org_scim_8b', }); await (engine as unknown as { update(o: string, data: Record): Promise; @@ -283,8 +294,12 @@ describe('[#8192/#3653] sys_scim_connection_credential.token_digest is a keyed o expect(deniedRevoked.status).toBe(401); // Expired: minted with a past expiry — refused. + // [C8 stage 8b] It carries an organization: under `single` an organization-less + // system insert is refused while the install holds none, and this rig holds none. + // The verifier reads the digest alone, so the organization changes nothing it answers. const expired = await mintScimConnectionCredential(engine as never, SECRET, { connectionId: 'okta-expired', + organizationId: 'org_scim_8b', expiresAt: new Date(Date.now() - 60_000), }); const deniedExpired = await send( diff --git a/packages/plugins/plugin-auth/src/driver-fault-auth-log-carriers.test.ts b/packages/plugins/plugin-auth/src/driver-fault-auth-log-carriers.test.ts index 53118354f17..5c8d48ce2b5 100644 --- a/packages/plugins/plugin-auth/src/driver-fault-auth-log-carriers.test.ts +++ b/packages/plugins/plugin-auth/src/driver-fault-auth-log-carriers.test.ts @@ -257,10 +257,12 @@ for (const cell of CELLS) { // below will meet in the DATABASE — not in any check of ours. await engine.insert('sys_user', { id: 'usr_holder', email: 'holder@corp.example', name: S }, SYSTEM); await driver.getKnex().raw(`create unique index sys_user_name_forced_unique on sys_user (${cell.nameKey})`); - await engine.insert( + // [C8 stage 8b] A legacy organization-less catalogue row, landed beneath the + // engine: under `single` the engine refuses an organization-less platform + // insert while the install holds no organization (ADR-0131 D9). + await (engine as any).getDriver('sys_permission_set').create( 'sys_permission_set', { id: 'ps_member_default', name: 'member_default', label: 'member_default', active: true }, - SYSTEM, ); for (const email of ['first@corp.example', 'second@corp.example']) { await engine.insert( diff --git a/packages/plugins/plugin-auth/src/principal-less-producers-system-context.test.ts b/packages/plugins/plugin-auth/src/principal-less-producers-system-context.test.ts index 034f8adb2ee..399526c8a71 100644 --- a/packages/plugins/plugin-auth/src/principal-less-producers-system-context.test.ts +++ b/packages/plugins/plugin-auth/src/principal-less-producers-system-context.test.ts @@ -182,7 +182,9 @@ describe('SCIM bearer verification — the credential probe carries the system o const { engine, seen } = await bootEngine(); await engine.insert( SCIM_CREDENTIAL_OBJECT, - { connection_id: 'conn-pin', token_digest: digestScimBearerToken(SECRET, TOKEN), active: true }, + // [C8 stage 8b] The row carries an organization: under `single` an organization-less + // system insert is refused while the install holds none, and this rig holds none. + { connection_id: 'conn-pin', token_digest: digestScimBearerToken(SECRET, TOKEN), active: true, organization_id: 'org_scim_8b' }, SYSTEM, ); seen.length = 0; diff --git a/packages/plugins/plugin-auth/src/scim-deactivation-reconcile-user.test.ts b/packages/plugins/plugin-auth/src/scim-deactivation-reconcile-user.test.ts index 0fbcf875db0..fa5ec7facea 100644 --- a/packages/plugins/plugin-auth/src/scim-deactivation-reconcile-user.test.ts +++ b/packages/plugins/plugin-auth/src/scim-deactivation-reconcile-user.test.ts @@ -210,8 +210,15 @@ async function boot(): Promise { plugins: { scim: true, organization: true }, } as never); + // [C8 stage 8b] It carries an organization: under `single` an organization-less + + // system insert is refused while the install holds none, and this rig holds none. + + // The verifier reads the digest alone, so the organization changes nothing it answers. + const { token } = await mintScimConnectionCredential(engine as never, SECRET, { connectionId: 'okta-14360', + organizationId: 'org_scim_8b', }); return { engine, manager, token, send: (request) => manager.handleRequest(request) }; diff --git a/packages/plugins/plugin-auth/src/scim-transaction-scope.test.ts b/packages/plugins/plugin-auth/src/scim-transaction-scope.test.ts index ee68073bd91..6587c608fc9 100644 --- a/packages/plugins/plugin-auth/src/scim-transaction-scope.test.ts +++ b/packages/plugins/plugin-auth/src/scim-transaction-scope.test.ts @@ -129,8 +129,15 @@ async function boot(): Promise { plugins: { scim: true, organization: true }, } as never); + // [C8 stage 8b] It carries an organization: under `single` an organization-less + + // system insert is refused while the install holds none, and this rig holds none. + + // The verifier reads the digest alone, so the organization changes nothing it answers. + const { token } = await mintScimConnectionCredential(engine as never, SECRET, { connectionId: 'okta-14522', + organizationId: 'org_scim_8b', }); return { engine, driver, manager, token, send: (request) => manager.handleRequest(request) }; diff --git a/packages/plugins/plugin-auth/src/signup-existing-address-refusal.test.ts b/packages/plugins/plugin-auth/src/signup-existing-address-refusal.test.ts index c71a42d90f7..514040ce296 100644 --- a/packages/plugins/plugin-auth/src/signup-existing-address-refusal.test.ts +++ b/packages/plugins/plugin-auth/src/signup-existing-address-refusal.test.ts @@ -171,10 +171,12 @@ async function seedPopulation(engine: ObjectQL): Promise { ] as const) { await engine.insert('sys_user', { id, email, name: id }, SYSTEM); } - await engine.insert( + // [C8 stage 8b] A legacy organization-less catalogue row, landed beneath the engine: + // under `single` the engine refuses an organization-less platform insert while the + // install holds no organization (ADR-0131 D9), and this rig holds none. + await (engine as any).getDriver('sys_permission_set').create( 'sys_permission_set', { id: 'ps_member_default', name: 'member_default', label: 'member_default', active: true }, - SYSTEM, ); } diff --git a/packages/plugins/plugin-auth/src/sso-register-platform-admin-gate.test.ts b/packages/plugins/plugin-auth/src/sso-register-platform-admin-gate.test.ts index 9c39d82ef65..46b4de6031e 100644 --- a/packages/plugins/plugin-auth/src/sso-register-platform-admin-gate.test.ts +++ b/packages/plugins/plugin-auth/src/sso-register-platform-admin-gate.test.ts @@ -251,22 +251,19 @@ async function createOrg( /** Grant platform admin the ADR-0068 D2 way: an ORG-LESS `admin_full_access` link. */ async function grantPlatformAdmin(engine: ObjectQL, userId: string): Promise { - await engine.insert( - 'sys_permission_set', - { id: 'ps_admin_full_access', name: ADMIN_FULL_ACCESS }, - SYSTEM as never, - ); - await engine.insert( - 'sys_user_permission_set', - { - id: 'ups_platform_admin', - user_id: userId, - permission_set_id: 'ps_admin_full_access', - permission_set: 'admin_full_access', - organization_id: null, - }, - SYSTEM as never, - ); + // [C8 stage 8b] The legacy organization-less catalogue row and grant, landed + // beneath the engine: under `single` the engine refuses an organization-less + // platform insert while the install holds no organization (ADR-0131 D9). + const residue = (object: string, row: Record) => + (engine as any).getDriver(object).create(object, row); + await residue('sys_permission_set', { id: 'ps_admin_full_access', name: ADMIN_FULL_ACCESS }); + await residue('sys_user_permission_set', { + id: 'ups_platform_admin', + user_id: userId, + permission_set_id: 'ps_admin_full_access', + permission_set: 'admin_full_access', + organization_id: null, + }); } async function userIdOf(engine: ObjectQL, email: string): Promise { diff --git a/packages/plugins/plugin-security/src/__tests__/legacy-catalog-rows.testkit.ts b/packages/plugins/plugin-security/src/__tests__/legacy-catalog-rows.testkit.ts index f73aed75f95..bf1fe8cf5ec 100644 --- a/packages/plugins/plugin-security/src/__tests__/legacy-catalog-rows.testkit.ts +++ b/packages/plugins/plugin-security/src/__tests__/legacy-catalog-rows.testkit.ts @@ -38,7 +38,15 @@ export async function seedLegacyPlatformBucket( continue; } const id = legacyBucketRowId(ps.name); - await engine.insert('sys_permission_set', { + // [C8 stage 8b] Written beneath the engine when it has a driver to reach: under + // `single` the engine stamps an organization-less platform insert with the + // Default Organization (ADR-0131 D9), and these rows ARE the organization-less + // residue an earlier release left. An engine double without a driver keeps + // the engine write, where nothing stamps it. + const driver = (engine as { getDriver?: (object: string) => any }).getDriver?.('sys_permission_set'); + const write = (row: Record) => + driver ? driver.create('sys_permission_set', row) : engine.insert('sys_permission_set', row, { context: SYS }); + await write({ id, name: ps.name, label: ps.label ?? ps.name, @@ -51,7 +59,7 @@ export async function seedLegacyPlatformBucket( admin_scope: (ps as any).adminScope ? JSON.stringify((ps as any).adminScope) : null, active: true, managed_by: 'platform', - }, { context: SYS }); + }); ids[ps.name] = id; } return ids; diff --git a/packages/plugins/plugin-security/src/bootstrap-platform-admin-existing-holder-scan.test.ts b/packages/plugins/plugin-security/src/bootstrap-platform-admin-existing-holder-scan.test.ts index 3f2a5a42558..cbf1a3b10b9 100644 --- a/packages/plugins/plugin-security/src/bootstrap-platform-admin-existing-holder-scan.test.ts +++ b/packages/plugins/plugin-security/src/bootstrap-platform-admin-existing-holder-scan.test.ts @@ -148,11 +148,14 @@ type NaturalOrder = (typeof NATURAL_ORDERS)[number]; * query is forwarded verbatim and the RESULT is returned untouched. The facade * never sorts, so everything the fix relies on is done by the real SQL engine. */ +/** [C8 stage 8b] The real engine behind a {@link withNaturalOrder} facade, for landing residue. */ +const ENGINE_OF = new WeakMap(); + function withNaturalOrder(engine: ObjectQL, order: NaturalOrder): any { const insertionRank = new Map(); let nextRank = 0; const rankKey = (object: string, id: unknown) => `${object}:${String(id)}`; - return { + const facade = { async find(object: string, query: any, options: any) { const rows = await (engine as any).find(object, query, options); if (!Array.isArray(rows)) return rows; @@ -178,6 +181,8 @@ function withNaturalOrder(engine: ObjectQL, order: NaturalOrder): any { return (engine as any).update(object, data, options); }, }; + ENGINE_OF.set(facade, engine); + return facade; } // ─────────────────────────────────────────────────────────────────────────── @@ -236,16 +241,18 @@ async function seedTenant( // The founder — this deployment's platform admin, and the row the guard has // to find. Inserted FIRST so the insertion-order family puts it in row 1. await seedUser(ql, 'usr_founder', 'founder@tenant.example', '2026-01-01T00:00:00.000Z', true); - await ql.insert( - 'sys_user_permission_set', - { - id: 'ups_zzz_founder', - user_id: 'usr_founder', - permission_set: 'admin_full_access', - organization_id: unscopedOrganizationId, - }, - { context: SYSTEM_CTX }, - ); + // [C8 stage 8b] The unscoped grant is legacy residue, landed straight through + // the driver: under `single` the engine stamps an organization-less platform + // insert with the Default Organization (ADR-0131 D9), which is where 8b-0a + // already puts a new promotion's grant. An unranked row sorts first in the + // insertion-order family, as it did when it was inserted first. + const real: any = ENGINE_OF.get(ql) ?? ql; + await real.getDriver('sys_user_permission_set').create('sys_user_permission_set', { + id: 'ups_zzz_founder', + user_id: 'usr_founder', + permission_set: 'admin_full_access', + organization_id: unscopedOrganizationId, + }); for (let i = 1; i <= orgGrants; i++) { const n = String(i).padStart(3, '0'); diff --git a/packages/plugins/plugin-security/src/builtin-positions.boot.test.ts b/packages/plugins/plugin-security/src/builtin-positions.boot.test.ts index 4ec9e9c6937..52b7909c936 100644 --- a/packages/plugins/plugin-security/src/builtin-positions.boot.test.ts +++ b/packages/plugins/plugin-security/src/builtin-positions.boot.test.ts @@ -344,7 +344,13 @@ async function boot(scenario: Scenario): Promise { const organizationId = scenario.posture === 'isolated' ? ORG : undefined; const context = organizationId ? { isSystem: true, tenantId: organizationId } : SYS; for (const row of LEGACY_ROWS) { - await insert('sys_position', { id: `pos_${row.name}`, ...row, active: true, is_default: false }, { context } as any); + const stored = { id: `pos_${row.name}`, ...row, active: true, is_default: false }; + // [C8 stage 8b] The organization-less rows go in beneath the engine: under + // `single` the engine stamps such a platform insert with the Default + // Organization (ADR-0131 D9), and these rows ARE the organization-less + // residue an earlier release's seeder left. + if (organizationId) await insert('sys_position', stored, { context } as any); + else await (engine as any).getDriver('sys_position').create('sys_position', { ...stored, organization_id: null }); } } @@ -523,8 +529,12 @@ for (const name of Object.keys(SCENARIOS) as ScenarioName[]) { expect(held.some((h) => h.startsWith('usr_orgadmin -> organization_admin'))).toBe(true); } // Positive control: the census sees a row — one insert reads back as one. + // [C8 stage 8b] Under `single` the organization-less system insert takes + // the sole organization (ADR-0131 D9); under the wall it stays unstamped, + // the exemption 18.0 keeps there. await engine.insert('sys_permission_set', { id: 'ps_census_control', name: 'zz_census_control', label: 'Control' }, { context: SYS } as any); - expect(await permissionSetCensus(engine)).toEqual(['zz_census_control | -']); + const stamped = scenario.posture === 'single' ? (scenario.organizationAtBoot ? ORG : 'org_default') : '-'; + expect(await permissionSetCensus(engine)).toEqual([`zz_census_control | ${stamped}`]); await engine.delete('sys_permission_set', { where: { id: 'ps_census_control' }, context: SYS } as any); expect(await permissionSetCensus(engine)).toEqual([]); }); diff --git a/packages/plugins/plugin-security/src/catalog-reference-report.test.ts b/packages/plugins/plugin-security/src/catalog-reference-report.test.ts index 008f5abb8a0..c2c619a3fef 100644 --- a/packages/plugins/plugin-security/src/catalog-reference-report.test.ts +++ b/packages/plugins/plugin-security/src/catalog-reference-report.test.ts @@ -199,7 +199,11 @@ const orgCtx = (organizationId: string | null) => async function insertRows(b: Booted, object: string, rows: Array>): Promise { for (const row of rows) { const organizationId = (row.organization_id as string | undefined) ?? null; - await b.engine.insert(object, row, { context: orgCtx(organizationId) } as any); + // [C8 stage 8b] An organization-less row is legacy residue, landed straight + // through the driver: under `single` the engine stamps or refuses such a + // platform insert (ADR-0131 D9), and this rig holds two organizations. + if (organizationId === null) await (b.engine as any).getDriver(object).create(object, row); + else await b.engine.insert(object, row, { context: orgCtx(organizationId) } as any); } } diff --git a/packages/plugins/plugin-security/src/controlled-by-parent-chain-middle-master.test.ts b/packages/plugins/plugin-security/src/controlled-by-parent-chain-middle-master.test.ts index d9fc5917528..0db87c56aab 100644 --- a/packages/plugins/plugin-security/src/controlled-by-parent-chain-middle-master.test.ts +++ b/packages/plugins/plugin-security/src/controlled-by-parent-chain-middle-master.test.ts @@ -232,10 +232,17 @@ beforeAll(async () => { ], SYS); await engine.insert('sys_organization', [{ id: ORG, name: 'CBC', slug: 'cbc' }], SYS); await engine.insert('sys_member', [{ id: 'mem_delegator', user_id: DELEGATOR_MEMBER, organization_id: ORG, role: 'member' }], SYS); - await engine.insert('sys_user_position', [ + // [C8 stage 8b] Organization-less position rows are landed straight through the + // driver, as the residue an older release left: under `single` the engine now + // stamps such a system insert with the one organization (ADR-0131 D9), which + // would tie DELEGATOR_UNBOUND's position to an organization it is not a member of. + const positions = (engine as any).getDriver('sys_user_position'); + for (const row of [ { id: 'up_unbound', user_id: DELEGATOR_UNBOUND, position: MANAGER_SET.name }, { id: 'up_member', user_id: DELEGATOR_MEMBER, position: MANAGER_SET.name }, - ], SYS); + ]) { + await positions.create('sys_user_position', row); + } let security: unknown; let sharing: SharingService | undefined; diff --git a/packages/plugins/plugin-security/src/explain-asker-gate.test.ts b/packages/plugins/plugin-security/src/explain-asker-gate.test.ts index a165eb05ce6..5d2ce28d622 100644 --- a/packages/plugins/plugin-security/src/explain-asker-gate.test.ts +++ b/packages/plugins/plugin-security/src/explain-asker-gate.test.ts @@ -169,22 +169,27 @@ beforeAll(async () => { for (const id of [MEMBER, ADMIN.userId, BLIND_ADMIN.userId, DELEGATE.userId, REFUSED_ADMIN.userId, REFUSED_DELEGATE.userId, EDIT_ONLY_USER]) { await e.insert('sys_user', { id, name: id, email: `${id}@example.test` }, SYS); } + // [C8 stage 8b] The grant-side rows below carry no organization: under `single` + // the engine stamps or refuses such a platform insert (ADR-0131 D9), and this rig + // holds no organization, so they are landed straight through the driver, as the + // residue an older release left. The explainer reads them as before. + const residue = (o: string, row: Record) => (engine as any).getDriver(o).create(o, row); // The explained member's grant, as the explainer resolves it: a grant row // pointing at a catalogue row; what the set confers is declared in metadata. - await e.insert('sys_permission_set', { + await residue('sys_permission_set', { id: 'ps_ag_member', name: MEMBER_SET.name, label: MEMBER_SET.name, organization_id: null, managed_by: 'admin', active: true, object_permissions: JSON.stringify({}), system_permissions: JSON.stringify([]), - }, SYS); - await e.insert('sys_user_permission_set', { + }); + await residue('sys_user_permission_set', { id: 'ups_ag_member', user_id: MEMBER, permission_set: MEMBER_SET.name, - }, SYS); - await e.insert('sys_permission_set', { + }); + await residue('sys_permission_set', { id: 'ps_ag_edit_only', name: EDIT_ONLY_SET.name, label: EDIT_ONLY_SET.name, organization_id: null, managed_by: 'admin', active: true, object_permissions: JSON.stringify({}), system_permissions: JSON.stringify([]), - }, SYS); - await e.insert('sys_user_permission_set', { + }); + await residue('sys_user_permission_set', { id: 'ups_ag_edit_only', user_id: EDIT_ONLY_USER, permission_set: EDIT_ONLY_SET.name, - }, SYS); + }); await e.insert(BOARD, [ { id: HIDDEN, name: 'hidden', owner_id: 'usr_ag_owner' }, { id: OPEN, name: 'open', owner_id: 'usr_ag_owner' }, diff --git a/packages/plugins/plugin-security/src/explain-enforce-parity.test.ts b/packages/plugins/plugin-security/src/explain-enforce-parity.test.ts index 4c03c0c9ff0..92c13a3a1f5 100644 --- a/packages/plugins/plugin-security/src/explain-enforce-parity.test.ts +++ b/packages/plugins/plugin-security/src/explain-enforce-parity.test.ts @@ -437,11 +437,19 @@ async function bootPrincipal(source: PostureSource) { await e.insert('sys_member', { id: `mem_admin_${n}`, user_id: USER_ADMIN, organization_id: ALPHA, role: 'member' }, SYS); await e.insert('sys_member', { id: `mem_member_${n}`, user_id: USER_MEMBER, organization_id: ALPHA, role: 'member' }, SYS); await e.insert('sys_member', { id: `mem_removed_${n}`, user_id: USER_REMOVED, organization_id: BETA, role: 'member' }, SYS); - const catalogue = (id: string, name: string, organization: string | null, objects: Record) => - e.insert('sys_permission_set', { + // [C8 stage 8b] A catalogue row with no organization is landed straight through + // the driver, as the residue an older release left: under `single` the engine + // stamps or refuses an organization-less platform insert (ADR-0131 D9), and this + // rig's engine holds two organizations. + const catalogue = (id: string, name: string, organization: string | null, objects: Record) => { + const row = { id, name, label: name, organization_id: organization, managed_by: 'admin', active: true, object_permissions: JSON.stringify(objects), system_permissions: JSON.stringify([]), - }, SYS); + }; + return organization === null + ? (engine as any).getDriver('sys_permission_set').create('sys_permission_set', row) + : e.insert('sys_permission_set', row, SYS); + }; await catalogue(`ps_user_admin_${n}`, 'qa_user_admin', null, {}); await catalogue(`ps_reader_${n}`, READER_SET, null, {}); await catalogue(`ps_alpha_notes_${n}`, ALPHA_ONLY_SET, ALPHA, { [NOTES]: { allowRead: true } }); diff --git a/packages/plugins/plugin-security/src/explain-principal-person.test.ts b/packages/plugins/plugin-security/src/explain-principal-person.test.ts index 2e1d348e0a4..05d0f396ca3 100644 --- a/packages/plugins/plugin-security/src/explain-principal-person.test.ts +++ b/packages/plugins/plugin-security/src/explain-principal-person.test.ts @@ -129,18 +129,23 @@ beforeAll(async () => { ], SYS); // Grace holds the directory too, so an agent acting for her keeps it under // the D10 intersection; the plain user holds nothing beyond the baseline. - await e.insert('sys_permission_set', { + // [C8 stage 8b] The three grant-side rows below carry no organization: under + // `single` the engine stamps or refuses such a platform insert (ADR-0131 D9), + // and this rig holds no organization, so they are landed straight through the + // driver, as the residue an older release left. The explainer reads them as before. + const residue = (o: string, row: Record) => (engine as any).getDriver(o).create(o, row); + await residue('sys_permission_set', { id: 'ps_pp_directory', name: DIRECTORY_SET.name, label: DIRECTORY_SET.name, organization_id: null, managed_by: 'admin', active: true, object_permissions: JSON.stringify({}), system_permissions: JSON.stringify([]), - }, SYS); - await e.insert('sys_user_permission_set', { + }); + await residue('sys_user_permission_set', { id: 'ups_pp_grace', user_id: GRACE, permission_set: DIRECTORY_SET.name, - }, SYS); + }); // A position Lin holds because Grace delegated it, until a date far ahead. - await e.insert('sys_user_position', { + await residue('sys_user_position', { id: 'up_pp_lin_delegated', user_id: LIN, position: DELEGATED_POSITION, delegated_from: GRACE, valid_until: DELEGATED_UNTIL, - }, SYS); + }); await e.insert(BOARD, [{ id: 'b_pp_1', name: 'one' }], SYS); const sets = [MEMBER_DEFAULT, DIRECTORY_SET, MASKED_DIRECTORY_SET, USERS_ONLY_SET]; diff --git a/packages/plugins/plugin-security/src/explain-removed-member-principal.test.ts b/packages/plugins/plugin-security/src/explain-removed-member-principal.test.ts index f88cd400b81..7a62538a884 100644 --- a/packages/plugins/plugin-security/src/explain-removed-member-principal.test.ts +++ b/packages/plugins/plugin-security/src/explain-removed-member-principal.test.ts @@ -146,11 +146,15 @@ async function boot(makeDriver: () => Driver, posture: TenancyPosture) { await e.insert('sys_member', { id: `mem_removed_${n}`, user_id: USER_REMOVED, organization_id: BETA, role: 'member' }, SYS); // The catalogue rows the grant rows point at (the resolver maps id → name); // what each set CONFERS is declared in metadata below. + // [C8 stage 8b] They carry no organization, so they are landed straight + // through the driver, as the residue an older release left: under `single` + // the engine stamps or refuses such a platform insert (ADR-0131 D9), and this + // rig's engine holds two organizations. const catalogue = async (id: string, name: string) => - e.insert('sys_permission_set', { + (engine as any).getDriver('sys_permission_set').create('sys_permission_set', { id, name, label: name, organization_id: null, managed_by: 'admin', active: true, object_permissions: JSON.stringify({}), system_permissions: JSON.stringify([]), - }, SYS); + }); await catalogue(`ps_user_admin_${n}`, 'qa_user_admin'); await catalogue(`ps_probe_${n}`, PROBE_SET); // Grant rows, each scoped to org_alpha. Removing a member deletes their diff --git a/packages/plugins/plugin-security/src/granted-by-writer-provenance.test.ts b/packages/plugins/plugin-security/src/granted-by-writer-provenance.test.ts index fb4faaa6c8c..a55e9bb688a 100644 --- a/packages/plugins/plugin-security/src/granted-by-writer-provenance.test.ts +++ b/packages/plugins/plugin-security/src/granted-by-writer-provenance.test.ts @@ -127,8 +127,10 @@ async function boot(opts: { security: boolean } = { security: true }): Promise ({ id, name: id })), SYS); await engine.insert('sys_business_unit', [ { id: 'bu_hq', name: 'hq', parent_business_unit_id: null }, diff --git a/packages/plugins/plugin-security/src/orgless-position-name-fold.test.ts b/packages/plugins/plugin-security/src/orgless-position-name-fold.test.ts index ac067559b48..b2eb89e8a98 100644 --- a/packages/plugins/plugin-security/src/orgless-position-name-fold.test.ts +++ b/packages/plugins/plugin-security/src/orgless-position-name-fold.test.ts @@ -95,8 +95,21 @@ afterEach(async () => { } }); +/** + * [C8 stage 8b] An organization-less GLOBAL row is legacy residue: under + * `single` the engine stamps or refuses an organization-less platform insert + * (ADR-0131 D9), so a row with `organization_id` NULL is landed straight + * through the driver, as an older release left it. The readers this file pins + * still read it until C8's NULL-reader stages (18.x). + */ +function write(e: any, object: string, row: Record) { + return row.organization_id === null + ? e.getDriver(object).create(object, row) + : e.insert(object, row, SYS); +} + async function insertSet(e: any, id: string, name: string, organizationId: string | null, caps: string[]) { - await e.insert('sys_permission_set', { + await write(e, 'sys_permission_set', { id, name, label: `${organizationId ?? 'global'} ${name}`, @@ -105,7 +118,7 @@ async function insertSet(e: any, id: string, name: string, organizationId: strin active: true, object_permissions: JSON.stringify({ probe_ledger: FULL }), system_permissions: JSON.stringify(caps), - }, SYS); + }); } async function boot(): Promise { @@ -145,9 +158,9 @@ async function boot(): Promise { // USER_GLOBAL's GLOBAL grants — what an organization-less principal really holds: // (1) a global position assignment, folded onto a global same-named set; - await e.insert('sys_user_position', { + await write(e, 'sys_user_position', { id: 'up_global_ops_lead', user_id: USER_GLOBAL, position: 'ops_lead', organization_id: null, - }, SYS); + }); await insertSet(e, 'ps_global_ops_lead', 'ops_lead', null, ['probe.global_ops_lead']); // (2) a global user grant to a global set, beside ORG_OTHER's same-named // copy. The copy's id SORTS FIRST (`ps_0_…` before `ps_1_…`) and the @@ -157,10 +170,10 @@ async function boot(): Promise { // load-bearing. await insertSet(e, 'ps_0_other_platform_ops', 'platform_ops', ORG_OTHER, ['probe.other_platform_ops']); await insertSet(e, 'ps_1_global_platform_ops', 'platform_ops', null, ['probe.global_platform_ops']); - await e.insert('sys_user_permission_set', { + await write(e, 'sys_user_permission_set', { id: 'ups_global_platform_ops', user_id: USER_GLOBAL, permission_set: 'platform_ops', organization_id: null, - }, SYS); + }); // [ADR-0131 D3/D4] The two GLOBAL sets are declared in the catalog, which is // what makes a grant of them confer anything; the sets ORG_OTHER authored are // rows only, with no definition (#15196 Q3 A). diff --git a/packages/plugins/plugin-security/src/sys-audit-log-row-scope.test.ts b/packages/plugins/plugin-security/src/sys-audit-log-row-scope.test.ts index 99443c8ba74..8676d471aac 100644 --- a/packages/plugins/plugin-security/src/sys-audit-log-row-scope.test.ts +++ b/packages/plugins/plugin-security/src/sys-audit-log-row-scope.test.ts @@ -113,7 +113,10 @@ async function boot(opts: { posture: Posture; sets?: PermissionSet[]; ledger?: R type: 'plugin', scope: 'system', objects: [ - { name: 'sys_organization', label: 'Organization', fields: { name: { name: 'name', type: 'text' } } }, + // [C8 stage 8b] better-auth owns the table (`sys-organization.object.ts`): no + // injected organization column, so an organization's own insert is not judged + // as an organization-less platform write. + { name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { name: 'name', type: 'text' } } }, opts.ledger ?? LEDGER_NOW, ], } as never); From 6b72dfeef179804afaa893404da04b0cbd1ed9d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 07:16:22 +0000 Subject: [PATCH 07/11] test: two more fixtures 8b breaks land their organization-less legacy rows beneath the engine Claude-Session: https://claude.ai/code/session_01AZMXDVkiaxSpJkGXACuVwr Co-authored-by: Claude --- .../src/scim-deactivation-reconcile-user.test.ts | 11 ++++++++--- .../src/sys-audit-log-row-scope.test.ts | 10 ++++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/plugins/plugin-auth/src/scim-deactivation-reconcile-user.test.ts b/packages/plugins/plugin-auth/src/scim-deactivation-reconcile-user.test.ts index fa5ec7facea..2c163359999 100644 --- a/packages/plugins/plugin-auth/src/scim-deactivation-reconcile-user.test.ts +++ b/packages/plugins/plugin-auth/src/scim-deactivation-reconcile-user.test.ts @@ -407,13 +407,18 @@ async function makePlatformAdmin(h: Harness, userId: string): Promise { { where: { id: PS_ADMIN }, fields: ['id'] }, SYSTEM, ); + // [C8 stage 8b] The legacy organization-less catalogue row and the unscoped + // platform-admin grant go in beneath the engine: under `single` the engine + // refuses an organization-less platform insert while the install holds no + // organization (ADR-0131 D9), and the engine pool keeps none. + const residue = (object: string, row: Record) => + (h.engine as any).getDriver(object).create(object, row); if (!existing) { - await h.engine.insert('sys_permission_set', { id: PS_ADMIN, name: ADMIN_FULL_ACCESS }, SYSTEM); + await residue('sys_permission_set', { id: PS_ADMIN, name: ADMIN_FULL_ACCESS }); } - await h.engine.insert( + await residue( 'sys_user_permission_set', { id: `ups_${userId}`, user_id: userId, permission_set_id: PS_ADMIN, permission_set: ADMIN_FULL_ACCESS }, - SYSTEM, ); } diff --git a/packages/plugins/plugin-security/src/sys-audit-log-row-scope.test.ts b/packages/plugins/plugin-security/src/sys-audit-log-row-scope.test.ts index 8676d471aac..a8ff3270d4f 100644 --- a/packages/plugins/plugin-security/src/sys-audit-log-row-scope.test.ts +++ b/packages/plugins/plugin-security/src/sys-audit-log-row-scope.test.ts @@ -123,11 +123,17 @@ async function boot(opts: { posture: Posture; sets?: PermissionSet[]; ledger?: R await engine.syncSchemas(); await engine.insert('sys_organization', [{ id: 'org_a', name: 'A' }, ...(single ? [] : [{ id: 'org_b', name: 'B' }])], SYS); const before = opts.ledger === LEDGER_BEFORE; - await engine.insert(LEDGER, [ + const ledgerRows = [ { id: 'a1', action: 'update', tenant_id: 'org_a', ...(before ? { organization_id: 'org_a' } : {}) }, ...(single ? [] : [{ id: 'b1', action: 'update', tenant_id: 'org_b', ...(before ? { organization_id: 'org_b' } : {}) }]), { id: 'd1', action: 'platform_admin_standing_change', tenant_id: null }, - ], SYS); + ]; + // [C8 stage 8b] The pre-change ledger carried an organization column, and its + // deployment-level row none: that row is residue, landed beneath the engine, + // which now stamps or refuses an organization-less platform insert under + // `single` (ADR-0131 D9). The current ledger has no such column. + if (before) for (const row of ledgerRows) await (engine as any).getDriver(LEDGER).create(LEDGER, row); + else await engine.insert(LEDGER, ledgerRows, SYS); const services: Record = { manifest: { register: vi.fn() }, From af977a684ef2264e7df0877e734b2d8f1b729a97 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 07:18:28 +0000 Subject: [PATCH 08/11] =?UTF-8?q?chore(changeset):=20state=208b's=20change?= =?UTF-8?q?=20as=20before=20/=20now=20/=20remedy=20=E2=80=94=20nothing=20a?= =?UTF-8?q?n=20author=20writes=20is=20renamed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01AZMXDVkiaxSpJkGXACuVwr Co-authored-by: Claude --- .changeset/15212-8b-single-platform-insert-stamped.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.changeset/15212-8b-single-platform-insert-stamped.md b/.changeset/15212-8b-single-platform-insert-stamped.md index f70c659c59d..368ccfa25df 100644 --- a/.changeset/15212-8b-single-platform-insert-stamped.md +++ b/.changeset/15212-8b-single-platform-insert-stamped.md @@ -14,6 +14,4 @@ Clause-②: no (narrowing) - **Now, under `single`.** That insert goes through the same decision every application object already gets. With exactly one organization (the Default Organization), the row is stamped with it. With none, or with several, the insert is refused with `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` (status 500), and the message names the missing organization. Nothing is written. - **Unchanged.** Under a walled posture (`group` / `isolated`) the per-object exclusion stands as before; the walled half of this change ships in a later 18.x release. A write that carries an organization, on the execution context or on the record, is never re-stamped or refused. Objects with no organization column, objects declaring `tenancy: { enabled: false }`, and federated objects are outside the rule. Updates are untouched. The engine's diagnostics-only tenant-audit mute is unchanged. -**FROM → TO.** A system insert into an un-admitted platform object under `single`: FROM `organization_id` NULL, silently, TO the Default Organization's id, or a refusal when the install holds zero or several organizations. - -**The one-line fix.** If a refusal names your insert, carry the organization on the write: `ql.insert(object, { ...row, organization_id: orgId }, { context: { isSystem: true } })`, or `{ context: { isSystem: true, tenantId: orgId } }`. On a fresh `single` deployment the auth plugin creates the Default Organization at boot, so a refusal reading `no-organization` means the write ran before it, or in a composition without the auth plugin's identity objects. +**The remedy.** If a refusal names your insert, carry the organization on the write: `ql.insert(object, { ...row, organization_id: orgId }, { context: { isSystem: true } })`, or `{ context: { isSystem: true, tenantId: orgId } }`. On a fresh `single` deployment the auth plugin creates the Default Organization at boot, so a refusal reading `no-organization` means the write ran before it, or in a composition without the auth plugin's identity objects. From a88d1911edc9da39cbfc16787fe194e6c7e39fb4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 08:53:58 +0000 Subject: [PATCH 09/11] test(dogfood): fixtures 8b breaks resolve in the organization a single session carries, land global grants beneath the engine, or carry their owner Claude-Session: https://claude.ai/code/session_01AZMXDVkiaxSpJkGXACuVwr Co-authored-by: Claude --- .../activity-withheld-update.dogfood.test.ts | 5 +++- .../catalog-activation-door.dogfood.test.ts | 26 +++++++++++++------ ...-write-refusal-not-visible.dogfood.test.ts | 9 ++++++- .../permission-set-clone-door.dogfood.test.ts | 11 +++++++- ...ite-unreadable-not-matched.dogfood.test.ts | 9 ++++++- .../showcase-permission-zoo.dogfood.test.ts | 7 ++++- ...wcase-scope-depth-fallback.dogfood.test.ts | 10 ++++--- ...showcase-scope-depth-write.dogfood.test.ts | 9 +++++-- .../test/showcase-scope-depth.dogfood.test.ts | 11 +++++--- ...nsert-default-organization.dogfood.test.ts | 7 +++-- ...gle-tenant-identity-create.dogfood.test.ts | 14 +++++++--- ...wall-authoring-trail-doors.dogfood.test.ts | 13 ++++++++-- ...vironment-metadata-refusal.dogfood.test.ts | 13 ++++++++-- ...wall-package-doors-refusal.dogfood.test.ts | 11 +++++++- .../test/wall-package-refusal.dogfood.test.ts | 13 ++++++++-- ...or-unreadable-is-not-found.dogfood.test.ts | 9 ++++++- 16 files changed, 142 insertions(+), 35 deletions(-) diff --git a/packages/qa/dogfood/test/activity-withheld-update.dogfood.test.ts b/packages/qa/dogfood/test/activity-withheld-update.dogfood.test.ts index b460b041219..cbd14916bac 100644 --- a/packages/qa/dogfood/test/activity-withheld-update.dogfood.test.ts +++ b/packages/qa/dogfood/test/activity-withheld-update.dogfood.test.ts @@ -115,8 +115,11 @@ describe('[#21388] an org peer is withheld the activity rows of a colleague’s } const activitySet = await catalogPermissionSet(ql, activityReadSet.name); expect(activitySet, 'fixture permission set served by the security catalog').toBeTruthy(); + // [C8 stage 8b] The grant carries the fixture organization the sessions are + // active in: this `single` install now holds two organizations, so an + // organization-less system insert is refused as ambiguous (ADR-0131 D9). for (const who of ['member', 'admin']) { - await ql.insert('sys_user_permission_set', { user_id: uid[who], permission_set: activitySet.name }, { context: SYS }); + await ql.insert('sys_user_permission_set', { user_id: uid[who], permission_set: activitySet.name, organization_id: orgId }, { context: SYS }); } // The colleague signs in twice through the auth door (two sign-in stamps), diff --git a/packages/qa/dogfood/test/catalog-activation-door.dogfood.test.ts b/packages/qa/dogfood/test/catalog-activation-door.dogfood.test.ts index dc54f9843ba..2065220aeef 100644 --- a/packages/qa/dogfood/test/catalog-activation-door.dogfood.test.ts +++ b/packages/qa/dogfood/test/catalog-activation-door.dogfood.test.ts @@ -52,6 +52,15 @@ interface Engine { update(object: string, data: Record, options?: unknown): Promise; } +/** + * [C8 stage 8b] The organization a `single` session is active in: the Default + * Organization, which an organization-less system insert of a grant or a + * position is now stamped with (ADR-0131 D9). Resolving with no organization + * would read the global arm only, which no fresh `single` install writes any more. + */ +const sessionOrgOf = async (s: VerifyStack): Promise => + (await (await s.kernel.getServiceAsync<{ defaultOrgId(): Promise }>('tenancy')).defaultOrgId()) ?? undefined; + describe('the catalog activation door, on a booted showcase', () => { let stack: VerifyStack; let token: string; @@ -86,7 +95,8 @@ describe('the catalog activation door, on a booted showcase', () => { it('the resolver over the booted engine stops granting through a switched-off position, and grants again once it is back on', async () => { // Its own holder, so the leg depends on nothing a boot seeds later: a - // signed-up member, assigned the `exec` position unscoped. + // signed-up member, assigned the `exec` position by a system write, which + // under `single` takes the Default Organization (C8 stage 8b). await stack.signUp(HOLDER_EMAIL); const [user] = await ql.find('sys_user', { where: { email: HOLDER_EMAIL }, limit: 1, context: SYSTEM_CTX }); expect(user?.id, `no sys_user row for ${HOLDER_EMAIL}`).toBeTruthy(); @@ -99,20 +109,20 @@ describe('the catalog activation door, on a booted showcase', () => { // CONTROL: before the flip the position grants its set — without it the // two assertions below would pass on a holder that was never granted. - const before = await resolveUserAuthzGrants(ql as never, holder, {}); + const before = await resolveUserAuthzGrants(ql as never, holder, { tenantId: await sessionOrgOf(stack) }); expect(before.positions).toContain(HELD_POSITION); expect(before.permissions).toContain(HELD_POSITION_SET); const offRes = await flip('position', HELD_POSITION, { enabled: false }); expect(offRes.status, await offRes.clone().text()).toBe(200); - const off = await resolveUserAuthzGrants(ql as never, holder, {}); + const off = await resolveUserAuthzGrants(ql as never, holder, { tenantId: await sessionOrgOf(stack) }); expect(off.positions).not.toContain(HELD_POSITION); expect(off.permissions).not.toContain(HELD_POSITION_SET); const onRes = await flip('position', HELD_POSITION, { enabled: true }); expect(onRes.status, await onRes.clone().text()).toBe(200); expect(await rowsOf('position', HELD_POSITION)).toHaveLength(1); - const on = await resolveUserAuthzGrants(ql as never, holder, {}); + const on = await resolveUserAuthzGrants(ql as never, holder, { tenantId: await sessionOrgOf(stack) }); expect(on.positions).toContain(HELD_POSITION); expect(on.permissions).toContain(HELD_POSITION_SET); }); @@ -211,7 +221,7 @@ describe('the catalog activation door, on a booted showcase', () => { ); // CONTROL: before any flip the position grants its set. - const before = await resolveUserAuthzGrants(ql as never, holder, {}); + const before = await resolveUserAuthzGrants(ql as never, holder, { tenantId: await sessionOrgOf(stack) }); expect(before.positions).toContain(ENV_POSITION); expect(before.permissions).toContain(ENV_SET); @@ -221,7 +231,7 @@ describe('the catalog activation door, on a booted showcase', () => { expect(positionRows).toHaveLength(1); expect(positionRows[0].active === false || positionRows[0].active === 0).toBe(true); expect(positionRows[0].package_id ?? null).toBeNull(); - const positionSwitchedOff = await resolveUserAuthzGrants(ql as never, holder, {}); + const positionSwitchedOff = await resolveUserAuthzGrants(ql as never, holder, { tenantId: await sessionOrgOf(stack) }); expect(positionSwitchedOff.positions).not.toContain(ENV_POSITION); expect(positionSwitchedOff.permissions).not.toContain(ENV_SET); @@ -234,13 +244,13 @@ describe('the catalog activation door, on a booted showcase', () => { const setRows = await rowsOf('permission', ENV_SET); expect(setRows).toHaveLength(1); expect(setRows[0].package_id ?? null).toBeNull(); - const setSwitchedOff = await resolveUserAuthzGrants(ql as never, holder, {}); + const setSwitchedOff = await resolveUserAuthzGrants(ql as never, holder, { tenantId: await sessionOrgOf(stack) }); expect(setSwitchedOff.positions).toContain(ENV_POSITION); expect(setSwitchedOff.permissions).not.toContain(ENV_SET); const setOn = await flip('permission', ENV_SET, { enabled: true }); expect(setOn.status, await setOn.clone().text()).toBe(200); - const after = await resolveUserAuthzGrants(ql as never, holder, {}); + const after = await resolveUserAuthzGrants(ql as never, holder, { tenantId: await sessionOrgOf(stack) }); expect(after.positions).toContain(ENV_POSITION); expect(after.permissions).toContain(ENV_SET); }); diff --git a/packages/qa/dogfood/test/parent-derived-write-refusal-not-visible.dogfood.test.ts b/packages/qa/dogfood/test/parent-derived-write-refusal-not-visible.dogfood.test.ts index 979c594a2f7..d900b833f2e 100644 --- a/packages/qa/dogfood/test/parent-derived-write-refusal-not-visible.dogfood.test.ts +++ b/packages/qa/dogfood/test/parent-derived-write-refusal-not-visible.dogfood.test.ts @@ -134,7 +134,14 @@ async function boot(inside: boolean, email: string): Promise { for (const name of GRANTS) { const set = await catalogPermissionSet(ql, name); expect(set, `fixture permission set ${name} served by the security catalog`).toBeTruthy(); - await ql.insert('sys_user_permission_set', { user_id: userId, permission_set: set.name }, { context: { ...SYS } }); + // [C8 stage 8b] A GLOBAL grant, which the outside principal (no membership, no + // active organization) must still hold: under `single` the engine now stamps an + // organization-less system insert with the Default Organization (ADR-0131 D9), + // so the global row is landed beneath the engine, as the residue an older + // release left. The readers still apply it until C8's NULL-reader stages. + await ql.getDriver('sys_user_permission_set').create('sys_user_permission_set', { + id: `ups_${userId}_${set.name}`, user_id: userId, permission_set: set.name, organization_id: null, + }); } if (!inside) token = await leaveOrganization(stack, email); return { stack, rootDir, ql, adminId, token }; diff --git a/packages/qa/dogfood/test/permission-set-clone-door.dogfood.test.ts b/packages/qa/dogfood/test/permission-set-clone-door.dogfood.test.ts index e4c241c17fd..bd31f07da3c 100644 --- a/packages/qa/dogfood/test/permission-set-clone-door.dogfood.test.ts +++ b/packages/qa/dogfood/test/permission-set-clone-door.dogfood.test.ts @@ -42,6 +42,15 @@ interface Engine { getObject?(object: string): unknown; } +/** + * [C8 stage 8b] The organization a `single` session is active in: the Default + * Organization, which an organization-less system insert of a grant or a + * position is now stamped with (ADR-0131 D9). Resolving with no organization + * would read the global arm only, which no fresh `single` install writes any more. + */ +const sessionOrgOf = async (s: VerifyStack): Promise => + (await (await s.kernel.getServiceAsync<{ defaultOrgId(): Promise }>('tenancy')).defaultOrgId()) ?? undefined; + describe('the permission-set clone door, on a booted showcase', () => { let stack: VerifyStack; let token: string; @@ -117,7 +126,7 @@ describe('the permission-set clone door, on a booted showcase', () => { { id: 'usp_clone_door_holder', user_id: String(user.id), position: COPY_POSITION }, { context: SYSTEM_CTX }, ); - const grants = await resolveUserAuthzGrants(ql as never, String(user.id), {}); + const grants = await resolveUserAuthzGrants(ql as never, String(user.id), { tenantId: await sessionOrgOf(stack) }); expect(grants.positions).toContain(COPY_POSITION); expect(grants.permissions).toContain(DEFAULT_COPY); }); diff --git a/packages/qa/dogfood/test/predicate-write-unreadable-not-matched.dogfood.test.ts b/packages/qa/dogfood/test/predicate-write-unreadable-not-matched.dogfood.test.ts index 45004861493..1c30c648be9 100644 --- a/packages/qa/dogfood/test/predicate-write-unreadable-not-matched.dogfood.test.ts +++ b/packages/qa/dogfood/test/predicate-write-unreadable-not-matched.dogfood.test.ts @@ -157,7 +157,14 @@ async function boot(inside: boolean, email: string): Promise { for (const name of GRANTS) { const set = await catalogPermissionSet(ql, name); expect(set, `fixture permission set ${name} served by the security catalog`).toBeTruthy(); - await ql.insert('sys_user_permission_set', { user_id: userId, permission_set: set.name }, { context: { ...SYS } }); + // [C8 stage 8b] A GLOBAL grant, which the outside principal (no membership, no + // active organization) must still hold: under `single` the engine now stamps an + // organization-less system insert with the Default Organization (ADR-0131 D9), + // so the global row is landed beneath the engine, as the residue an older + // release left. The readers still apply it until C8's NULL-reader stages. + await ql.getDriver('sys_user_permission_set').create('sys_user_permission_set', { + id: `ups_${userId}_${set.name}`, user_id: userId, permission_set: set.name, organization_id: null, + }); } if (!inside) token = await leaveOrganization(stack, email); return { stack, rootDir, ql, adminId, userId, token }; diff --git a/packages/qa/dogfood/test/showcase-permission-zoo.dogfood.test.ts b/packages/qa/dogfood/test/showcase-permission-zoo.dogfood.test.ts index 09b305c5da3..63cbebdfb87 100644 --- a/packages/qa/dogfood/test/showcase-permission-zoo.dogfood.test.ts +++ b/packages/qa/dogfood/test/showcase-permission-zoo.dogfood.test.ts @@ -125,9 +125,14 @@ describe('showcase: ADR-0090 permission-model zoo', () => { it('explain() reports the vama_bypass layer with the auditor set as contributor', async () => { const security: any = stack.kernel.getService('security'); expect(security?.explain, 'security service exposes explain()').toBeTruthy(); + // [C8 stage 8b] Asked in the organization a `single` session carries: the + // auditor's grant is an organization-less system insert, which now takes the + // Default Organization (ADR-0131 D9), and explain resolves the principal in + // the caller's organization, with none reading only the global arm. + const tenancy = await stack.kernel.getServiceAsync<{ defaultOrgId(): Promise }>('tenancy'); const decision = await security.explain( { object: 'showcase_private_note', operation: 'read', userId: auditorId }, - { isSystem: true }, + { isSystem: true, tenantId: (await tenancy.defaultOrgId()) ?? undefined }, ); expect(decision?.allowed).toBe(true); const vama = (decision?.layers ?? []).find((l: any) => l.layer === 'vama_bypass'); diff --git a/packages/qa/dogfood/test/showcase-scope-depth-fallback.dogfood.test.ts b/packages/qa/dogfood/test/showcase-scope-depth-fallback.dogfood.test.ts index 52d006c86d3..eb598db0259 100644 --- a/packages/qa/dogfood/test/showcase-scope-depth-fallback.dogfood.test.ts +++ b/packages/qa/dogfood/test/showcase-scope-depth-fallback.dogfood.test.ts @@ -107,9 +107,13 @@ async function bootFallbackWorld(withResolver = true): Promise { const id = {} as Record; for (const who of WHO) id[who] = await uid(who); - let org = await ql.findOne('sys_organization', { where: {}, context: { isSystem: true } }).catch(() => null); - let orgId = org?.id; - if (!orgId) { orgId = 'org_fb'; await sys('sys_organization', { id: orgId, name: 'FB Org', slug: 'fb' }).catch(() => {}); } + // [ADR-0131 D3/D9, C8 stage 8b] The showcase boots `single`, so the Default + // Organization exists before the first sign-up and owns every row this fixture + // seeds — asked of the `tenancy` service. (An empty-`where` `findOne` is refused + // as an arbitrary pick; the refusal was swallowed and a second organization + // minted, which under `single` makes every organization-less insert ambiguous.) + const orgId = (await (await stack.kernel.getServiceAsync<{ defaultOrgId(): Promise }>('tenancy')).defaultOrgId()); + if (!orgId) throw new Error('fixture: the showcase boot has no Default Organization to own the business units'); await sys('sys_business_unit', { id: 'bu_parent_fb', name: 'Parent', kind: 'division', organization_id: orgId, active: true }); await sys('sys_business_unit', { id: 'bu_child_fb', name: 'Child', kind: 'department', parent_business_unit_id: 'bu_parent_fb', organization_id: orgId, active: true }); diff --git a/packages/qa/dogfood/test/showcase-scope-depth-write.dogfood.test.ts b/packages/qa/dogfood/test/showcase-scope-depth-write.dogfood.test.ts index 768df95c32c..c61188aa281 100644 --- a/packages/qa/dogfood/test/showcase-scope-depth-write.dogfood.test.ts +++ b/packages/qa/dogfood/test/showcase-scope-depth-write.dogfood.test.ts @@ -99,8 +99,13 @@ async function bootWriteWorld(tag: string, opts: { writeScope?: 'unit'; withReso expect(id[who], `${who} resolved`).toBeTruthy(); } - let orgId = (await ql.findOne('sys_organization', { where: {}, context: SYS }).catch(() => null))?.id; - if (!orgId) { orgId = `org_w_${tag}`; await sys('sys_organization', { id: orgId, name: 'WScope Org', slug: `wscope_${tag}` }).catch(() => {}); } + // [ADR-0131 D3/D9, C8 stage 8b] The showcase boots `single`, so the Default + // Organization exists before the first sign-up and owns every row this fixture + // seeds — asked of the `tenancy` service. (An empty-`where` `findOne` is refused + // as an arbitrary pick; the refusal was swallowed and a second organization + // minted, which under `single` makes every organization-less insert ambiguous.) + const orgId = (await (await stack.kernel.getServiceAsync<{ defaultOrgId(): Promise }>('tenancy')).defaultOrgId()); + if (!orgId) throw new Error('fixture: the showcase boot has no Default Organization to own the business units'); await sys('sys_business_unit', { id: `bu_wparent_${tag}`, name: 'Parent', kind: 'division', organization_id: orgId, active: true }); await sys('sys_business_unit', { id: `bu_wchild_${tag}`, name: 'Child', kind: 'department', parent_business_unit_id: `bu_wparent_${tag}`, organization_id: orgId, active: true }); diff --git a/packages/qa/dogfood/test/showcase-scope-depth.dogfood.test.ts b/packages/qa/dogfood/test/showcase-scope-depth.dogfood.test.ts index 86fc566e7cb..e3b1d48b539 100644 --- a/packages/qa/dogfood/test/showcase-scope-depth.dogfood.test.ts +++ b/packages/qa/dogfood/test/showcase-scope-depth.dogfood.test.ts @@ -126,10 +126,13 @@ async function bootScopeWorld(scope: 'unit' | 'unit_and_below' | 'own_and_report const id = {} as Record; for (const who of WHO) id[who] = await uid(who); - // org (BU.organization_id is required) — reuse any existing, else create one. - let org = await ql.findOne('sys_organization', { where: {}, context: { isSystem: true } }).catch(() => null); - let orgId = org?.id; - if (!orgId) { orgId = 'org_scope'; await sys('sys_organization', { id: orgId, name: 'Scope Org', slug: `scope_${scope}` }).catch(() => {}); } + // [ADR-0131 D3/D9, C8 stage 8b] The showcase boots `single`, so the Default + // Organization exists before the first sign-up and owns every row this fixture + // seeds — asked of the `tenancy` service. (An empty-`where` `findOne` is refused + // as an arbitrary pick; the refusal was swallowed and a second organization + // minted, which under `single` makes every organization-less insert ambiguous.) + const orgId = (await (await stack.kernel.getServiceAsync<{ defaultOrgId(): Promise }>('tenancy')).defaultOrgId()); + if (!orgId) throw new Error('fixture: the showcase boot has no Default Organization to own the business units'); const p = `${scope}`; await sys('sys_business_unit', { id: `bu_parent_${p}`, name: 'Parent', kind: 'division', organization_id: orgId, active: true }); diff --git a/packages/qa/dogfood/test/showcase-single-platform-insert-default-organization.dogfood.test.ts b/packages/qa/dogfood/test/showcase-single-platform-insert-default-organization.dogfood.test.ts index 64fa7b76a43..a603510bcc3 100644 --- a/packages/qa/dogfood/test/showcase-single-platform-insert-default-organization.dogfood.test.ts +++ b/packages/qa/dogfood/test/showcase-single-platform-insert-default-organization.dogfood.test.ts @@ -90,9 +90,12 @@ describe('showcase (single): a platform object system insert takes the Default O }); it('with a second organization the same insert is REFUSED, loudly, and nothing is written', async () => { + // A unit the user is not yet a member of, so the one-membership-per-unit + // constraint cannot answer before the organization rule does. + await ql.insert('sys_business_unit', { id: 'bu_c8_8b_carried', name: 'C8 8b carried', kind: 'department', active: true, organization_id: orgId }, { context: SYS }); await ql.insert('sys_organization', { id: 'org_c8_8b_second', name: 'Second', slug: 'c8-8b-second' }, { context: SYS }); await expect( - ql.insert('sys_business_unit_member', { id: 'bm_c8_8b_refused', business_unit_id: 'bu_c8_8b', user_id: userId }, { context: SYS }), + ql.insert('sys_business_unit_member', { id: 'bm_c8_8b_refused', business_unit_id: 'bu_c8_8b_carried', user_id: userId }, { context: SYS }), ).rejects.toMatchObject({ code: 'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED', status: 500, @@ -100,7 +103,7 @@ describe('showcase (single): a platform object system insert takes the Default O }); expect(await ql.findOne('sys_business_unit_member', { where: { id: 'bm_c8_8b_refused' }, context: SYS })).toBeFalsy(); // Carrying the organization is the remedy the refusal names, and it lands. - await ql.insert('sys_business_unit_member', { id: 'bm_c8_8b_carried', business_unit_id: 'bu_c8_8b', user_id: userId, organization_id: orgId }, { context: SYS }); + await ql.insert('sys_business_unit_member', { id: 'bm_c8_8b_carried', business_unit_id: 'bu_c8_8b_carried', user_id: userId, organization_id: orgId }, { context: SYS }); expect((await ql.findOne('sys_business_unit_member', { where: { id: 'bm_c8_8b_carried' }, context: SYS }))?.organization_id).toBe(orgId); }); }); diff --git a/packages/qa/dogfood/test/single-tenant-identity-create.dogfood.test.ts b/packages/qa/dogfood/test/single-tenant-identity-create.dogfood.test.ts index 431a914bffd..8090144b5f9 100644 --- a/packages/qa/dogfood/test/single-tenant-identity-create.dogfood.test.ts +++ b/packages/qa/dogfood/test/single-tenant-identity-create.dogfood.test.ts @@ -66,9 +66,14 @@ describe('ADR-0057: org-scoped identity creatable single-tenant', () => { expect(denied.code).toBe('OBJECT_API_METHOD_NOT_ALLOWED'); // ADR-0057's actual regression target — `organization_id` is OPTIONAL at - // the schema level, so a single-tenant (no org row) write does not die - // with VALIDATION_FAILED. System-context writes (org-structure sync, + // the schema level, so a system write that names no organization does not + // die with VALIDATION_FAILED. System-context writes (org-structure sync, // seeding) are the writers that remain legitimate post-ADR-0092. + // + // [ADR-0131 D9 / D11, C8 stage 8b] Under `single` that write is no longer + // left organization-less: it takes the Default Organization, the one owner + // a `single` install has. It used to land NULL, through the platform-object + // exemption 8b lifted under `single`. const ql = await stack.kernel.getServiceAsync('objectql'); const row = await ql.insert( 'sys_team', @@ -76,6 +81,9 @@ describe('ADR-0057: org-scoped identity creatable single-tenant', () => { { context: { isSystem: true } }, ); expect(row?.id).toBeTruthy(); - expect(row?.organization_id ?? null).toBeNull(); + const tenancy = await stack.kernel.getServiceAsync<{ defaultOrgId(): Promise }>('tenancy'); + const defaultOrgId = await tenancy.defaultOrgId(); + expect(defaultOrgId).toBeTruthy(); + expect(row?.organization_id).toBe(defaultOrgId); }); }); diff --git a/packages/qa/dogfood/test/wall-authoring-trail-doors.dogfood.test.ts b/packages/qa/dogfood/test/wall-authoring-trail-doors.dogfood.test.ts index d573affe5a6..b8b5fbe3a49 100644 --- a/packages/qa/dogfood/test/wall-authoring-trail-doors.dogfood.test.ts +++ b/packages/qa/dogfood/test/wall-authoring-trail-doors.dogfood.test.ts @@ -159,6 +159,15 @@ async function boot(posture: 'isolated' | 'group' | 'single'): Promise return h; } +/** + * [C8 stage 8b] The organization a `single` session is active in: the Default + * Organization, which an organization-less system grant is now stamped with + * (ADR-0131 D9). A precheck that resolves with no organization would read the + * global arm only, which no fresh `single` install writes any more. + */ +const sessionOrgOf = async (h: Harness): Promise => + (await (await h.stack.kernel.getServiceAsync<{ defaultOrgId(): Promise }>('tenancy')).defaultOrgId()) ?? undefined; + const grantsOf = (h: Harness, user: string, org: string | undefined) => resolveUserAuthzGrants(h.ql as never, user, org ? { tenantId: org } : {}); @@ -259,10 +268,10 @@ describe('[#23546] single: the narrowing is not a posture branch, and no door ga }); it('guard the guard: the metadata author holds manage_metadata alone; the setup holder holds setup.access alone', async () => { - const author = await grantsOf(h, h.author, undefined); + const author = await grantsOf(h, h.author, await sessionOrgOf(h)); expect([...author.systemPermissions].sort()).toEqual(['manage_metadata']); expect(author.posture).not.toBe('PLATFORM_ADMIN'); - const setup = await grantsOf(h, h.setup, undefined); + const setup = await grantsOf(h, h.setup, await sessionOrgOf(h)); expect([...setup.systemPermissions].sort()).toEqual(['setup.access']); }); diff --git a/packages/qa/dogfood/test/wall-environment-metadata-refusal.dogfood.test.ts b/packages/qa/dogfood/test/wall-environment-metadata-refusal.dogfood.test.ts index 14017868c4b..c4ad838c4da 100644 --- a/packages/qa/dogfood/test/wall-environment-metadata-refusal.dogfood.test.ts +++ b/packages/qa/dogfood/test/wall-environment-metadata-refusal.dogfood.test.ts @@ -145,7 +145,16 @@ async function ok(res: Response, status = 200): Promise { } for (const posture of ['isolated', 'group'] as const) { - describe(`[ADR-0131 D12 items 7–8] ${posture}: every environment-metadata write is the platform operator's`, () => { + /** + * [C8 stage 8b] The organization a `single` session is active in: the Default + * Organization, which an organization-less system grant is now stamped with + * (ADR-0131 D9). A precheck that resolves with no organization would read the + * global arm only, which no fresh `single` install writes any more. + */ +const sessionOrgOf = async (h: Harness): Promise => + (await (await h.stack.kernel.getServiceAsync<{ defaultOrgId(): Promise }>('tenancy')).defaultOrgId()) ?? undefined; + +describe(`[ADR-0131 D12 items 7–8] ${posture}: every environment-metadata write is the platform operator's`, () => { let h: Harness; const previous = process.env.OS_TENANCY_POSTURE; const p = posture.slice(0, 3); @@ -328,7 +337,7 @@ describe('[ADR-0131 D12 items 7–8 · D11] single (CONTROL): the refusal is ine }); it('a `manage_metadata` holder below the operator writes at the /meta (the stored rewrite included), /packages, install-local and /automation doors as before', async () => { - const a = await resolveUserAuthzGrants(h.ql as never, h.userA, {}); + const a = await resolveUserAuthzGrants(h.ql as never, h.userA, { tenantId: await sessionOrgOf(h) }); expect(a.systemPermissions).toContain('manage_metadata'); expect(a.posture).not.toBe('PLATFORM_ADMIN'); await ok(await h.stack.apiAs(h.tokA, 'PUT', '/meta/object/em_sin_obj', object('em_sin_obj', 'Object by A'))); diff --git a/packages/qa/dogfood/test/wall-package-doors-refusal.dogfood.test.ts b/packages/qa/dogfood/test/wall-package-doors-refusal.dogfood.test.ts index 23c6bf10f93..6f7a7db5cee 100644 --- a/packages/qa/dogfood/test/wall-package-doors-refusal.dogfood.test.ts +++ b/packages/qa/dogfood/test/wall-package-doors-refusal.dogfood.test.ts @@ -140,6 +140,15 @@ async function boot(posture: 'isolated' | 'group' | 'single'): Promise return h; } +/** + * [C8 stage 8b] The organization a `single` session is active in: the Default + * Organization, which an organization-less system grant is now stamped with + * (ADR-0131 D9). A precheck that resolves with no organization would read the + * global arm only, which no fresh `single` install writes any more. + */ +const sessionOrgOf = async (h: Harness): Promise => + (await (await h.stack.kernel.getServiceAsync<{ defaultOrgId(): Promise }>('tenancy')).defaultOrgId()) ?? undefined; + const grantsOf = (h: Harness, user: string, org: string | undefined) => resolveUserAuthzGrants(h.ql as never, user, org ? { tenantId: org } : {}); @@ -305,7 +314,7 @@ describe('[ADR-0131 D3 · #15204 W3] single (CONTROL): the refusal is inert', () }); it('a `manage_metadata` holder below the operator runs every one of these doors on the operator\'s package as before', async () => { - const a = await grantsOf(h, h.userA, undefined); + const a = await grantsOf(h, h.userA, await sessionOrgOf(h)); expect(a.systemPermissions).toContain('manage_metadata'); expect(a.posture).not.toBe('PLATFORM_ADMIN'); const ok = async (res: Response) => expect(res.status, await res.clone().text()).toBe(200); diff --git a/packages/qa/dogfood/test/wall-package-refusal.dogfood.test.ts b/packages/qa/dogfood/test/wall-package-refusal.dogfood.test.ts index 4cf22417626..c90db0b6299 100644 --- a/packages/qa/dogfood/test/wall-package-refusal.dogfood.test.ts +++ b/packages/qa/dogfood/test/wall-package-refusal.dogfood.test.ts @@ -149,6 +149,15 @@ async function boot(posture: 'isolated' | 'group' | 'single'): Promise return h; } +/** + * [C8 stage 8b] The organization a `single` session is active in: the Default + * Organization, which an organization-less system grant is now stamped with + * (ADR-0131 D9). A precheck that resolves with no organization would read the + * global arm only, which no fresh `single` install writes any more. + */ +const sessionOrgOf = async (h: Harness): Promise => + (await (await h.stack.kernel.getServiceAsync<{ defaultOrgId(): Promise }>('tenancy')).defaultOrgId()) ?? undefined; + const grantsOf = (h: Harness, user: string, org: string | undefined) => resolveUserAuthzGrants(h.ql as never, user, org ? { tenantId: org } : {}); @@ -287,7 +296,7 @@ describe('[ADR-0131 D3 · #15204 W2] single (CONTROL): the refusal is inert', () }); it('a `manage_metadata` holder below the operator installs, upgrades and uninstalls catalog-carrying packages, and deletes one at /packages', async () => { - const a = await grantsOf(h, h.userA, undefined); + const a = await grantsOf(h, h.userA, await sessionOrgOf(h)); expect(a.systemPermissions).toContain('manage_metadata'); expect(a.posture).not.toBe('PLATFORM_ADMIN'); const installed = await h.stack.apiAs(h.tokA, 'POST', INSTALL_LOCAL, @@ -296,7 +305,7 @@ describe('[ADR-0131 D3 · #15204 W2] single (CONTROL): the refusal is inert', () const upgraded = await h.stack.apiAs(h.tokA, 'POST', INSTALL_LOCAL, packageBody(OP_PKG, '1.1.0', { permissions: [set(OP_SET, ['manage_users'])] })); expect(upgraded.status, await upgraded.clone().text()).toBe(200); - expect((await grantsOf(h, h.userB, undefined)).systemPermissions).toContain('manage_users'); + expect((await grantsOf(h, h.userB, await sessionOrgOf(h))).systemPermissions).toContain('manage_users'); const removed = await h.stack.apiAs(h.tokA, 'DELETE', `${INSTALL_LOCAL}/${OP_PKG}`); expect(removed.status, await removed.clone().text()).toBe(200); const deleted = await h.stack.apiAs(h.tokA, 'DELETE', `/packages/${RT_PKG}`); diff --git a/packages/qa/dogfood/test/write-door-unreadable-is-not-found.dogfood.test.ts b/packages/qa/dogfood/test/write-door-unreadable-is-not-found.dogfood.test.ts index c109eb5919a..69a8d1d01fe 100644 --- a/packages/qa/dogfood/test/write-door-unreadable-is-not-found.dogfood.test.ts +++ b/packages/qa/dogfood/test/write-door-unreadable-is-not-found.dogfood.test.ts @@ -126,7 +126,14 @@ async function boot(inside: boolean, email: string): Promise { for (const name of GRANTS) { const set = await catalogPermissionSet(ql, name); expect(set, `fixture permission set ${name} served by the security catalog`).toBeTruthy(); - await ql.insert('sys_user_permission_set', { user_id: userId, permission_set: set.name }, { context: { ...SYS } }); + // [C8 stage 8b] A GLOBAL grant, which the outside principal (no membership, no + // active organization) must still hold: under `single` the engine now stamps an + // organization-less system insert with the Default Organization (ADR-0131 D9), + // so the global row is landed beneath the engine, as the residue an older + // release left. The readers still apply it until C8's NULL-reader stages. + await ql.getDriver('sys_user_permission_set').create('sys_user_permission_set', { + id: `ups_${userId}_${set.name}`, user_id: userId, permission_set: set.name, organization_id: null, + }); } if (!inside) token = await leaveOrganization(stack, email); return { stack, rootDir, ql, adminId, token }; From d1838bf965a4f84649019c1bff7e0f9f3e2f0b93 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 09:00:43 +0000 Subject: [PATCH 10/11] test(dogfood): three more fixtures 8b breaks; the new pin's refusal leg asks a unit the user is not yet in Claude-Session: https://claude.ai/code/session_01AZMXDVkiaxSpJkGXACuVwr Co-authored-by: Claude --- .../identity-admin-fields-org-peer.dogfood.test.ts | 5 ++++- .../sharing-rule-org-less-caller.dogfood.test.ts | 14 ++++++++------ ...ll-environment-metadata-refusal.dogfood.test.ts | 6 +++--- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/qa/dogfood/test/identity-admin-fields-org-peer.dogfood.test.ts b/packages/qa/dogfood/test/identity-admin-fields-org-peer.dogfood.test.ts index d436351cdb0..ac4ce79fe7e 100644 --- a/packages/qa/dogfood/test/identity-admin-fields-org-peer.dogfood.test.ts +++ b/packages/qa/dogfood/test/identity-admin-fields-org-peer.dogfood.test.ts @@ -152,8 +152,11 @@ describe('[#21237] the identity object Admin group at the HTTP door: org peers a const activitySet = await catalogPermissionSet(ql, activityReadSet.name); expect(activitySet, 'fixture permission set served by the security catalog').toBeTruthy(); + // [C8 stage 8b] The grant carries the fixture organization the sessions are + // active in: this `single` install now holds two organizations, so an + // organization-less system insert is refused as ambiguous (ADR-0131 D9). for (const who of ['member', 'owner', 'admin']) { - await ql.insert('sys_user_permission_set', { user_id: uid[who], permission_set: activitySet.name }, { context: SYS }); + await ql.insert('sys_user_permission_set', { user_id: uid[who], permission_set: activitySet.name, organization_id: orgId }, { context: SYS }); } // The colleague's group values, written as the system in two steps so the diff --git a/packages/qa/dogfood/test/sharing-rule-org-less-caller.dogfood.test.ts b/packages/qa/dogfood/test/sharing-rule-org-less-caller.dogfood.test.ts index e353973f854..18fc74ea469 100644 --- a/packages/qa/dogfood/test/sharing-rule-org-less-caller.dogfood.test.ts +++ b/packages/qa/dogfood/test/sharing-rule-org-less-caller.dogfood.test.ts @@ -161,9 +161,13 @@ describe('#8158 — a manage_sharing holder with NO active organization cannot r // with no active organization only global grants apply, so this is the one // spelling under which it still carries `manage_sharing` and still reaches // `adminOrgScope`. - await ql.insert('sys_user_permission_set', { - user_id: orgLessUserId, permission_set: 'sharing_admin_8158', organization_id: null, - }, { context: SYS }); + // [C8 stage 8b] A global grant is legacy residue in 18.0: the engine now stamps + // or refuses an organization-less system insert under `single` (ADR-0131 D9), + // so it is landed beneath the engine. The readers still apply it until C8's + // NULL-reader stages. + const globalGrant = (row: Record) => + ql.getDriver('sys_user_permission_set').create('sys_user_permission_set', { ...row, organization_id: null }); + await globalGrant({ id: 'ups_8158_org_less', user_id: orgLessUserId, permission_set: 'sharing_admin_8158' }); // The #8158 grant as filed — scoped to tenant A — for the control (which // has tenant A active) and for the org-scoped persona (which has none). for (const userId of [orgBoundUserId, orgScopedOrgLessUserId]) { @@ -190,9 +194,7 @@ describe('#8158 — a manage_sharing holder with NO active organization cannot r await stack.signUp(OPERATOR_EMAIL, PASSWORD); const adminSet = await catalogPermissionSet(ql, 'admin_full_access'); expect(adminSet, 'admin_full_access is served by the security catalog').toBeTruthy(); - await ql.insert('sys_user_permission_set', { - user_id: await uid(OPERATOR_EMAIL), permission_set: adminSet.name, organization_id: null, - }, { context: SYS }); + await globalGrant({ id: 'ups_8158_operator', user_id: await uid(OPERATOR_EMAIL), permission_set: adminSet.name }); // Sign in AFTER the grants, so every session is minted by the same path // with the membership state above already in place. The org-less personas diff --git a/packages/qa/dogfood/test/wall-environment-metadata-refusal.dogfood.test.ts b/packages/qa/dogfood/test/wall-environment-metadata-refusal.dogfood.test.ts index c4ad838c4da..bc88ea803a9 100644 --- a/packages/qa/dogfood/test/wall-environment-metadata-refusal.dogfood.test.ts +++ b/packages/qa/dogfood/test/wall-environment-metadata-refusal.dogfood.test.ts @@ -144,8 +144,7 @@ async function ok(res: Response, status = 200): Promise { expect(res.status, await res.clone().text()).toBe(status); } -for (const posture of ['isolated', 'group'] as const) { - /** +/** * [C8 stage 8b] The organization a `single` session is active in: the Default * Organization, which an organization-less system grant is now stamped with * (ADR-0131 D9). A precheck that resolves with no organization would read the @@ -154,7 +153,8 @@ for (const posture of ['isolated', 'group'] as const) { const sessionOrgOf = async (h: Harness): Promise => (await (await h.stack.kernel.getServiceAsync<{ defaultOrgId(): Promise }>('tenancy')).defaultOrgId()) ?? undefined; -describe(`[ADR-0131 D12 items 7–8] ${posture}: every environment-metadata write is the platform operator's`, () => { +for (const posture of ['isolated', 'group'] as const) { + describe(`[ADR-0131 D12 items 7–8] ${posture}: every environment-metadata write is the platform operator's`, () => { let h: Harness; const previous = process.env.OS_TENANCY_POSTURE; const p = posture.slice(0, 3); From 327ad89907d48b1a7d9490e4f16710ad02738610 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 10:15:02 +0000 Subject: [PATCH 11/11] chore(changeset): a no-organization refusal names a composition that registers sys_organization with no auth plugin Claude-Session: https://claude.ai/code/session_01AZMXDVkiaxSpJkGXACuVwr Co-authored-by: Claude --- .changeset/15212-8b-single-platform-insert-stamped.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/15212-8b-single-platform-insert-stamped.md b/.changeset/15212-8b-single-platform-insert-stamped.md index 368ccfa25df..482d7ecf440 100644 --- a/.changeset/15212-8b-single-platform-insert-stamped.md +++ b/.changeset/15212-8b-single-platform-insert-stamped.md @@ -14,4 +14,4 @@ Clause-②: no (narrowing) - **Now, under `single`.** That insert goes through the same decision every application object already gets. With exactly one organization (the Default Organization), the row is stamped with it. With none, or with several, the insert is refused with `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` (status 500), and the message names the missing organization. Nothing is written. - **Unchanged.** Under a walled posture (`group` / `isolated`) the per-object exclusion stands as before; the walled half of this change ships in a later 18.x release. A write that carries an organization, on the execution context or on the record, is never re-stamped or refused. Objects with no organization column, objects declaring `tenancy: { enabled: false }`, and federated objects are outside the rule. Updates are untouched. The engine's diagnostics-only tenant-audit mute is unchanged. -**The remedy.** If a refusal names your insert, carry the organization on the write: `ql.insert(object, { ...row, organization_id: orgId }, { context: { isSystem: true } })`, or `{ context: { isSystem: true, tenantId: orgId } }`. On a fresh `single` deployment the auth plugin creates the Default Organization at boot, so a refusal reading `no-organization` means the write ran before it, or in a composition without the auth plugin's identity objects. +**The remedy.** If a refusal names your insert, carry the organization on the write: `ql.insert(object, { ...row, organization_id: orgId }, { context: { isSystem: true } })`, or `{ context: { isSystem: true, tenantId: orgId } }`. On a fresh `single` deployment the auth plugin creates the Default Organization at boot, so a refusal reading `no-organization` means the write ran before it, or in a composition that registers `sys_organization` but mounts no auth plugin to create the Default Organization.