Repository navigation
feat(cli,runtime)!: the upgrade ceremony opens — v17 and 18.0 databases take it, and a server serves only a database it created or one the ceremony certified (18.x steps 5 and 6) - #24681
Conversation
…r retired; webhook activation door Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ Co-authored-by: Claude <noreply@anthropic.com>
…he 18.0 dual read; door pins for webhook Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ Co-authored-by: Claude <noreply@anthropic.com>
…asserts no row and a dispatch Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ Co-authored-by: Claude <noreply@anthropic.com>
…the checklist, the webhooks guide and the showcase; changeset Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ Co-authored-by: Claude <noreply@anthropic.com>
…tcher's switch read Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ Co-authored-by: Claude <noreply@anthropic.com>
…posture builds no wall, as its 18.0 seeded copy stamped with the Default Organization was Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ Co-authored-by: Claude <noreply@anthropic.com>
…red writes; the declared-webhook double is drivable by the limit gate Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ Co-authored-by: Claude <noreply@anthropic.com>
…does not implement Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ Co-authored-by: Claude <noreply@anthropic.com>
…authors; checklist and admitsOrganization docblock say a declared webhook is deployment-wide under single Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ Co-authored-by: Claude <noreply@anthropic.com>
…0g-webhooks-registry
…the fixture's customized declared-webhook mirror Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ Co-authored-by: Claude <noreply@anthropic.com>
…n fail on a double delivery Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ Co-authored-by: Claude <noreply@anthropic.com>
…0g-webhooks-registry # Conflicts: # content/docs/permissions/tenant-audit-census.mdx # docs/audits/2026-08-tenant-audit-write-call-sites.counts.md
…0g-webhooks-registry
…0g-webhooks-registry # Conflicts: # content/docs/permissions/tenant-audit-census.mdx # docs/audits/2026-08-tenant-audit-write-call-sites.counts.md
…-registry' into claude/issue-24581-ceremony-open
…NOT NULL door end to end
…he ceremony, then boot; boot-refusal-order step 2 back on the binary
…he checklist; changeset
…ap, docs and strictness-ledger artifacts
…el — the cipher names no organization Claude-Session: https://claude.ai/code/session_014UATXWqAXqkaZkbG92ox6V Co-authored-by: Claude <noreply@anthropic.com>
…sys_flow_credential leave reach) Claude-Session: https://claude.ai/code/session_014UATXWqAXqkaZkbG92ox6V Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Read: cards #24581 and #24582; claims 6276776808 and 6276790571; seat note 6271949316; rulings 6266750393, 6267391172, 6269955919 with its amendment 6270010349, and 6270035914 on #15194; ruling 6278609968 on #24580; step 0's plan 6268559156 (steps[4], steps[5], F1 to F6); the dev's report 6279428591; the Landed records 6279236728 (#24529), 6275112954 (#24530), 6272692402 (#24531); #24489; #24625 (measured record 6278938192, claim 6278944518); the head's check-runs twice. Diff: 66 files, +2,797 / -737, head ① Derived judgments1. 5a, webhooks (
2. 5b, sibling moves (
DECISION 2, the magic-link selector (apply.ts 3344-3351, 3369-3377): 3. 5c, 9c ( DECISION 1, the walled block (apply.ts 3209-3219 for webhooks, 3624-3633 for rules; test 376-385). Under 4. 5d, NOT NULL after the moves ( 5. Step 6.
6. The 18.x line rule after this lands. The harness ( 7. Ablations and gates. Eleven legs, each red then restored to 8. Complete feature. The command runs, the harness upgrades an 18.0 and a v17 database through the real binary and boots them, 9. Interactions.
10. Semver and the Clause-② arm. See ②. ② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL
|
…e and find, so a fresh libsql:// install gets its birth marker and boots
The contract review (B1) measured an R1 regression: the remote face answered
getSchemaSyncStats {0, 0} ("cannot say") by contract, so the boot guard never
saw a fresh remote store as created from empty and refused a fresh install as
"an earlier release created it". The transport's two schema doors already
probe sqlite_master per table; they now count created vs found, as
SqlDriver.initObjects counts from hasTable, and the remote face answers with
that count. Pinned on a real kernel over the transport's file double: a fresh
remote database boots on its birth marker, and an 18.0-shaped one is refused.
Claude-Session: https://claude.ai/code/session_014UATXWqAXqkaZkbG92ox6V
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UATXWqAXqkaZkbG92ox6V Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UATXWqAXqkaZkbG92ox6V Co-authored-by: Claude <noreply@anthropic.com>
… helper (upgrade-harness/ceremony.ts) Every suite that boots the 18.0 fixture under the ownership boot guard needs it: the guard refuses an 18.0 database until the ceremony has certified it. Claude-Session: https://claude.ai/code/session_014UATXWqAXqkaZkbG92ox6V Co-authored-by: Claude <noreply@anthropic.com>
Closes #24581
Closes #24582
Part of #24421
Part of #24489
Clause-②: yes (narrowing)
18.x line, steps 5 and 6 as ONE landing (ruling 6270035914 on #15194), under rulings 6266750393, 6267391172 and 6269955919 (and its amendment). Step 0's plan: os-dev-report 6268559156 on #24522. Seat note 6271949316 on #24581 holds what steps 1–3 left the ceremony to do.
Base and merge gate. The branch started from
origin/mainmerged with #24588's head (step 1), because 5a reads its declared-webhook source. #24588 has since landed (191e0430), andmainis merged in atd6195b90. ⛔ It lands only after step 4 (#24580) has merged and this branch has mergedmainagain. It meets step 4 only onserve.ts, which this branch does not touch. The diff is 3,534 changed lines (+2,797 / −737) over 66 files, which is over the 3,000-line human-merge line.Restated from #24635 (on
main). That PR taught four surfaces to say "18.0 does not upgrade; keep a v17 database on 17.x": the drop-gate refusal ofos migrate apply --allow-destructive,os dev's way-on line, the refusing class's help examples, and that class's pin. That is false once the ceremony is open. The drop refusal andos devnow name the ceremony's steps again, and their pins say so (apply.organization-ownership-refusal.test.ts,dev-organization-ownership-upgrade.test.ts). The--jsondetailkeeps #24635's wording ("No run of the organization-ownership ceremony is recorded…"). The refusing class and its pin stay deleted.What changed
os migrate organization-ownershipruns again (#24489 items 1, 4, 6)MigrateOrganizationOwnershipandORGANIZATION_OWNERSHIP_UPGRADE_UNAVAILABLEare deleted. The default export isOrganizationOwnershipCeremonyagain.os dev's automatic upgrade works again: it runs the binary, which now runs the ceremony.Four new ceremony steps (shape generation 1)
CEREMONY_STEPSorder: attribution, authored-conversion, metadata-promotion, template-promotion, settings-move, sibling-moves, declared-webhooks, declared-rule-copies, rewrite, mirror-deletion, history-renumber, column-drops, not-null.Every new step selects by shape, never by
organization_id IS NULL(F3).sibling-moves. Rows move id for id. Each is copied over the columns both tables carry (neverorganization_id), read back, then deleted with its selector re-asserted. A row already in the sibling is not copied twice.sys_setting_audit WHERE scope = 'global'moves tosys_platform_setting_audit.sys_emailtosys_identity_email. It is selected asrelated_object = 'sys_user'(reset, verification, account invitation, change-email notice), plus the magic link. Selector gap, defined here: an 18.0 magic-link row has no related record and no template column, so it is selected byrelated_object IS NULL AND related_id IS NULLand a body carrying better-auth's sign-in path/magic-link/verify?token=. The organization invitation (sys_invitation) is tenant mail and stays.email.send.async,email.attachment.reclaim) that name a moving row getobject: 'sys_identity_email'in their payload and metadata, and the sibling's idempotency-key spelling. Without this, a send finds no row and a reclaim takes the row-gone branch and deletes attachment content.sys_activityrows about a moved row (every rotation shard) name the new object.sys_audit_logis a ledger and is not rewritten.sys_emailchanges. The moved rows land in a table withrequiredPermissions, and the re-pointed activity rows follow them there, so the exposure is not widened. It is not fixed either.declared-webhooks. For eachsys_webhookrow withmanaged_bypackageorplatformunder a name a package still declares:declared:NAME, then the row is deleted, then thesys_secretcipher it referenced is deleted once no webhook row references it.definition_jsonsnapshot the 18.0 seeder last wrote, and no credential is stored): the switch becomes the declaration'ssys_metadata_activationrow, the deliveries are re-sourced, and the row is deleted. The harness fixture's row is this case.NAME_customwithmanaged_by 'admin', and the declaration is switched off. Its id, credentials and deliveries stay its own, so the deliveries' tail already names the clone's row id. A row holding a credential is always settled this way, because a stored secret cannot be compared without decrypting it.classifyDeclaredNameHoldertreatsplatformlikepackage(a new sharedisSeededProvenance, also used by the provenance hook).DECLARED_WEBHOOK_REF_PREFIX/declaredWebhookRefIdare now exported by plugin-webhooks. The ceremony's copy of the spelling is pinned equal to them.declared-rule-copies(9c, plugin-sharing: refuse a new or renamed sys_sharing_rule row that takes a declared rule's name, and reconcile the shadowed declaration's grants on a same-name write (ruling B′ on #24378) #24421). The runtime half already landed in plugin-sharing, runtime: sharing_rule under regime C, so every same-name row is refused and declared rules are switched through the activation ledger (ruling A on #24378) #24428, measured: the same-name guard, the ledger switch and the load report. This step is the copy migration, ruling A item 3. Eachsys_sharing_rulerow under a declared rule's name that is not a pristine mirror (a customized copy, or an admin rule a later declaration collided with) is renamed in place toNAME_custom,managed_by 'admin'. Its id-keyed grants stay its own. The declaration is switched off (sharing_ruleactivation row,active: false), and grants still naming the declaration itself are removed. Pristine mirrors stay the mirror-deletion step's.not-null(8e, ruling ② A). After column-drops, every table the post-step plan clears (notNull.willReceive, zero NULL rows) takesorganization_idNOT NULL. It goes through the drift apply door of the driver that holds the table: the SQLite rebuild, orSET NOT NULL. Results:not-declared.unsupported.unsupported: the door rebuilds only declared tables, and new shards are created nullable.diffManagedTable) takes the table's tenant column (ceremonyOwnedNullability, passed bySqlDriver.detectTableDriftfromresolveTenantField) and proposes norelax_not_nullfor it. No metadata changes.CeremonyCheckpointcarriesshapeGeneration.DeclaredSharingRulesPort.rules()returnspackageId. Two new ports:DeclaredWebhooksPort(the command reads the registry, parsed withWebhookSchema) andNotNullPort.The guard's verdict on an 18.0 database (F4, F5; ruling ① A)
ORGANIZATION_OWNERSHIP_SHAPE_GENERATION = 1, plusOrganizationOwnershipBirthMarkerSchemaandorganizationOwnershipShapeGenerationOfin@objectstack/spec/system. Themigration.zod.tsprohibition is amended for this marker only.sys_migration, so a telemetry sibling neither ages a fresh primary nor freshens an old one), with no marker and no v17-shape table, the guard writes the ceremony's row once: verified,details.birth = { shapeGeneration, platformVersion, createdAt }.commitis absent, because no build records it. A failed write refuses the boot.shapeGenerationalone. A verified marker at the current generation boots. A database with no marker (every 18.0 one) is refused, and so is one verified below the current generation (F5). Unverified or unreadable markers are refused as before.OrganizationOwnershipBirthMarkerPluginincreateStandaloneStack. It never refuses. Without it, an install whose first command isos migrate applyoros seedwould be refused by its firstos serve. The ceremony's own boots defer DDL, so they never trip it.outdatedarm.tenancy: { enabled: false }gap closed: the v17-shape probe reads that spelling andsystemFields: falsetoo.sys_secret/sys_flow_credentialflips (step 2's PR 2b, F2)systemFields: { tenant: false }, and their inventory rows arecolumn-drop.organization_idforsys_secret.SettingsSecretStore.insertno longer takes it.Inventory
sys_setting_auditgains theglobal-audit-movedeparture, andsys_emailgainsidentity-mail-move.sys_webhook'sdeclaredcategory now coverspackageandplatform, and its label names the settlement.Proof
R1 — a fresh boot writes the birth marker and boots, again and again. Pins:
organization-ownership-boot-guard.kernel.test.ts(a real kernel) andorganization-ownership-boot-guard.test.ts(the CLI birth half).R2 and R3, restated by name in
upgrade-18x-harness.dogfood.test.ts: 8/8 pass.declared:showcase_task_changed).organization_idgone onsys_secret/sys_flow_credential;A shorter ceremony's marker is refused (F5):
organization-ownership-boot-guard.test.ts.The ceremony's own pins:
organization-ownership-apply.generation-1.test.ts, 16 cases over an 18.0-shaped database (every row stamped), one or more per step, plus idempotence and the walled block. Alsoorganization-ownership-not-null.test.ts, on a real SQLite driver: NOT NULL is applied and the next drift scan reports nothing for it.Ablations (
scripts/ablation-replace.mjs). Every leg is committed first and restored toblob == HEAD, withgit diff HEADempty after the last leg:>= current→>= 0)tenancy.enabled: falsespellingTests. Run on this branch before the
mainmerge, then the touched sets again after it atd6195b90. Exit codes were captured to files before any pipe.@objectstack/runtime: full suite, 428 files, 7,492 passed, 19 skipped.@objectstack/cli: full suite. The first run went red in 72 cases, mostly whole-file import errors while the specdistwas being rebuilt underneath it. A re-run of every failed file left 6, all fixed:@objectstack/driver-sql: 234 files, 3,678 passed. Two drift fixtures were re-keyed offorganization_id, and the tenant-column exemption pin was added.service-settings: 892 passed (one pin restated: the cipher names no organization).platform-objects: 1,192 passed (one control moved tosys_setting_audit, plus asys_secretpin).service-automation: 2,580 passed.plugin-webhooks: 212 passed.upgrade-18x-harness,organization-ownership-boot-refusal-order,settings-audit-global-rung-siblingandwebhook-materialization: 4 files, 17 passed, 50 s.typecheck: exit 0 for cli, runtime, driver-sql, plugin-webhooks and service-settings.eslint --no-inline-configover the changed.tsfiles: exit 0.Gates.
node scripts/pm/dispatch-gates.mjs --commandsderived 137 commands before the merge, and 132 after it, all among the 137.check-platform-object-tenancy-censusand its self-test: the census was regenerated with--write(sys_secretandsys_flow_credentialleave reach, 44 → 42).check:engine-double-contract: the guard double'sfindOnenow asserts the predicate, and the pinned ledger was rewritten with--write.check:spec-parsed-alias: the birth marker'sz.inputalias, with aParsedalias beside it.check:test-source-alias: a CLI vitest alias for@objectstack/plugin-webhooks.check-plugin-teardown-shape --self-test: exit 3, NOT MEASURED. Its positive control needs history this shallow clone lacks; this is checker health, not this diff.check:adr-0087-registration,check:tenant-audit-census,check:platform-checklist, and the speccheck:generated(14 artifacts up to date).check-governed-merges --branch: 0 of 95 paths on the register before the merge. The size decides: human merge or an authorized approval.Decisions taken, for the seat
group/isolated, naming the rows and the remedy: rename it to a name no package declares, or delete it, then re-run. The declaration's switch is deployment-wide (ruling A item 1: no per-organization switch-off), so switching it off on the operator's behalf would remove it from every organization. Pristine copies are still deleted under a wall.sys_webhookstays a category, not a departure. A departure withholds its NULL rows from attribution, and a seeded row no package declares is a live webhook that must be attributed like any other. The settlement selects by shape in its own step.Acceptance notes
docs/qa/platform-checklist/areas/cli.json(itemcli.organization-ownership-ceremony, revision 3: its pinorganization-ownership.release-refusal.test.tsis deleted here);content/docs/deployment/cli.mdx(the 18.0 refusal sentences);driver-sqldrift tests whoserelax_not_nullfixture usedorganization_id, re-keyed to another column, plus a new pin that the tenant column is exempt;standalone-stack.ts(the birth half).docs/adr/**,content/docs/releases/,serve.ts, step 4's walled sentences intenancy-modes.mdx/self-hosting.mdx/environment-variables.mdx.partition_key. It is still claimed and sent, by that partition's worker.Generated by Claude Code