Repository navigation
fix(cli): the ownership ceremony names the uncomposed security plugin, never removal, for a shipped set or built-in position - #23540
Conversation
… for a shipped set or built-in position (WIP) Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
…ership ceremony (WIP) Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
…he command's wiring (WIP) Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
…ed boot (WIP) Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
…ach pin Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
…composed-security-remedy
📓 Docs Drift CheckThis PR changes 1 package(s): 12 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 — 28 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 bed447fa05cba2c44e69f0c6da4bfe7f246dd34e && git checkout bed447fa05cba2c44e69f0c6da4bfe7f246dd34e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fba37122b0f6646a3d8dd0b5384319e8bed3ae12 7d73667169af10c1a96443b13010d3654eca3cc7 && git checkout -B drift-repro fba37122b0f6646a3d8dd0b5384319e8bed3ae12 && git merge --no-ff 7d73667169af10c1a96443b13010d3654eca3cc7
node scripts/docs-audit/affected-docs.mjs --json fba37122b0f6646a3d8dd0b5384319e8bed3ae12
|
席位核对✅ ACCEPT: PR #23540 at
Checklist, read against GitHub and
Accepted deviations:
Out-of-scope findings, one line each:
Landing: Generated by Claude Code |
Fixes #23359
Clause-②: no
What was wrong
os migrate organization-ownership --applycomposes the security plugin only whereos servewould: behind the auth gate, which needsOS_AUTH_SECRETor a development boot. That plugin declares the platform's shipped permission sets and the built-in positions. Run without a secret, the ceremony's catalog resolves none of them, and the ceremony blocked with a remedy that pointed at the rows, not at the cause.Measured on
mainbefore this change: 4 remedy sentences in 2 steps, all from this one cause.rewriteadmin_full_access/organization_admin_no_bypass(the card's setup)rewritepermission_setempty) whose set row isadmin_full_accessauthored-conversioneveryonebinding (therepo:cloud#1instance,6167973978)rewritegate, under a walleveryonebindingThe dispatch's mechanism hypothesis (a) named the first and third rows. I measured the second and fourth in the same function as the first (
verifyReferences, the rewrite step's gate). Both have the same cause, and the second prints removal advice for a shipped set, which triage's ruling forbids ("Never print "remove the grants" for a shipped set."). So this PR covers all four. Each one is pinned, and each is listed under Acceptance notes.What changed
ApplyOptions.securityPlugincarriesbootSchemaStack's owncomposition.securityPlugin, the same fieldos migrate security-catalog-overlaysprints. The command passes it at the head of theapplyOrganizationOwnershipoptions literal (one line).uncomposedSecurityRemedy(inorganization-ownership-apply.ts) answers only when the boot reported the plugincomposed: falseand the name is one the plugin declares:securityDefaultPermissionSets(@objectstack/plugin-security), the list its manifest registers;BUILTIN_IDENTITY_NAMESandAUDIENCE_ANCHOR_POSITIONS(@objectstack/spec), the two lists the plugin'ssecurityBuiltinPositionsis built from (that list is not exported).describeSecurityPluginComposition(status), followed by the one instruction that holds for every gate reason: "NAME is one of the permission sets it ships: keep the grants, and re-run where the security plugin is composed." For a position it says "built-in positions it declares" and "keep the bindings". No third spelling of the composition, and no edit toschema-migration-plugins.ts.unnamedForCompositionrerunsplanGrantRewriteover the unnamed grants. Only itsunresolvedreason, for a shipped name, moves to the composition blocker ("their set row is NAME, which the security catalog does not resolve."). A grant unnamed for any other reason (no id, a missing row, another organization's row) keeps the general blocker.Measured once on a real boot with no secret (
bootSchemaStackwith the command's four composition flags,NODE_ENV=production, noOS_AUTH_SECRET; a throwaway file, deleted after the run and not committed):composition.securityPluginwas{"composed":false,"reason":"no-secret"}, and none ofadmin_full_access,organization_admin_no_bypassoreveryoneresolved. The card's two named grants then blocked with "1 grant(s) name the permission set admin_full_access, which the security catalog does not resolve (grant_admin). Did not compose the security plugin:os servecomposes none here (no OS_AUTH_SECRET, and not a development boot: no --dev, and NODE_ENV is not development), so its shipped permission sets are not package-held names on this deployment's boot. admin_full_access is one of the permission sets it ships: keep the grants, and re-run where the security plugin is composed."organization_admin_no_bypassgot the same sentence. A 17.x binding toeveryonegot the position form, inauthored-conversion(single) and in the rewrite gate (isolated). A binding to the undeclaredzz_undeclaredkept today's remedy in both.Pins
organization-ownership-apply.test.ts, describe "a name only the uncomposed security plugin declares". Each case reads its unknown-name control before its shipped name:rewrite: grants namingadmin_full_accessandorganization_admin_no_bypassget the composition and no "remove". A grant namingzz_unknown_setkeeps "or remove the grants". Mirror deletion is not reached and nothing is deleted.rewrite: a legacy grant whose set row isadmin_full_accessgets the composition and no "remove". A legacy grant on an unknown set row keeps "name or remove each".authored-conversion: theeveryonebinding gets the composition and no "remove".zz_undeclaredkeeps "or remove the bindings".composed: true, and absent): both steps keep every remedy and print no composition sentence.organization-ownership-apply.served-catalog.integration.test.ts: the composed control on the real served boot.composition.securityPluginis{ composed: true }, and the rewrite verifies grants namingadmin_full_accessandorganization_admin_no_bypass, 2 of 2. Itsceremony()helper now passes the boot's ownsecurityPlugin, as the command does.organization-ownership.fate-supplement.integration.test.ts: the command hands the boot'scomposition.securityPluginto the apply step. The boot double gained thecompositionfield the command now reads.Ablations (
node scripts/ablation-replace.mjs, WRAP mode; restore proven as blob == HEAD with an emptygit diff HEAD):if (!status || status.composed) return undefined;becomesreturn undefined;). The 4 uncomposed pins go red at their shipped-name assertion (lines 2054, 2079, 2096, 2113), after their unknown-name assertions held. The 4 control cases stay green:Tests 4 failed | 4 passed. Restored tod3dd0eda148f.a4ce0f93c21f.Verification
On the final head
7d73667169. It mergesorigin/mainfba37122b0, which touches onlypackages/lint; that package was rebuilt.pnpm --filter @objectstack/cli typecheck: exit 0, includingcheck:test-typecheck.Tests 101 passed.pnpm lint's own--no-inline-config: exit 0. This is a narrowedpnpm lint; why that is sound is under Acceptance notes.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 65 commands.pnpm check:dual-build-cjs-loadsandpnpm check:i18n-coverageexited 3 (PREREQUISITE NOT MET), because this worktree has no fullpnpm build. NOT MEASURED locally: CI'sLint & Repo Gatesruns both.--ranwith each exit code recorded: "65 derived famil(ies) accounted for — 63 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)", 0 UNRUN.On
563552a332. The source is the same; the later commits only reorder assertions in one test file and add the changeset and the merge.@objectstack/cliunit project, the whole of it:286 passedfiles,4286 passed | 29 skippedtests. Two suites first failed for a missing prerequisite ("packages/cli is not built"), so they were not measured on that run. Afterpnpm --filter @objectstack/cli buildthey passed (36 tests, withorganization-ownership.apply-flags.test.ts).Test Files 6 passed,Tests 195 passed.Acceptance notes
Scope beyond the two named sites. The two added sentences are in the
rewritestep's own gate (verifyReferences), where no other claim holds code. Without them, a 17.x database (legacy grants) or a walled posture would keep the card's defect.The control route. The composed control runs on
OS_AUTH_SECRET(the served-catalog suite's existing boot), not onNODE_ENV=development. The auth gate answers{ composed: true }on either route, and no second boot was added.No committed no-secret real-boot pin. The served-catalog suite measured
Duration 28.28s, of whichimport 23.29s, so a second file would pay that load again. The chain is pinned in three links: the boot's no-secret answer (security-catalog-overlays.integration.test.ts, existing), the command's wiring (new), and the remedy given the answer (new). The real no-secret boot was measured once (above).describeSecurityPluginComposition's no-secret reason names--dev.--devisos serve's flag; this command has none. It is left unchanged, as the dispatch requires:schema-migration-plugins.tsis reused, not edited.The rewrite's notes are unchanged. The note "unnamed grant(s) point at a set row whose name the security catalog does not resolve" still lists a shipped-set grant without the cause. The blocker beside it now names the cause.
Why narrowing
pnpm lintto the changed files is sound. Three facts:dispatch-gateschange set, merge basefba37122b);eslint.config.mjsenables no type-aware or cross-file rule ("never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file"), so no other file's verdict can move;pnpm lint's own--no-inline-configand exited 0.CI runs the full
pnpm lint.Out of scope, found while measuring, reported to the seat and not changed here. Under a wall, a 17.x binding to a position no catalog declares blocks the rewrite gate with "The authored-conversion step carries them; resolve its blocker". With the plugin composed or not, that step only reports under a wall, so it has no blocker to resolve. The
zz_undeclaredcases above reproduce it.Generated by Claude Code