Repository navigation
feat(cli): os migrate security-catalog-overlays — list, and with --apply delete, the environment rows a v18 cold boot refuses - #22523
Conversation
…r env rows the cold boot refuses Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <[email protected]>
…sal's remedy Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <[email protected]>
…notes Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <[email protected]>
…sites pin Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <[email protected]>
…erlay-cleanup-step
📓 Docs Drift CheckThis PR changes 5 package(s): 40 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 153 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 7eb97d19fc0b7c7237abf2363533a2db662ad347 && git checkout 7eb97d19fc0b7c7237abf2363533a2db662ad347
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ce78ff7bcd850f440b2bacc25a35cedbcc847c56 9b0c250a70732c33a0b460cbe6e058e4be75983a && git checkout -B drift-repro ce78ff7bcd850f440b2bacc25a35cedbcc847c56 && git merge --no-ff 9b0c250a70732c33a0b460cbe6e058e4be75983a
node scripts/docs-audit/affected-docs.mjs --json ce78ff7bcd850f440b2bacc25a35cedbcc847c56
|
…erlay-cleanup-step
…otocol 18 for the step's D3 entry Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <[email protected]>
… and occupancy tables Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <[email protected]>
Contract reviewServed-tier: Inputs, and nothing else: card #22371 (body and all 14 comments, rulings 6073500921 A′ and 6074838935 B included), PR #22523 (body, the 23-file list, the net diff of the branch against its merge-base with ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL On ② alone: Generated by Claude Code |
…eset and --dev, read through serve's own rules isDevelopmentBoot (core) is serve's isDev; stackBootPlugins (cli) is serve's devPlugins merge; the preset options are STACK_TIER_PRESETS' keys in both commands. The 22371 changeset names @objectstack/spec and its D3 entry; the entry, the 22307 note and the cli docs say to run the step with the flags and environment the deployment boots with. Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <[email protected]>
Contract reviewServed-tier: Re-review after the FAIL record 6087652785 at ① Derived judgmentsCarried from the earlier record and re-read against this head, every item the round touched re-judged on the new code:
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS The ② defect of the earlier record is fixed on this head ( Generated by Claude Code |
…erlay-cleanup-step
…the merged migration registry os-regen-merge step 4: main's storage-scope-public-retired entry and its flow-value-slot-template-dialect-refused edit join this branch's security-catalog-environment-overlay-refused. registry.ts regenerates to the merged bytes unchanged. Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <[email protected]>
Contract reviewServed-tier: Fresh review on the head that moved after the PASS record 6088873078 at What the hop did to the diff, measured. The PR's own diff is byte-identical across the hop:
The rest of ① Derived judgmentsEvery judgment of the PASS record re-read against the merged code at this head; each item names what is right or wrong.
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS The PR's own diff is byte-identical to the one the PASS record at Generated by Claude Code |
…erlay-cleanup-step
|
Regen-provenance: 6090076525 ·
The head moved after the contract review PASS
The record carries to Generated by Claude Code |
Landing pre-checks at
|
Fixes #22371
Clause-②: yes (widening)
Item 1 of ruling A′ (record 6073500921), as amended by ruling letter B (record 6074838935): the offline step that lists, and with
--applydeletes, the environment-wide rows a v18 cold boot refuses. Item 2 (retiring the three in-kernel remedies inplugin-security) is a separatedomain:servicescard and is not in this PR.What it is
os migrate security-catalog-overlays, a sibling in theos migratefamily (themeta --storedconventions: preview by default,--apply,--yes,--force,--json,--database-url/$OS_DATABASE_URL).Why a sibling, not a mode of
os migrate meta --stored. The ruling namedmeta --storedas the nearest landing, and the step departs from it on three counts:--stored --applyrewrites every stored row throughsaveMetaItem, while the step deletes one refused population. Under one--apply, the flag would carry a second, destructive meaning.--storedboots without the host config and hydratessys_metadata, which is exactly the boot the cold-boot refusal stops over a compiled artifact. The step needsserve's composition, with the auth-gated security plugin and hydration off.--storedexits 1 when rows remain uncanonical; the step exits 1 when the next boot would be refused.As a sibling, the step keeps the family's conventions (preview by default,
--apply/--yes/--force/--json/--database-url, the occupancy gate), and the refusal message names it directly.sys_metadatarow (organization_id IS NULL,state = 'active') of typepermissionorposition, the legacy pluralspermissions/positionsincluded, whose name a configured package holds: the population the cold-boot check (ADR-0048 N.3) refuses. A draft row and an organization-scoped row are never listed.os servecomposes, for the first phase only, and hydrates nothing. The host config's plugins and the application or compiled artifact (the existingcomposeHostStackdeclaration boot), plus the security plugin behindserve's auth gate. The boot runs withsys_metadatahydration off, so the cold-boot check meets an empty environment half and the boot comes up with no server.@objectstack/objectqlexport,findPackageHeldSecurityCatalogNames(registry). The cold-boot check now computes its conflicts from the same private reading (that list met with the bare slot), so the step and the boot share one reading of "who holds a name".--applydeletes through the engine's audited write path. A row stored under the canonical type goes through the protocol'sdeleteMetaItem(theDELETE /api/v1/meta/TYPE/NAMEdoor): asys_metadata_historytombstone and asys_metadata_auditrow, actoros migrate security-catalog-overlays. A legacy-plural row is out of that door's reach (it folds the type before reading, and the audit trail refuses a non-canonical type), so it goes through theSysMetadataRepositorydelete beneath it, addressed by its stored type, name,package_idand checksum, which writes the same history tombstone. One audit line per row names the door it took. Nothing is adopted;managed_byandpackage_idare never rewritten.--apply: 0 when every listed row is gone, 1 when one could not be deleted. A host config that exists and cannot be loaded is refused before any row is read.NAMESPACE_CONFLICTmessage now points at the step for the environment's rows. The envelope is unchanged.The first reading: does
materializeStackPlugincover the auth-gated security plugin?No.
materializeStackPlugin(packages/core/src/stack-plugins.ts, from97610a533) is the rule for one entry of a stack's ownpluginsarray. The security plugin is composed byserve's "5d" auth step, gated inline inserve.tson four conditions: the stack mounts noAuthPlugin, theauthtier is on, the composition is not a host kernel, and an auth secret resolves (with the development fallback).Extraction done here:
packages/core/src/stack-auth.ts, exported from@objectstack/core:resolvePlatformAuthComposition({ plugins, tiers, secret })returns{ composes: true, secret }or{ composes: false, reason }. The checks run inserve's order.resolveStackTiersand itsSTACK_TIER_PRESETS/CAPABILITY_TO_TIER.Serve.TIER_PRESETSandServe.CAPABILITY_TO_TIERare now handles over these.resolveAuthSecret,stackSuppliesAuthPluginandisHostKernelComposition.serveasks this rule. Its gate lineif (!hasAuthPlugin && tierEnabled('auth'))is kept, now reading the rule's own predicate and tier set; the host-kernel and no-secret branches read the rule's answer. Whatservecomposes is unchanged.Still owed (not in this PR's surface):
--presetand--dev(patch round 2,7f961c71a9). The step takesserve's two flags withserve's meaning, through the rulesserveitself now calls.--preset: both commands listObject.keys(STACK_TIER_PRESETS)as options, and the value reachesresolveStackTiers.--dev:isDevelopmentBoot(@objectstack/core, nowserve'sisDev) decides the secret fallback, andstackBootPlugins(cli/utils/stack-collections.ts, nowserve'spluginsline) mergesdevPlugins. The flags move the composition only. The step keeps the one-shot boot posture:NODE_ENVis untouched, no.env*file loads, and the standalone stack gets nodevkey. None of these moves the gate or the held names.AuthPlugin, the organizations plugin and the audit plugin are still constructed byservealone. None of them holds a catalog name. Measured: the only manifest registration of permission sets among serve's platform plugins issecurity-plugin.ts's.@objectstack/verify's harness composesAuthPluginand the security plugin unconditionally. That is verify: the in-process handle boots a leaner stack thanserveand has no door for eight things an app's tests need (requires[] capabilities, system/predicate update, the form door, user-less triggers, …), measured by hotcrm#2013 #22301's territory and is untouched here.Premises re-measured (zone 2), on
origin/maine148ca9842packages/runtime/src/standalone-stack.ts:910hard-codedhydrateMetadataFromDb: true. Everyos migratedatabase command booted throughschema-migrate.ts:492(new Runtime) and:522(runtime.start()). Held.createStandaloneStacknow acceptshydrateMetadataFromDb: false, andbootSchemaStackacceptshydrateMetadata: false. Both default to on.findEnvironmentHeldSecurityCatalogNames,declaredSecurityCatalogNames,BUILT_IN_SECURITY_CATALOG_NAMESandENVIRONMENT_HELD_SECURITY_CATALOG_TYPEShad 0 exports frompackages/objectql/src/index.tsandcore.ts. Held. None of the four is exported now either; the one new export is the package half of the reading.materializeStackPlugindoes not cover it (above).OS_AUTH_SECRETand 4 with it (the fourth ismember_default,com.objectstack.plugin-security, stored under the legacy plural).deleteMetaItem. Measured in a scratch run (not committed), with one database and two kernels of the step's own composition: one booted without hydration over stored rows, and one booted with hydration before the rows were written into it. Each calleddeleteMetaItemon one permission set and one position. Thesys_metadata_historyrows and thesys_metadata_auditrows were column-for-column identical apart from ids and timestamps (operation_type: delete,source: protocol.deleteMetaItem,recorded_by/actor= the step,outcome: allowed,code: ok,lock_state: none), and so were the receipts. What hydration does not decide, and the step's composition does: the security plugin's mutation projector registers in itsstart(), which a declaration boot suppresses, so nosys_permission_setre-projection runs in the step. The next boot's reconciler re-projects, as after Discard Overlay.The ADR-0087 marker on #22307's changeset
.changeset/22307-cold-boot-catalog-refusal.md(unreleased;pre.jsonlists 0 consumed changesets):not-required (no-migration-prescription)toregistered security-catalog-environment-overlay-refused. That is a new D3 semantic entry (packages/spec/src/migrations/entries/semantic/18.security-catalog-environment-overlay-refused.ts, plus its generated region inregistry.ts). Its replacement prescribes the step before the first v18 boot.findMigrationPrescriptionon the base and on the rewritten body:nullfor both. The old marker would therefore still have passed mechanically; the flip carries out the ruling, not a gate demand.spec-changes.jsonand the upgrade guide do not move: major-18 entries do not project before protocol 18.oscommand deletes asys_metadatarow offline" is rewritten. The pre-upgrade remedies are unchanged.Tests
Local runs. Builds and tests went through
scripts/pm/os-verify-lock.sh; each step's exit code was captured before any pipe. The branch mergedorigin/main446c8b2a6, which movedcore,runtimeandplugin-security. After the merge, ated5af305c8, these all exited 0: the whole-workspace build (72/72); typecheck of core, objectql, runtime and cli; core tests (88 / 2288); runtime tests (345 / 4878 passed, 19 skipped); the 7 touched cli unit files (186); and the 4 touched cli integration files (103). The numbers below are the pre-merge runs at5a5cb1057.Build of
@objectstack/{objectql,spec,core,runtime,cli}and their closure: 59/59 tasks, exit 0.Typecheck:
check:test-typecheck.Package tests:
src/migrations/*(3 files) 200 tests.CLI
unittier: 274 files / 4055 tests, 1 red. The red wastest/normalized-call-sites.test.ts: it flagged the newstack.requiresread inschema-migrate.ts. ThatstackiscreateStandaloneStack's result, whoserequiresthe runtime already resolves over package bodies. The read is now classified as atop-levelrow with that reason. Re-run: 11/11.CLI
integration, for the files this diff touches:security-catalog-overlays.integration.test.ts;schema-migrate.one-shot-family.integration.test.ts, where the step is now a declared caller with a preview mode and an--applymode;schema-migrate.host-composition.integration.test.ts;schema-migrate.requires-providers.integration.test.ts.Total: 4 files / 103 tests, exit 0.
The step's pin (
security-catalog-overlays.integration.test.ts) runs the command in-process through oclif over one SQLite database and one compiled artifact.permissionrow, the package-boundpositionrow and the legacy-pluralpositionsrow. With auth on it also lists the legacy-pluralpermissions/member_defaultrow, held bycom.objectstack.plugin-security. The controls are never listed: an environment-wide name no package holds, an organization-scoped row and a draft row.conflicts[]over the same database and configuration. The refusal comes from the same composition booted with hydration on.--applydeletes exactly the 4 listed rows and every control stays. It writes 4 history tombstones and 2 ADR-0010 audit rows: one per canonical row, actor = the step. Then the cold boot comes up, and a second preview lists 0 and exits 0. With auth off,member_defaultis left alone and the boot comes up.Ablation, run once and not committed:
composeAuthGatedSecurity: trueset tofalsein the command throughscripts/ablation-replace.mjs(anchor 1 to 0, bloba484cc0a4to92908a4cb).member_defaultmissing), and both preview cases lost theirsecurityPluginanswer.a484cc0a4andgit diff HEADis 0 bytes.The public door, measured by hand with the built CLI (
bin/run.js,NODE_ENV=production) over one fixture database:os servewithoutOS_AUTH_SECRETexits 1 and refuses 3 names. With it,os serveexits 1 and refuses 4: the same 3, plusmember_defaultheld bycom.objectstack.plugin-security.os migrate security-catalog-overlays --jsonlists exactly those 3 and those 4 rows, withsecurityPluginno-secretandcomposedrespectively.--applywith auth on (4 deleted: 2 viaprotocol.deleteMetaItem, 2 viasys-metadata-repository),os servewith auth on on the same database printedServer is ready. It was stopped by its owntimeout.Lint, measured over a stated narrowing:
pnpm exec eslint --no-inline-config --format jsonover the 18 changed.tsfiles: 18 file results, 0 errors, 0 warnings, 0 ignored.eslint.config.mjs: none of the 18 is ignored.eslint.config.mjsnever enables type-aware linting (noparserOptions.project, no typed rules, stated at its lines 326 to 328), so this diff cannot move a verdict on an untouched file.Patch round (head
ae87d67c8d)origin/mainda989bbb24was merged throughscripts/pm/os-regen-merge.sh(16e58dc320).7dc9788339):spec-changes.json+14 anddocs/protocol-upgrade-guide.md+3, additions only.check:spec-changes,check:upgrade-guide,check:migration-registryandcheck:generatedall exit 0, the last against a freshly built spec dist.ae87d67c8d:ae87d67c8d: 125 derived families. 124 exit 0, andcheck-empty-changesetis red by design (the declared 22307 correction).--ran: 125 derived, 125 run, 0 NOT-MEASURED, 0 UNRUN.check:error-code-provenanceincluded. The 3 PR-context ones were run withPR_NUMBER=22523.Patch round 2 (head
7f961c71a9)The contract review FAIL
6087652785and seat order6087686331..changeset/22371-security-catalog-overlays-step.mdnames"@objectstack/spec": minor. A bullet under 'New exports it is built on' names the D3 entrysecurity-catalog-environment-overlay-refusedand its upgrade-guide projection.--preset/--dev, read the wayservereads them.servelines now call the shared rules:isDev = isDevelopmentBoot(flags.dev),plugins = stackBootPlugins(config, flags.dev), and the--presetoptionsObject.keys(STACK_TIER_PRESETS). The truth table, the array identity and the option order are unchanged, soservecomposes what it composed.serveFlagsthroughbootSchemaStacktobuildSchemaMigrationPlugins. There--devcomposes the host config'sdevPluginsfor their declarations, and the gate reads them;--presetreachesresolveStackTiers.serveFlags, and the text report printsComposed as: os serve --preset NAME [--dev].--preset minimaland a secret:securityPluginisauth-tier-off,member_defaultis not listed, and the list equals the cold-bootconflicts[]of the same flags. With--devand no secret: composed, and 4 rows listed, equal to that boot's conflicts.serveFlagsturned the minimal case red ({ composed: true }). The restore was proven by blob.registry.ts,spec-changes.jsonand the upgrade guide regenerated,check:generated/check:spec-changes/check:upgrade-guide/check:migration-registrygreen) and one word-group of the 22307 note's upgrade-shape sentence. All four surfaces give the operator one instruction.7f961c71a9:stack-auth11/11; cli unit 9 files / 177; the step's integration file 6/6; host-composition plus one-shot-family 97/97; spec migrations 203/203;.tsfiles: 0 / 0.7f961c71a9:check-empty-changesetis red by design.--ran: 125/125, 0 NOT-MEASURED.check-changeset-no-majorwith the PR event readsClause-②: yes (widening)and passes.Landing step A (head
f59b85e44e)Seat order
6088904280.origin/mainfaf6348508was merged throughos-regen-merge.shas4cbd442f11, and the regeneration isf59b85e44e.main'sstorage-scope-public-retiredentry and itsflow-value-slot-template-dialect-refusededit join this PR's entry inspec-changes.jsonand the upgrade guide.registry.tsregenerates to its merged bytes unchanged.git greponorigin/mainand the head. Againstorigin/main, the PR's migration surfaces differ by additions only, and all of them are this PR's entry.check:migration-registry,check:spec-changes,check:upgrade-guide,check:generated,check:authorable-surface, specsrc/migrations(203), and the climigrate-meta-engine-guidanceintegration file. The re-derived 125 families give 124 exit 0, withcheck-empty-changesetred by design, and the 48 roster families all pass.serve.ts,normalized-call-sites.test.tsandregistry.tsmoved on both sides), so a fresh contract review is owed on this head.Gates
At
ed5af305c8:dispatch-gates.mjs --commands): 99 exit 0, and 1 is red by design.--ranreconciliation: 100 derived, 100 run, 0 NOT-MEASURED, 0 UNRUN.check-empty-changeset.mjs --base origin/mainis red by design. This PR deliberately corrects a pending release note:.changeset/22307-cold-boot-catalog-refusal.md, written by PR feat(objectql)!: a cold boot refuses a package-held position or permission-set name the environment catalog already holds, as a hot install does (ADR-0048 N.3) #22365 and not yet released. What changed under it: this PR adds the offline step its upgrade shape now prescribes. The corrections are the ADR-0087 marker, the upgrade shape, the after-upgrade bullets, and the sentence "Nooscommand deletes asys_metadatarow offline". The gate's own text says this class takes a confirmation on the PR, not a restore from base. Requested here.check:error-code-provenanceamong them. The step stamps no registered error code, and no owner-key row is owed. The other 3 need PR context:check-partof-closing-keyword.mjswith this body exits 0, and the two that need a PR number were run after this PR opened (see the report on the card).Acceptance notes
content/docs/deployment/cli.mdxgains the step's rows in this PR (ae87d67c8d): one in the "Data migrations" table and one in the "If the database is in use" table, where--applyrefuses a database a live process holds. The carrier noted in round 1 is discharged here._lock: 'full'or'no-delete'is refused bydeleteMetaItem's lock gate (ITEM_LOCKED). The step reports that rowfailedwith the reason and exits 1; the SQL in the changeset remains the statement of what to delete. Not measured on a real package: no shipped package declares a lock on a permission set or position.serve.tskeeps a second spelling ofisDevfor port auto-shift:portAutoShiftAllowed = flags.dev || process.env.NODE_ENV === 'development', earlier inrun()thanisDev. It decides the port policy, not the composition, so it was left as is: outside this card's surface, no carrier.Generated by Claude Code