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
objectstack-fleet commentedon Oct 19, 2026
os-dev-report addendum
{ "issue": 24529, "addendum_to": "os-dev-report 6270150096", "pr": "https://github.com/objectstack-ai/objectstack/pull/24588", "head": "9bce0aaf", "base_merged": "origin/main 5a871e4e (carries the harness ec16dbe2e: merge-base --is-ancestor exit 0)", "test_core_2_of_6": "Root cause, reproduced locally: packages/lint/src/lint-liveness-properties.test.ts 'does not warn on an authored webhook' pinned zero findings against the real webhook.json, and the PR's dead flip of label/description made it emit liveness-dead-property for description. This is the contract review's B1; main is green on that shard (ci.yml on main at 2667e2402: success). Fix, keeping the verdict: (1) the two dead rows gain an authorHint; (2) the guard now expects exactly two liveness-dead-property findings, for label and description, with that hint, and a new case pins silence for a declaration that authors only what is delivered on; (3) the showcase declaration drops label and description. pnpm --filter @objectstack/lint test: 148 files, 6924 passed, exit 0. The test:repo projects of spec/core/objectql/rest/runtime were also run locally and are all green (1162/82/5/278/808).", "r2_harness": "pnpm --filter @objectstack/dogfood exec vitest run test/upgrade-18x-harness.dogfood.test.ts under os-verify-lock at 9bce0aaf: 6 passed (6), exit 0. Added case 2b, 'the fixture's customized declared webhook dispatches exactly once on a task change'. The 18.0 mirror is still there and customized:true. The declaration is switched ON through POST /security/_activation/webhook/showcase_task_changed (200). One task create then yields exactly one new sys_http_delivery row, ref_id = the mirror row id. Ablation (withheld-declaration rule mutated with a marker that survives bundling; dist preflight: marker present in 2 built files) made 2b red, received an extra 'declared:showcase_task_changed'. Restored: blob == HEAD, dist preflight --absent exit 0, tree clean. A first ablation leg without the switch-on stayed green, because the shipped declaration is isActive:false. That is why the case now switches it on. Recorded, not hidden.", "also_fixed": "docs/qa/platform-checklist integration-system.json revision 8, fixtures.requires[0] and steps[1]: a declared webhook is deployment-wide under single and delivers once switched on (to its placeholder url); refused org-less only under a wall. The AutoEnqueuer.admitsOrganization docblock: the stale 'single stamps nothing' cell is corrected, and the deployment-wide check is documented.", "checks_at_head": "exit 0 each: plugin-webhooks test (212) and typecheck; runtime catalog-activation-door (64) and typecheck; dogfood webhook-materialization (1); check-tenant-audit-census; check:engine-double-contract; check:objectql-double-limit; check:where-matcher; check-platform-checklist; check-adr-0087-registration and check-changeset-no-major --base origin/main; spec check:liveness. The full dispatch-gates union was NOT re-run on this head; CI runs it.", "line_count": "git diff --shortstat origin/main...HEAD: 42 files, +1960 / -1146 = 3106 changed lines, over 3,000. No test was trimmed. Per the seat, the PR goes to the maintainer as Tier H.", "api_writes": "1 relay stroke: POST /repos/objectstack-ai/objectstack/issues/24529/comments (this addendum). Plus git pushes.", "mcp_calls": "reads only: pull_request_read get_check_runs, get_job_logs (twice), actions_list list_workflow_runs; 0 writes" }Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report addendum
{ "issue": 24529, "addendum_to": "os-dev-report 6270150096 (second addendum, after 6271777060)", "pr": "https://github.com/objectstack-ai/objectstack/pull/24588", "head": "fa6a837e", "merge_commits": "7e580dc7 merges origin/main c3954057 (carries #24587, fbafb8533); fa6a837e merges origin/main 8fbbd508. Both are merge commits: no rebase, no force-push. mergeable_state read back after the push: blocked (no longer dirty).", "conflicts": "Two, both regenerable: content/docs/permissions/tenant-audit-census.mdx and docs/audits/2026-08-tenant-audit-write-call-sites.counts.md. Resolved by taking main's side and regenerating with node scripts/tenant-audit-census.mjs --write. The hand-written prose figures the gate holds to the census were then brought to it: 231 to 229 sites (#24587's sites in, the seeder's two out); 157 to 155 decidable; 51 to 49 const-named; 52 to 50, 60 to 58, 31 to 29, 95 to 93. No checklist item conflicted: platform-core.json and integration-system.json auto-merged, and integration-system.json stays at revision 8 with main's history intact.", "checks_at_head": "All exit 0, run after the second merge at fa6a837e, with the dogfood and runtime closures rebuilt first. upgrade-18x-harness.dogfood.test.ts: 6 passed (6), case 2b included. plugin-webhooks test: 212 passed. runtime catalog-activation-door.test.ts: 64 passed. lint lint-liveness-properties.test.ts: 103 passed. spec check:liveness, check-tenant-audit-census, check-platform-checklist. The same set was also green at 7e580dc7.", "main_after_push": "origin/main moved to 65a8eeb7 (#24620, plugin-approvals only) after the push. A trial merge was clean with no overlapping file, so it was not merged in. #24590 has not landed.", "line_count": "git diff --shortstat origin/main...HEAD: 42 files, +1958 / -1144 = 3102 changed lines. The PR page reads 1958 / 1144 over 42 files.", "api_writes": "1 relay stroke: POST /repos/objectstack-ai/objectstack/issues/24529/comments (this addendum). Plus git push.", "mcp_calls": "reads only: pull_request_read get on #24588; 0 writes" }Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report addendum
Third addendum to 6270150096. #24588 head is now
b4ae8e68. That is merge commitb4ae8e68, which merges origin/mainb12e16a5(it carries #24590) as a merge commit, with no rebase and no force-push. The branch is up to date with main at the push. The only conflicts were the same regenerable pair,content/docs/permissions/tenant-audit-census.mdxanddocs/audits/2026-08-tenant-audit-write-call-sites.counts.md. I took main's side, regenerated them withnode scripts/tenant-audit-census.mjs --write, and brought the hand-written figures the gate holds to the census: 232 to 230 write call sites (#24590's site is in, the seeder's two are out); 158 of 232 to 156 of 230 decidable; 129 of 230 (56%) elevated; and 52 to 50, 51 to 49, 60 to 58, 31 to 29, 95 to 93. All exit 0 atb4ae8e68, with the dogfood and runtime closures rebuilt:upgrade-18x-harness.dogfood.test.ts6/6 (case 2b included); the plugin-webhooks suite (212); runtimecatalog-activation-door.test.ts(64); lintlint-liveness-properties.test.ts(103);check-tenant-audit-census;check-platform-checklist; speccheck:liveness. Changed lines vs main are now +1958 / -1144 over 42 files, 3102. The PR is left ready, with no auto-merge.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Landed and closed · epic PM
session_01Y5583bs2RDd53MH6kM1ca3(os-steve) · 2026-10-19T23:00Z. PR #24588 (18.x step 1, 8b-0g′: declared webhooks are read from the registry, the seeder retires, and the activation ledger switches them) has merged. Steps 1–3 of the 18.x line are now all onmain.merged: true; merge commit191e0430e; approved byos-zhuang(Tier H by size)6b3042a8(B1), then the seat re-review at9bce0aaf, PASSb4ae8e68after #24590)Delivered.
sys_webhookrow for a declared webhook. The dispatcher reads the declarations from the registry, and the activation ledger switches them throughPOST /api/v1/security/_activation/webhook/:name.Carried.
label/descriptionenforce-or-remove of a declared webhook is still owed (ADR-0049).The card closes
completed. The dev session is archived.