Repository navigation
fix(cli,plugin-security): os migrate organization-ownership --apply resolves the built-in positions and the everyone baseline - #23433
Conversation
… baseline in init() A boot that composes the security plugin for its declarations only runs init() and nothing after it, so a declaration made from start() or kernel:ready never reached that boot's catalog. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sSBzUJfLxTo3ZZ68LLfCG
…larations-only catalog Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sSBzUJfLxTo3ZZ68LLfCG
…)+start() grant goldens Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sSBzUJfLxTo3ZZ68LLfCG
…ons and the everyone baseline Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sSBzUJfLxTo3ZZ68LLfCG
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 15 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 6b5af90653261b3a1e062f6ef612e042966f01fc && git checkout 6b5af90653261b3a1e062f6ef612e042966f01fc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2659901c5ea8a174de4129264cb857e203d42b88 35ab3835fbd46f53731ba81e3da3fe29eee1d877 && git checkout -B drift-repro 2659901c5ea8a174de4129264cb857e203d42b88 && git merge --no-ff 35ab3835fbd46f53731ba81e3da3fe29eee1d877
node scripts/docs-audit/affected-docs.mjs --json 2659901c5ea8a174de4129264cb857e203d42b88
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 38537845899 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
Queue red, triaged ·
Generated by Claude Code |
…(queue removal: #23412 dropped the AnchorBindingContext import)
…e everyone baseline filter uses The merge of main brought the audience-anchor gate's deletion, which took the import with it; the init() baseline filter still names the type. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sSBzUJfLxTo3ZZ68LLfCG
|
Pointer from This PR's queue group is red on a semantic conflict with
Generated by Claude Code |
席位核对: PR #23433 at
|
Fixes #23397
Clause-②: no (the
authored-conversionguard and the rewrite gate are unchanged; the catalog they read gains what a started kernel already serves, perschema-migration-plugins.ts's "What a schema command needs from a plugin is its DECLARATIONS, and the kernel contract puts those ininit()"; no export is added)What was wrong
os migrate organization-ownership --applyboots the deployment's served composition with every platform provider composed for its declarations only. The security plugin'sinit()runs; itsstart()and every hook do not. The plugin declared the six built-in positions instart(), andeveryone's baseline from akernel:readyhook registered instart(). So the ceremony's catalog held neither, and every 17.x database's seededeveryonetomember_defaultbinding blocked the ceremony.What changed (route (a) of the card, inside
plugin-security)SecurityPlugin.init()declares the six built-in positions, witheveryonenaming the deployment's baseline (the app'sisDefaultset composed withmember_default), through the sameregisterBuiltinPositions/declareEveryoneBaselineseam as before. These are registry writes only. No database read or write is added.init()is the fail-closed reading: the high-privilege check runs with no declared-capability context, because the stack's declarations are still arriving while plugins initialize. With no context a token is never excused, so this list is never wider than the onekernel:readyderives.kernel:ready'sdeclareEveryoneis unchanged in what it computes. It runs the check against every declared capability, adds the accepted package defaults (a database read, which stays there), and replaces theinit()declaration. The baseline filter is one private method that both call (everyoneBaseline), so there is no second derivation.init(). It still fires when the engine exposes no registry. When no engine is registered,init()says nothing, becausestart()already reports that.organization-ownership-apply.ts,organization-ownership-plan.tsorschema-migration-plugins.ts.What a started kernel sees
Its catalog after boot is the same. Three things move:
everyone's baseline are in the engine registry from the plugin'sinit()instead of fromstart()andkernel:ready. Any reader between the two now sees the final names earlier.kernel:readyderivation throws,everyonekeeps theinit()baseline. It used to name no set.init().Unchanged: the cold boot's environment-held-name refusal skips the built-in names, as it did. A stored definition under a built-in name still shadows the declaration at read. objectql's
protocol-boot-hydration-scoped.test.tscontrol already declares the built-in in Phase 1 and holds both.Measurement of the dispatch hypotheses (on
5656c5638)registerBuiltinPositionswas instart(), anddeclareEveryoneBaselinewas insidedeclareEveryone, which ran from akernel:readyhook registered instart(). The apply boot composes the security plugin twice: once throughcomposeAuthGatedSecurity(composeForDeclarations) and once in the served platform's auth family (composeProviderForDeclarations). The kernel keeps the later one, so the surviving instance is the provider posture, which withholds every hook includingkernel:ready. Measured: the boot logs "Plugin superseded: 'com.objectstack.security' — the later registration REPLACED the earlier one". So onlyinit()runs, and akernel:readyhook registered frominit()would not have reached the ceremony either.ql.registryis reachable ininit(): the plugin depends oncom.objectstack.engine.objectql, andinit()already reads that plugin'smanifestservice. No reader depends on the declaration happening instart(). The cold-boot check skips the built-in names, the item seam's built-in carve-out holds in either order, and the plugin-security, plugin-auth, rest, runtime and organizations suites listed below are green.authored-conversionand the rewrite gate compare the binding rows against thepermissionSetsthat the catalog's position definition names (declaredPositionSets, which feedsconvertPositionBindingRows). The fate must bedeclared.authored-conversionadditionally needs the position to exist. So both need the built-in position and the deployment baseline. The accepted-default half is asys_audience_binding_suggestionread and stays atkernel:ready; the ceremony does not read it (Acceptance notes).os migrate plan. The no-write pins stay green:schema-migrate.host-composition.integration.test.ts("writes NOTHING while planning — no host seeder runs", "still writes NOTHING — the declaration-phase suppression is intact", thecomposeForDeclarationssuppresses onlystart(), so aninit()-registeredkernel:readyhook still writes duringos migrate plan— the guarantee holds only for hosts following an unwritten convention #13332 "THE FIX: the declaration boot lands none of them"),schema-migration-plugins.declaration-boot-write-guard.test.tsandsecurity-catalog-overlays.integration.test.ts: 3 files, 40 tests passed.Proof
The card's own case, the real command. I ran
os migrate organization-ownership --apply --backup-taken --jsonin-process against a database that this release's apply composition provisioned, plus the 17.x rows: the seededeveryoneposition,member_defaultand the binding between them, with noorg_memberbinding.singleauthored-conversionblocked: "Binding rows give position everyone the sets member_default, and the catalog holds no position of that name to carry them."authored-conversiondone,rewritedone,mirror-deletiondeleted 3OS_MULTI_ORG_ENABLED=true(postureisolated)rewriteblocked: "Binding rows give position everyone sets its catalog definition does not name (member_default); deleting them would lose a grant."rewritedone,mirror-deletiondeleted 3After the fix, both runs stop at
column-dropsonsys_metadata*.organization_id("not proposed for dropping"). That is the inventory'scolumn-droprows, out of bounds here.The pins:
packages/cli/src/utils/organization-ownership-apply.served-catalog.integration.test.ts. It runs the apply command's boot flag for flag, with core's catalog reader, against the 17.x rows plus a control binding tozz_undeclared. It checks that the six built-ins resolve andeveryonenamesmember_default. Undersingle,authored-conversionblocks on the control alone. Walled, the rewrite gate blocks on the control alone. A real conversion still blocks when a binding names a position that is absent.packages/plugins/plugin-security/src/builtin-positions.declaration-phase.test.ts. It runsinit()alone over a real engine registry. The six resolve, owned by the plugin.everyonenames the app default and thenmember_default. The fail-closed reading leaves out a set whose token only a declaration excuses, and says nothing. The warning fires on an engine with no registry, and stays silent when no engine is registered.Negative control (one-shot, at
7f3b67b69). I restoredsecurity-plugin.tsfrom the base (on-disk hashb042c660f9equals the base blob) and rebuilt plugin-security.ablation-dist-preflight --absentshowed the init call gone from all 6 built files. Then:Restored with
git checkout HEAD: hash7a64f14215equals the HEAD blob, andgit diff HEADis empty. I rebuilt, and the preflight found the marker in 2 built files. Both pins are green again.The grant goldens moved by one recorded step
Four plugin-security goldens record a harness that runs
init()andstart()but fires nokernel:ready:grant-permission-set-name.equivalence.test.tsgrant-permission-set-name-backfill.test.tsgrant-readers-by-name.golden.test.tsresolve-authz-grant-set-by-name.golden.test.tsIn that harness
everyoneused to name nothing. It now names the baseline, so every principal holdingeveryonegainsmember_default, and nothing else moves. That is what a started kernel'skernel:readyderivation has always given:builtin-positions.boot.test.ts's grant golden, which fires the hooks, already carriesmember_defaultfor every principal and is unchanged. Each golden's note records the move. The backfill's "an unnamed grant confers nothing" check now asserts exactly['member_default'](whateveryonenames) instead of[].Verification (final HEAD
1ebf9c5d7)pnpm --filter @objectstack/plugin-security exec vitest run: 186 files, 3781 passed, 45 skipped.typecheck: exit 0.@objectstack/cliunit tier, in three shards: 96 + 96 + 95 files. Twopublished-subpath-*pins first reported PREREQUISITE NOT MET because the cli was not built; afterpnpm --filter @objectstack/cli buildthey pass, 29 tests. clitypecheck: exit 0.kernel:ready: plugin-auth 5 files / 41, rest 9 files / 120, runtimeshare-links-enforcement-context28, organizationscreate-explicit-organization-wall21. All green.dispatch-gates --ran: 68 derived, 68 run, 0 NOT-MEASURED, every family exit 0.check:dual-build-cjs-loadsandcheck:i18n-coveragefirst answered PREREQUISITE NOT MET (exit 3, dist of unrelated packages); re-run with the dist present, both exit 0.pnpm lint(fulleslint .) at1ebf9c5d7: exit 0.Acceptance notes
everyoneis a database read and stays atkernel:ready. A binding to a set that only a deployment-level acceptance puts oneveryonestill blocks the ceremony. A 17.x database holds no such acceptance (rows resolved before the deployment-level decision are re-opened as pending), so the card's population is unaffected. This is measured by reading, not reproduced. Carrier: none.packages/objectql/src/registry.tsaround line 2795 and line 4468 say the platform declares its built-ins "in its ownstart()". It now does so ininit(), andeveryoneis re-declared atkernel:ready. The built-in carve-out they explain is still load-bearing for that re-declaration. Not edited here: outside the claimed file surface. Carrier: none.start(). No ceremony step resolves a capability from the catalog:authored-conversionreportssys_capabilityrows without a catalog read. So this card does not need them.authored-conversiononeveryone, with the "Declare the position (or remove the bindings)" remedy. That is the same cause and remedy shape os migrate organization-ownership --apply, run where the security plugin is not composed (no auth secret), blocks its rewrite on the platform's own shipped permission sets and prescribes removing the admin's grants, without naming the cause #23359 names for the shipped sets. os migrate organization-ownership --apply, run where the security plugin is not composed (no auth secret), blocks its rewrite on the platform's own shipped permission sets and prescribes removing the admin's grants, without naming the cause #23359 remains open.Generated by Claude Code