Repository navigation
feat(plugin-webhooks): 18.x step 1 (8b-0g′): declared webhooks are read from the registry, the seeder retires, expand-first #24529
Description
Filing gate ③: a task the maintainer directed. These are steps 1–3 of the 18.x line, under ruling records 6266750393 and 6267391172 on #15194. The second records the maintainer's 「用量很多,不用节约」: steps 1–3 start now, in parallel.
- Reader who acts: the epic seat (
epic:#15194,session_01Y5583bs2RDd53MH6kM1ca3), which dispatches it now. - Dedupe: none. Before this card, the stage lived only in the plan rows on feat(spec,drivers,objectql,plugin-security):
organization_idNOT NULL per cleared table; one predicate for Layer 0 and every driver; bothorWhereNullarms, the__global__sentinel and the #13491 ledger retire (ADR-0131 D1/D8/D9) — protocol 18 #15212.
Part of #15212 (the C8 card), from decision #24435 Q2 A (ruling 6259141885) · Step 1: 8b-0g′, declared webhooks read from the registry · ⛔ Not an 18.0 gate.
The line's rule (binding)
Once merged, this PR leaves main releasable:
- R1. A fresh install boots and serves.
- R2. A database an 18.0 install created upgrades to that commit, boots, and keeps its data. test(qa): the 18.x line's upgrade harness (an 18.0 database boots on every later main with its data) and the measured step plan #24522's harness proves this.
- R3. A v17 database is still refused.
- R4. A walled posture is still refused at boot.
Expand first. This step only adds: the new write shape, plus reads that accept both the 18.0 shape and the new one. It never moves or deletes an 18.0 row and never drops a column. Each such removal is written down for the step-5 ceremony.
Merge gate. The PR stays a draft until #24522's harness has merged. It lands after it has merged main and is green on R1–R4.
Scope
- Per [Decision] Two more framework writers land organization-less rows under a wall before C8 stage 8b: flow credentials (
sys_flow_credential) and boot-seeded declared webhooks (sys_webhook) #24435 Q2 A (ruling 6259141885), read declared webhooks from the metadata registry at dispatch. RetirebootstrapDeclaredWebhooks, so a fresh boot writes zerosys_webhookrows for declared webhooks. Switch a declared webhook off through the activation ledger (regime C, assharing_ruledid 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). - On an 18.0 database: the seeded
sys_webhookrows from 18.0 stay in place. The reader must not fire a declared webhook twice. Measure and choose: a seeded row is recognised and ignored in favour of the registry entry, or the registry entry defers to it. Organization-authored webhooks stay rows. Deleting the seeded rows is a step-5 ceremony item. - Plan amendment 13 (6259616282) on feat(spec,drivers,objectql,plugin-security):
organization_idNOT NULL per cleared table; one predicate for Layer 0 and every driver; bothorWhereNullarms, the__global__sentinel and the #13491 ledger retire (ADR-0131 D1/D8/D9) — protocol 18 #15212 carries the stage detail. Read it.
Acceptance
- The new shape is pinned on a fresh install. The dual read is pinned on an 18.0-shaped row.
- The harness stays green.
- The report lists every removal the step-5 ceremony owes, table by table.
- Measure first. If expand-first is impossible or unsafe for any part, stop and report that part with the alternative. Do not build a destructive change.
Activity
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 10 · 2026-10-19T12:40Z
Session:
session_01Y5583bs2RDd53MH6kM1ca3Account:
os-steve(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-24529-8b0g-webhooks-registry. If the dev splits this step into several independently releasable PRs, each PR's branch isclaude/issue-24529-8b0g-webhooks-registry-<part>and carriesPart of #24529.Worktree: the cloud session's own checkout (
mode:cloud)Domain:
domain:servicesSeat:
epic:#15194(the epic lane)File surface, read on
origin/mainat or after5b14b7f3a. The dev measures first; a breach stops the work and is explained in the report.packages/plugins/plugin-webhooks/**(the declared-webhook reader, the seeder's retirement).packages/qa/dogfood/**harness files of test(qa): the 18.x line's upgrade harness (an 18.0 database boots on every later main with its data) and the measured step plan #24522. ⛔ Notdocs/adr/**. ⛔ Notcontent/docs/releases/.Container & model:
M,mode:cloud,model: TIER_DEFAULTClause-②: yes — the dev re-measures; reading declared webhooks from the registry and a ledger switch for them add a surface; retiring the seeder narrows one
Responsibility: n/a — a ruled scope stage (ruling records 6266750393 and 6267391172 on [epic] ADR-0131 — total organization ownership: no NULL organization_id (v18 line) #15194; [Decision] Two more framework writers land organization-less rows under a wall before C8 stage 8b: flow credentials (
sys_flow_credential) and boot-seeded declared webhooks (sys_webhook) #24435 Q2 A, 6259141885)Thread-read: 6267391172
Serial constraints cleared:
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{ "issue": 24529, "status": "done", "branch": "claude/issue-24529-8b0g-webhooks-registry", "pr": "https://github.com/objectstack-ai/objectstack/pull/24588", "session": "session_01PDSxBZE561TFe1N2iEjKXZ", "premise_still_valid": true, "summary": "Claim verified: the newest Claim on #24529 is 6267446928 and it names this branch. The boot seeder bootstrapDeclaredWebhooks is deleted, and a fresh boot writes zero sys_webhook rows for a declared webhook. The dispatcher reads declarations from the registry: DeclaredWebhookSource in plugin-webhooks, bound as the declaredWebhooks service. The dispatcher projects each one into its subscription cache (ref_id declared:NAME), and its secret and headers are held in memory and never stored. A declared webhook is switched under regime C, through sys_metadata_activation rows of type webhook, by the runtime catalog activation door POST /security/_activation/webhook/:name, the sharing_rule door with webhook admitted. The ledger overrides isActive in both directions, and a ledger write refreshes the dispatcher in the same request. Dual read on an 18.0 DB: the seeder's rows stay. A pristine mirror (managed_by package, not customized) is ignored in favour of its declaration, so there is no double fire. A customized seeded row, or an admin row under the name, answers as itself and the declaration is withheld, which keeps an admin's active:false. Measured on a fresh main showcase: the 18.0 seeded row carries the Default Organization and delivers under single. So under a posture with no wall, a declared webhook is deployment-wide: it takes every organization's events and stamps the event's organization on each delivery. Under a wall the org-less refusal stands. The redelivery veto judges declared:NAME deliveries, and pristine-mirror replays, by the declaration.", "tests": "At head 6b3042a8. plugin-webhooks vitest: 19 files, 212 passed. typecheck exit 0 (check:test-typecheck OK). runtime full suite: 422 files, 6652 passed, 19 skipped; typecheck exit 0; catalog-activation-door.test.ts +7 webhook pins. Dogfood webhook-materialization.dogfood.test.ts: 1/1 (no sys_webhook row; dispatch through the bound declaredWebhooks service, object to object_name). check:liveness exit 0. The 18.0-shaped fixture: seeded18_0Row (fake engine) and seed18_0Rows (real ObjectQL, encrypted columns) reproduce the 18.0 seeder's mapWebhookToRow plus provenance; the dual read is pinned by 7 cases against them. Ablations through scripts/ablation-replace.mjs, each restored with blob == HEAD and git diff HEAD empty (tests import src relatively, so no dist leg): mirror skip 3 red; withheld rule 3; ledger override 6; ledger self-heal 1; deploymentWide 2; plugin posture read 1; guard declared branch 2; guard mirror branch 1; door declaredWebhooks lookup 5. Fresh showcase boot at 6b3042a8 (single): sys_webhook 0 rows; door enabled:true returns 200 and one ledger row (package com.example.showcase); undeclared name 404; a task create gives a sys_http_delivery row with ref_id declared:showcase_task_changed and Default Organization; enabled:false and a second create add no delivery. The same steps on fresh main: seeded row org-stamped, delivery ref_id whk_ in the same org.", "gates": "Local, head 6b3042a8: dispatch-gates --commands --repo objectstack-ai/objectstack gave 130 families, plus the 50-family artifact-roster block (STDERR): 178 commands, exit codes written to a file before any pipe. 173 exit 0. NOT MEASURED, 5: check-closing-target-claim, check-single-claim-paths and check-governed-queue-guard (exit 2/2/1: no PR or event context, CI runs them); check-partof-closing-keyword (no context in the sweep; rerun with PR_BODY = the PR body: exit 0, as is check-closing-keyword-parity); check-plugin-teardown-shape --self-test (exit 3: its pinned fixture needs an unshallowed clone; checker health only). --ran: 130 derived, 130 run, 0 NOT-MEASURED. Real findings fixed during the sweep: tenant-audit census 231 to 229 (regenerated with the tool, prose figures corrected); objectql-double-limit and where-matcher on the renamed test double (made drivable and conforming, baseline entry dropped); spec liveness state-counts regenerated with check:generated --fix. CI check-runs on #24588: in_progress at report time.", "mcp_calls": "3 MCP reads, 0 writes: issue_read get on #24529, issue_read get_comments on #24529, pull_request_read get on #24588. Ruling records read with gh api GET (reads only).", "api_writes": "2 relay strokes as objectstack-fleet[bot] via with-fleet.sh --via dispatch: (1) pr_create, i.e. POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /issues/24588/assignees [os-steve]; (2) this comment, POST /repos/objectstack-ai/objectstack/issues/24529/comments. Plus git pushes to the branch (not REST).", "ceremony_owes": [ "sys_webhook: delete the pristine declared mirrors (managed_by package or platform, customized not true, name still declared).", "sys_webhook: settle each customized seeded row the way the sharing-rule ceremony settles its customised copies: a copy under a new name, plus the declared webhook switched off in sys_metadata_activation.", "sys_webhook inventory: in packages/cli/src/utils/organization-ownership-inventory.ts, turn the row's declared category into a mirror-deletion departure (ADR-0131 D10 fate 2). Not edited here, because the apply executes departures and so it is ceremony behaviour.", "sys_http_delivery: re-source the deleted mirrors' tails, ref_id MIRROR_ROW_ID to declared:NAME, so history and the redelivery veto follow the declaration.", "sys_secret: delete the cipher rows the deleted mirrors' signing_secret and headers_secret referenced (orphans after deletion).", "sys_webhook columns: customized, and the package/platform values of managed_by, describe nothing once no mirror exists. Retire them in the ceremony or at step 7. Their help text still mentions boot seeding, left unedited so the i18n bundles are not churned.", "Code owed at step 7: classifyDeclaredNameHolder, the mirror skip in AutoEnqueuer.doRefresh, the redeliver guard's pristine-mirror branch, bindWebhookProvenanceStamp, and migrateLegacyWebhookSecrets for package rows." ], "open_questions": [ { "question": "How is a sys_webhook row holding a declared name read on an 18.0 DB?", "options": [ "A recognise a pristine seeded row and ignore it in favour of the registry entry; let any edited or admin row answer and withhold the declaration", "B always ignore seeded rows", "C always defer the declaration to any same-name row" ], "recommendation": "A, decided and built: it reproduces exactly what the 18.0 seeder did on each boot (it overwrote pristine rows and never touched edited or admin ones). So there is no double fire, an admin's active:false survives, and a changed declaration still propagates." }, { "question": "Organization dimension of a declared webhook (measured: the 18.0 seeded row carries the Default Organization and delivers under single)", "options": [ "A deployment-wide when the posture builds no wall (deliveries stamped with the event's organization); org-less refusal under a wall", "B org-less everywhere (silently delivers nothing under single, a regression from 18.0)" ], "recommendation": "A, decided and built. It keeps R2 behaviour and follows the sharing plugin's declared-rule scope reading of the posture." }, { "question": "Refuse a new sys_webhook row under a declared name with 409, as sharing_rule's guard does?", "options": [ "A not in this expand step (the row answers and the declaration is withheld, as under 18.0)", "B add it now" ], "recommendation": "A, decided: expand-first adds no new refusal. Take it up at step 7, or as its own card, when the mirrors are gone." }, { "question": "Files edited outside the claim's surface list", "options": [ "A accept them as necessary consequences", "B split them out" ], "recommendation": "A. Deleting the seeder forced these edits: spec liveness rows and comments; the dogfood webhook proof and fixture (not #24522's harness); the platform checklist item; content/docs webhooks guide; tenant-audit census regenerated; the showcase docblock; four scripts/ ledgers re-keyed or shrunk. No ADR, no releases page, no #24522 harness file." } ], "out_of_scope_findings": [ "class: none filed. label and description on WebhookSchema are now unread: the seeded Setup row was their only reader. The liveness ledger marks them dead, owed enforce-or-remove (ADR-0049): a Setup surface that lists declared webhooks, or retirement. carrier: none; noted in the PR's Acceptance notes. Dedupe words: webhook label dead, declared webhook display, enforce-or-remove webhook", "class: none filed. AutoEnqueuer.admitsOrganization's docblock says a single-posture deployment stamps nothing on events; measured on the showcase, single-posture record events carry the Default Organization (since 8b). The comment is stale; its behaviour is unchanged here. carrier: none; noted only." ] }Generated by Claude Code