Repository navigation
fix(cli,runtime): the 17 → 18 ownership ceremony leaves no row without an organization on a single-posture database, its prescribed boot writes nothing before the guard refuses, and an unverified ceremony is not served - #24325
Conversation
… ceremony; the ceremony creates the Default Organization Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
…efusal, and the ceremony's Default Organization Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
…the ceremony creates the Default Organization only when a row falls to it Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
…e guard and the unverified-marker refusal Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
… boot guard ahead of them Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
…s package-internal and hosts walk the refusal's cause chain; plugin-auth exports the invariant alone Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
…holds the inline seed behind the guard Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
…remony-null-org-guard Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 51 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 36b13634f5a9a9c7f4257a13997c5a7e2c8b265f && git checkout 36b13634f5a9a9c7f4257a13997c5a7e2c8b265f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ec749fbb44b5e6015d3f205c63dfa47cfb6ef445 eb9605968ec4cb423d08c29a7931f468a02b3da8 && git checkout -B drift-repro ec749fbb44b5e6015d3f205c63dfa47cfb6ef445 && git merge --no-ff eb9605968ec4cb423d08c29a7931f468a02b3da8
node scripts/docs-audit/affected-docs.mjs --json ec749fbb44b5e6015d3f205c63dfa47cfb6ef445
|
席位核对 — REWORK (patch round 1)
Accepted as it stands:
Owed in this PR:
Out-of-scope notes, settled:
Before pushing: read this head's Generated by Claude Code |
… check:init-service-contract reads optionalDependencies Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
…ariant does — slug default, else the sole organization; pin both definitions over one fixture set; the guard's fresh-database marker read is pinned absent Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
…rd reads the marker as absent when no driver serves sys_migration Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
… and imports the invariant statically Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
席位核对ACCEPT ·
Generated by Claude Code |
Fixes #24282
Clause-②: yes (widening)
Basis for the Clause-② line, as the seat re-declared it on the claim: one new entry export,
ensureDefaultOrganizationExistsfrom@objectstack/plugin-auth(the package entry lists 221 names onmainand 222 here, the list is below). The@objectstack/runtimeentry is byte-identical tomain(src/index.tsunchanged, 637 names before and after), and@objectstack/cliexports nothing new. No published schema and no REST answer moves.This is leg A of #24282: the organization-ownership ceremony and the v18 boot guard. Leg B, where the seed loader refuses an organization-scoped twin (
packages/metadata-protocol), is tracked on #24301, and #24301 is not addressed here. Repairing a hosted database's existing twins is an operator's data repair. It is not this PR.What changes
packages/cli). On a database that holds no organization at all, with rows that no anchor names,os migrate organization-ownershipnow plans those rows as the Default Organization's. The plan says so in a warning (defaultOrganizationCreatedByApply: true).--applythen creates the Default Organization before its first write and gives it every such row. A run that cannot create it blocks at attribution and says why. It no longer checkpoints attribution done at zero rows and then certifies.single, the Default Organization". The ceremony's attribution is a write that needs an owner, so the owner must exist before it runs.single, the Default Organization is the organization withslug = 'default', or else the sole organization whatever its slug. That is the find half of plugin-auth's invariant and the tenancy service'sdefaultOrgId(), and ADR-0093 D3 states it: "the bootstrap org (stableslug='default'), or the sole org row if a deployment renamed it". ADR-0131 has no text that limits it to the slug.defaultstill name none, and their rows are reported. The plan file'sdefaultOrganization.slugis now that organization's own slug. The plan file is the command's output, not a published schema.ensureDefaultOrganizationExists, which is the functionAuthPlugin.start()and the identity-objects plugin call. So the row is the one a single-posture boot would have created, and it has one producer.packages/cli). This is the card's step 5. The step writes only whereorganization_id IS NULL, so it never derives an owner a second time (D10 item 3). It covers D10 item 4's "resolves them and re-runs". It also completes a database that an earlier release's ceremony left with attribution checkpointed at zero.packages/runtime,packages/plugins/plugin-auth).OrganizationOwnershipBootGuardPluginnow judges in its ownstart(), not atkernel:ready.start()runs the additive schema sync that the rewrite step's "boot this release once" relies on, and after the platform objects.AppPlugin(the inline seed),AuthPluginand the identity-objects plugin (the Default Organization) now list it inoptionalDependencies, so the order is declared and does not depend on registration order.os serveregisters it before them as well.'com.objectstack.organization-ownership-boot-guard', becausecheck:init-service-contractreadsoptionalDependenciesonly as string literals. An identifier element there made the guard readAppPlugin's whole array as undeclared, which is CI's red ona91dad2d8, fixed in round 1.start()that throws is reported by the kernel as its own error, with the refusal as itscause.os serveandos devnow read the refusal from that chain, soos dev's automatic upgrade and the printed refusal text are unchanged.packages/runtime). Once no table is left in the v17 shape, a boot whoseadr-0131-organization-ownershipmarker exists but is not verified is refused withORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED. That state is the one where the post-check found rows the ceremony did not carry, and the refusal names the re-run. D10 item 5 refuses a database the ceremony has not completed, andverified_atis the ceremony's completion marker. A database with no marker (a fresh one) boots as before, and a marker that cannot be read is refused rather than passed.os servewith noOS_DATABASE_URLand no driver, so no driver servessys_migration) has no ledger to judge, and it boots as before. The guard checks this the same way its column probe skips an object no driver serves.Test Core3/6 and 4/6 ona91dad2d8(four storage-lessos serveintegration tests). Fixed in round 1.The measured path, before and after
This is the card's path, run with the real commands (
os servewithOS_MIGRATE_AND_EXIT=1for the boots,os migrate organization-ownershipfor the ceremony) on one SQLite file.The file was built from a served boot of this release. It was then put into the 17.x shape: no organization, every
organization_idNULL,sys_jobstill carrying its retired column, andsys_user_permission_setwithoutpermission_set, so the rewrite step asks for a boot. Its seed holds 5 rows of an application object, plus 5 NULL rows insys_activity,sys_settingandsys_setting_audit.main(d537a318b)defaultOrganizationCreatedByApply: true, and the warning says--applycreates the Default Organization and gives it 10 rows--apply#1donewithrowsAttributed: 0, rewrite blocked ("Boot this release once…"), exit 1donewithrowsAttributed: 10, rewrite blocked, exit 1sys_organization0 → 1 and stamping all 5 seeded rows with the new organization--apply#2checkpointed,complete: true, blocking: 5(sys_activity2,sys_setting1,sys_setting_audit2), marker applied withverified_atNULL, exit 1complete: true, blocking: 0, marker verified, exit 0The reproduction ran on
a91dad2d8, and round 1 does not change this path (the database holds no organization). The reproduction is not a permanent test. It is pinned instead by the command-level test below.Public surface
The
@objectstack/plugin-authentry adds exactly one export and removes none. Its full signature, from the builtdist/index.d.ts, with each generic spelled out in words so the platform's body sanitizer leaves it intact:The two interfaces are reached through the signature. They are not entry exports. Apart from that export line and these three declarations, the
dist/index.d.tswith comments stripped is line-for-line unchanged.Entry exports after this change, sorted (222 names). Before is the same list without
ensureDefaultOrganizationExists(221 names):@objectstack/runtime's entry is unchanged. Its declarations differ in one line, which is not a type change:readonly name = "com.objectstack.organization-ownership-boot-guard"becomesreadonly name: "com.objectstack.organization-ownership-boot-guard", the same literal type, now taken from a package-internal constant thatAppPluginreads. The one behaviour a host can observe is that a host composing the guard now sees the kernel's start-failure error with the refusal as itscause. The changeset says so, with the one-line read.Why the export, and not a narrower route.
default-organization-invariant.tskept the invariant internal so that no host composing a kernel has to remember to call it. That still holds: the module header now says so, and only the two plugins call it on a boot. The ceremony is not a host composing a kernel. It is the one caller that is not a boot, and the boot that would have created the organization is the one the guard now refuses before any plugin writes. The routes that need no export are either a second producer of the Default Organization row (an engine insert in the CLI) or callingIdentityObjectsPlugin.start()by hand outside a kernel. The first forks the invariant and the second is a workaround.Files outside this lane
These files belong to
domain:services(plugin-auth), and the seat declared them on the lane post:packages/plugins/plugin-auth/src/index.tsdefault-organization-invariant.ts(the header note)auth-plugin.tsandidentity-objects-plugin.ts(the guard inoptionalDependencies, plus the note)default-organization-guard-ordering.pin.test.tsThe changes there are the producer side of points 1 and 3.
packages/qa/dogfood/test/organization-ownership-boot-refusal-order.dogfood.test.tsis updated to read the refusal from the cause chain.Tests
packages/runtimeorganization-ownership-boot-guard.kernel.test.tsboots a real kernel over SQLite. In the hostile slot,AppPluginand plugin-auth's identity-objects plugin areuse()d before the guard, and the refused file holds no organization and no seed row.AppPluginalone gives the same result. Control: the same composition over a fresh file writes both. It also covers the unverified-marker refusal with no v17 column, plus its verified control.organization-ownership-boot-guard.test.tscovers the start-phase refusal registering no hook, the declarations, the unverified, unreadable and absent marker cases after the column drops, a boot with no storage, and the existing cases.packages/plugins/plugin-authdefault-organization-guard-ordering.pin.test.tsshows that the declaration moves the order forAuthPluginandIdentityObjectsPluginin a hostile registry, that removing it reverts the order, and that it is order-if-present.packages/cliutils/organization-ownership-apply.test.tscovers creation before the first write, the marker verifying, a missing or failing producer blocking at attribution, and the controls (no row falls to the Default Organization, a walled posture, organizations present with none the Default Organization). It also covers the checkpointed attribution running again over the state an earlier run left.utils/organization-ownership-apply.test.ts). Over the same SQLite fixtures, the plan'sdefaultOrganizationand plugin-auth'sensureDefaultOrganizationExistsname the same organization in each case:defaultpresent beside another organizationdefault(both name none; the invariant answersambiguous)--applycreates it; the invariant creates it, and the next plan names it)--apply, and the marker verifies.utils/organization-ownership-plan.integration.test.tscovers the plan's count and warning, the sole-organization case, and the walled control.commands/migrate/organization-ownership.no-organization.integration.test.tsruns the card's path through the real command: plan,--apply, the guarded boot refused with every row count unchanged,--applyagain verified, then the guarded boot starts with no duplicate.Round-1 results on head
eb9605968, through the shared verify lock:os serveintegration files CI found red ona91dad2d8(serve-no-transport-announcements(both),serve-no-storage-boot-guard,serve-kernel-only-lifetime,serve-no-server-banner), 29 files: 275 passed, 1 skippedRound 0, on
a91dad2d8:Full suites, on the round-0 pre-merge head:
unitproject: 308/310 files. The 2 remaining failed only becausepackages/cli/distwas not yet built (published-subpath-*.pin). They passed on the final head after the build.Incoming
maintouched none of these packages' sources other than a runtime test and a dev alias.The touched dogfood file (
organization-ownership-boot-refusal-order.dogfood.test.ts, the showcase composition upgraded through the refusal) was run once on the final head: 4/4 passed. In that run the refused boot still adds the column the ceremony waits for, the ceremony verifies, and the next boot starts.Ablations. Each one was mutated through
scripts/ablation-replace.mjs, which verifies the mutation landed, and restored, with blob equal toHEADandgit diff HEADempty:guard back at
kernel:ready: 4 red, including both "writes nothing" casesAppPluginwithout the guard declaration: 2 red (the declaration pin and theAppPlugin-alone case). The all-writers case stays green, because the identity-objects plugin's own declaration still pulls the guard ahead.no marker read once the columns are gone: 3 red
attribution without creating the Default Organization: 3 red
the plan reporting instead of counting: 4 red
a checkpointed attribution never re-run: 1 red
round 1, the plan's Default Organization reverted to slug-only: 3 red (the one-definition pin's sole-organization case, the plan's sole-organization case, and the
--applysole-organization case)round 1, the storage-less ledger check removed: 1 red
A first attempt at the
AppPluginleg was refused by the tool (a delete written as a replace) and was re-run as--delete.Gates (round 1, head
eb9605968).node scripts/pm/dispatch-gates.mjs --commandsderives 80 commands, and all 80 exit 0.check:dual-build-cjs-loadsandcheck:i18n-coveragefirst exited 3 on missingdist/and exited 0 after building what they named.--ranreconciliation: 80 derived, 80 run, 0 NOT-MEASURED.dispatch-gateswithout--commands, whose population covers the changed packages:check:init-service-contractexited 1 ona91dad2d8(reproduced locally:packages/runtime/src/app-plugin.ts:710 — AppPlugin) and exits 0 on the fix.check:live-db-isolation,check:optional-error-sink,check:resume-authority-declared,check:runner-env-posture,check:settings-bind-window,check:startup-registry-verdict,check:verify-stand-inandcheck:wildcard-fallthrougheach exit 0.check:meta-type-normalizedreadspackages/rest/srconly, which this diff does not touch.pnpm exec eslint --no-inline-configover the 18 changed source and test files exits 0.Acceptance notes
ORGANIZATION_OWNERSHIP_MIGRATION_ID(packages/spec/src/system/migration.zod.ts) describes the refusal only for a database still in the v17 shape. It is now narrower than the guard, which also refuses an unverified marker after the column drops.packages/specis not this lane's, so the docblock is left as it is.sys_user_permission_setadmin_full_accessgrant withorganization_idNULL. The same boot over a fresh file wrote one too. That is ADR-0131 D5's platform-admin grant row, whose Default-Organization ownership D5 assigns to C3. The seat dropped this note as covered by refactor(plugin-security,platform-objects,spec): retire the catalog seeders, the per-organization catalog machinery and the four catalog objects; Setup creation is an environment write undersingleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204 (C3; D5's stated interim).Generated by Claude Code