Repository navigation
feat(objectql)!: under single, a platform object's organization-less system insert is stamped or refused (C8 stage 8b) - #24513
Conversation
…nization their context reads under (C8 stage 8c-T-α) 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 <noreply@anthropic.com>
…s 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 <noreply@anthropic.com>
… inserts stamped or refused Claude-Session: https://claude.ai/code/session_01AZMXDVkiaxSpJkGXACuVwr Co-authored-by: Claude <noreply@anthropic.com>
…s, or carry their organization Claude-Session: https://claude.ai/code/session_01AZMXDVkiaxSpJkGXACuVwr Co-authored-by: Claude <noreply@anthropic.com>
…he 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 <noreply@anthropic.com>
… rows beneath the engine Claude-Session: https://claude.ai/code/session_01AZMXDVkiaxSpJkGXACuVwr Co-authored-by: Claude <noreply@anthropic.com>
…g an author writes is renamed Claude-Session: https://claude.ai/code/session_01AZMXDVkiaxSpJkGXACuVwr Co-authored-by: Claude <noreply@anthropic.com>
…e session carries, land global grants beneath the engine, or carry their owner Claude-Session: https://claude.ai/code/session_01AZMXDVkiaxSpJkGXACuVwr Co-authored-by: Claude <noreply@anthropic.com>
…eg asks a unit the user is not yet in Claude-Session: https://claude.ai/code/session_01AZMXDVkiaxSpJkGXACuVwr Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 17 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9e57470d9ca98cc30657fcce1b1ca880b357417c && git checkout 9e57470d9ca98cc30657fcce1b1ca880b357417c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 913fd6df8a2b3ec82612bf35dc40f4e062c3077f 327ad89907d48b1a7d9490e4f16710ad02738610 && git checkout -B drift-repro 913fd6df8a2b3ec82612bf35dc40f4e062c3077f && git merge --no-ff 327ad89907d48b1a7d9490e4f16710ad02738610
node scripts/docs-audit/affected-docs.mjs --json 913fd6df8a2b3ec82612bf35dc40f4e062c3077f |
Contract reviewServed-tier: ① Derived judgments(a) The lift under
(b) The walled branch.
(c) Boot order: a fresh
(d) Out-of-scope finding 2: a non-system, organization-less write into a platform object.
(e)
(f) Tests.
② Semver level
③ Boundary flags
Rendered by an isolated at-tier reviewer subagent of the adopting seat; the seat adopts the verdict. Implemented-by: VERDICT: PASS |
…registers sys_organization with no auth plugin Claude-Session: https://claude.ai/code/session_01AZMXDVkiaxSpJkGXACuVwr Co-authored-by: Claude <noreply@anthropic.com>
Part of #15212
Clause-②: no (narrowing)
C8 stage 8b, with stage 8c-T-α folded in: its commit
5d1b91e9b(5 grant rows in 4 test files stamped with the organization their context reads under) is this branch's first commit. Shaped by epic ruling record 6259899486 on #15194: 18.0 is fresh installs,singleonly, and 8b lifts the exemption undersingleonly.What changes
Engine.resolveSystemInsertOrganization(packages/objectql/src/engine.ts) used to return before it read the posture for every in-reach platform object outside the 9-entry admit list. Those system inserts landed withorganization_idNULL under every posture.singlethere is no platform-object exemption now. A system insert into a platform object with a tenant column that names no organization goes through the same ADR-0131 D9 decision every application object gets: one organization (the Default Organization) stamps it; zero or several refuse it withERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED. The census puts 35 objects in this newly reached set (44 in reach, minus the 9 admitted).group/isolated) the design:isSystem写入是否在租户审计控制范围内?——#13178 类级装置(A/B/C)的共同前置,从未被裁过 #13491 exemption stands, as a named posture condition with a comment citing 6259899486 (item 4). It is not a per-table list.PLATFORM_OBJECT_TENANCYdoes not retire here. Two readers remain: the walled exemption above, and the engine's diagnostics-onlybypassTenantAuditisSystem mute (buildDriverOptions, every posture). The ledger header now says this. It retires with its walled reader in 18.x (D13). The census tooling and its JSON are unchanged and green, andlint.ymlis untouched.tenancy/platform-object-tenancy.tsandtenancy/system-write-organization.ts.packages/core/src/security/system-write-organization.tsneeded no change: its decision function is unchanged, and editing its JSDoc would change published.d.tstext for no behavioural reason.#14570 (
sys_business_unit_memberunadjudicated): undersingleits system-insert row is now stamped (pinned on the real showcase below). Under a wall it still lands NULL, because the exemption stands there in 18.0. So this is Part of #14570; the walled half rides 18.x.Boot order (the main risk), measured
On a fresh
singleshowcase boot (pnpm dev -- --fresh, SQLite) the boot is clean and the admin is seeded. Then:POST /auth/organization/invite-member, which writes the invitation row and the identity mail;A direct DB census after these steps found 0 rows with
organization_idNULL in every platform table that has the column, acrosssys_setting,sys_setting_audit,sys_member,sys_invitation,sys_email,sys_user_permission_set,sys_user_position,sys_audience_binding_suggestion,sys_inbox_message,sys_notification_receipt,sys_webhook,sys_business_unit, the approval family andsys_record_share. Every one was stamped with the Default Organization. No producer writes before the Default Organization exists, so no producer needed changing. (AuthPlugin.start()creates the Default Organization before the application plugins start, and the seed loader already stamps.)Tests
New pins
packages/objectql/src/tenancy-by-object-classification.test.ts:single, un-admitted platform objects derive the sole organization;code,statusandorganizationCount;isolated,group) still land unstamped.packages/objectql/src/system-write-organization.test.ts: the same split, single against walled, on the System-context writes land untenanted at RUNTIME, so a single-tenant install keeps re-forking the autonumber scope and minting duplicate business identifiers — the producer #8686's backfill cannot reach (17.0.0 GA) #8844 fixture.packages/qa/dogfood/test/showcase-single-platform-insert-default-organization.dogfood.test.ts, on the real showcase composition:isSysteminsert intosys_business_unit/sys_business_unit_membertakes the Default Organization;ambiguous-organization) and nothing is written.Tests 8b broke, and how each was fixed. No pin of a NULL reader was rewritten, and no test was deleted or skipped.
Fixtures that hand-registered
sys_organization/sys_useras bare schemas now declare them as the platform does (managedBy: 'better-auth', so no injected column). Otherwise the first organization's own insert was refused for want of an organization.runtime/src/system-write-tenancy-autonumber-split.integration.test.tsruntime/src/seed-tenancy-autonumber-split.integration.test.tsservice-messaging/src/notification-organization-follows-event.integration.test.tsplugin-approvals/src/status-mirror-cascade.integration.test.tsplugin-security/src/sys-audit-log-row-scope.test.tsplugin-security/src/granted-by-writer-provenance.test.ts(plus the Default Organization row asingleboot holds)PIN fixtures that model organization-less legacy rows now land those rows through the driver, as the residue an older release left. These are global grants, the retired catalogue bucket, unscoped platform-admin holders and the pre-change ledger. The readers they pin still read NULL rows until 8c (18.x), so each assertion still pins what it pinned.
plugin-security/src/__tests__/legacy-catalog-rows.testkit.tsand its plugin-auth copyplugin-security/src/:explain-enforce-parityexplain-removed-member-principalexplain-principal-personexplain-asker-gateorgless-position-name-foldcatalog-reference-reportbootstrap-platform-admin-existing-holder-scancontrolled-by-parent-chain-middle-mastersys-audit-log-row-scope(LEDGER_BEFORE)builtin-positions.boot(legacysys_positionrows)plugin-auth/src/:signup-existing-address-refusalsso-register-platform-admin-gatedriver-fault-auth-log-carriersscim-deactivation-reconcile-userruntime/src/seed-tenancy-autonumber-split.integration.test.ts([GUARD]platform residue)One positive control rewritten to the ruled behaviour. In
plugin-security/src/builtin-positions.boot.test.ts, the census control's organization-lesssys_permission_setinsert now reads back with the sole organization undersingle, and unstamped under the wall.Incidental fixtures that only needed an owner now carry one:
plugin-audit/src/identity-objects-feeds-off.test.ts;credential-at-rest-posture,scim-transaction-scope,scim-deactivation-reconcile-userandprincipal-less-producers-system-context. Their mint carries an organization; the verifier reads the digest alone. The plugin-auth engine pool refuses a database holding an organization row, so adding a Default Organization there was not an option.Dogfood fixtures. The ablation below proved each of these red only because of 8b.
tenancyservice. This replaces an empty-wherefindOnewhose swallowed refusal minted a second organization (the trap thatsys_business_unit_memberis unadjudicated inPLATFORM_OBJECT_TENANCY, so seed-replayed and system-written membership rows land organization-less #14570's remainder named):showcase-scope-depthshowcase-scope-depth-fallbackshowcase-scope-depth-writeactivity-withheld-updateidentity-admin-fields-org-peerwrite-door-unreadable-is-not-foundpredicate-write-unreadable-not-matchedparent-derived-write-refusal-not-visiblesharing-rule-org-less-callersinglesession carries. The HTTP doors in the same files use real sessions, and they were green throughout.wall-package-refusalwall-package-doors-refusalwall-authoring-trail-doorswall-environment-metadata-refusalcatalog-activation-doorpermission-set-clone-doorshowcase-permission-zoo(explain is asked in that organization)single-tenant-identity-create. A systemsys_teaminsert now takes the Default Organization; the pin used to assert NULL.The plugin-auth edits are test-only. No plugin-auth source changed. 8b-0f (#24453) had merged into this branch's base before they were made.
Verification
Everything below is at head
d1838bf96. Heavy runs went throughos-verify-lock.sh.Build and types
pnpm build --concurrency=2passed 76/76 tasks, after the secondorigin/mainmerge.Tests
Unit and integration, in the downstream consumers of
@objectstack/objectql, plus core. All 48 packages are green after the fixes (vitest per package). Among them:unit4610One cli file (
boot-preparation-parity) timed out at 5 s under parallel load; it passed alone, 37/37. The 18 originally red files re-ran green.objectql: 446 files, 8369 tests. The only 4 reds were the pins this PR rewrites; those two files now pass 66/66.
Dogfood (whole suite, single and walled lanes): 2291 passed, 18 skipped, 0 failed (
pnpm --filter @objectstack/dogfood exec vitest run).Fresh showcase boot: clean, with the admin seeded and 0 NULL-organization platform rows (see above).
A1: the walled branch is unchanged
singleand simulated a wall only in plugin services.Ablation
scripts/ablation-replace.mjsandablation-dist-preflight.mjs, with a trap restore.!== 'single'became!== '8b-ablated'). Results:dist, 3/3 went red.git diff HEADempty, blob hash matched, rebuild exit 0, marker--absentexit 0.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 80 commands, plus a 51-family artifact-roster block. All 128 ran, with exit codes captured before any pipe.Green: every family not listed below. That includes:
check-platform-object-tenancy-census,check-tenant-audit-censusandcheck-system-context-census;check-adr-0087-registration(not-required (no-migration-prescription)) andcheck-changeset-no-major;check:cross-package-test-inputs,check:test-source-alias,check:engine-double-contract,check:nul-bytesandcheck:type-check-debt.The families that read tests were re-run on the final head.
Shallow clone:
check-engine-split-ratio --days 90andcheck-plugin-teardown-shape --self-testrefused on the shallow clone at first. Both are green aftergit fetch --shallow-since=2026-07-14.NOT MEASURED locally:
check-closing-target-claim,check-single-claim-pathsandcheck-governed-queue-guardneed PR context that only CI supplies.check-partof-closing-keywordwas run against this body and is green.Acceptance notes
mintScimConnectionCredential(plugin-auth) has no production caller. Undersinglean org-less mint is now stamped with the Default Organization. That is behaviour-neutral, because the verifier never reads the organization.[sharing-rule] … expands to NO recipientsforshare_new_inquiries_with_field_opswithorganization: null(the rule itself, under regime C). It is not a NULL membership row, and this stage does not touch it.single, a non-system write with notenantIdalso reaches the decision. The resolver runs for any context without an organization, not onlyisSystem, so such a write into a platform object is now stamped or refused as well. That is consistent with D9 ("every writer").Generated by Claude Code