Repository navigation
feat(plugin-webhooks,runtime)!: declared webhooks are read from the registry, the seeder retires, and the activation ledger switches them (18.x step 1, 8b-0g′) - #24588
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>
📓 Docs Drift CheckThis PR changes 3 package(s): 16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 148 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 cd534ecc718637e49ec42f3365d5b68311df243f && git checkout cd534ecc718637e49ec42f3365d5b68311df243f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b12e16a561cae3da15c09b463b470ce760b11834 b4ae8e68332554376921625922f126d6e1e0c09f && git checkout -B drift-repro b12e16a561cae3da15c09b463b470ce760b11834 && git merge --no-ff b4ae8e68332554376921625922f126d6e1e0c09f
node scripts/docs-audit/affected-docs.mjs --json b12e16a561cae3da15c09b463b470ce760b11834
|
Contract reviewServed-tier: Reviewed by an isolated at-tier reviewer subagent of the adopting seat ( ① Derived judgments
② Semver level
③ Boundary flags
Blocking item, with its fix:
Implemented-by: VERDICT: FAIL |
…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>
Seat re-review ·
|
…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
#24588 (step 1) has landed. The drop refusal, os dev's way-on line and their pins from #24635 said "18.0 does not upgrade; keep a v17 database on 17.x": with the ceremony open they name its steps again, and the refusing class and its pin stay deleted. Claude-Session: https://claude.ai/code/session_014UATXWqAXqkaZkbG92ox6V Co-authored-by: Claude <noreply@anthropic.com>
Closes #24529
Clause-②: yes (narrowing)
18.x line, step 1 (8b-0g′). Ruling #24435 Q2 A (6259141885), under ruling records 6266750393 and 6267391172 on #15194. Stage detail: plan amendment 13 (6259616282) on #15212. Merge gate: this PR stays a draft until #24522's upgrade harness has merged; it then merges
mainand must be green on R1–R4.What changed
Declared webhooks are read from the registry.
bootstrapDeclaredWebhooksis deleted. A fresh boot writes zerosys_webhookrows for a declared webhook. ADR-0131 D2's sentence "No seeder writes them into any table" is now true of webhooks.plugin-webhooks/src/declared-webhooks.ts(new).DeclaredWebhookSourcereads thewebhookregistry items, falling back to the metadata service, as the seeder did. It parses each withWebhookSchema(an invalid item warns once per name) and resolves the activation-ledger switch.AutoEnqueuertakes adeclaredWebhooksreader. On every refresh it projects each declaration that is switched on into a cached subscription throughparseRow, so the trigger drift-guard and thewebhook/without-triggersrefusal apply as they do to a row. The secret and headers come from the declaration in memory and are never stored. The subscription id and every delivery'sref_idaredeclared:NAME.declaredWebhooksservice, on the data engine alone (no realtime needed). The dispatcher, the redelivery veto and the activation door share it.The switch: regime C (ADR-0131 D6), as
sharing_ruledid in #24428.sys_metadata_activationrows of typewebhookoverride the declaredisActivein either direction. A webhook shipped inactive is switched on the same way.runtime's catalog activation door,POST /api/v1/security/_activation/webhook/:name.webhookis admitted next tosharing_rule, and the two service-backed types now share one lookup table. The authority tiers are unchanged. The door answers404for an undeclared name, and503when no reader is bound or the read fails.declared:NAMEdelivery by its declaration: not declared any more, or switched off, is refused.The organization dimension (measured, then fixed in this PR). On
main(18.0, after 8b), the seededsys_webhookrow is stamped with the Default Organization. Measured on a freshmainshowcase:organization_id: org_…. So once activated, it delivered undersingle, because record events there carry that organization too. An org-less declaration would have been refused byadmitsOrganization, which is a silent loss.tenancyservice, read as the sharing plugin reads it), a declared webhook is deployment-wide. It receives every organization's events, and each delivery is stamped with the event's organization, as the 18.0 seeded copy's deliveries were.18.0-row behaviour (expand first: nothing is moved or deleted)
The 18.0 seeder's rows stay. A
sys_webhookrow holding a declared name is read the way the 18.0 seeder would have treated it on this boot (classifyDeclaredNameHolder). Choice, measured: the seeded row is recognised and ignored in favour of the registry entry when it is pristine, and the registry entry defers to it when it is not.managed_by: 'package', notcustomized)customized: true(an admin edited the seeded row)warnonce per name). An admin'sactive: falsekeeps holding.managed_by: 'admin'under a declared nameThe 18.0-shaped fixture is
seeded18_0Rowindeclared-webhooks.test.ts, byte-for-byte the seeder'smapWebhookToRowplus provenance.webhook-secret-at-rest.test.ts'sseed18_0Rowswrites the same rows through the realObjectQLengine, so both credentials land encrypted as they did.Ceremony debt (step 5), table by table
sys_webhook: delete the pristine declared mirrors (managed_byinpackage/platform,customizednot true, name still declared). Settle the customized seeded rows, as the sharing-rule ceremony settles its customised copies: a copy under a new name plus the declaration switched off. Then turn theorganization-ownership-inventory.tsrow'sdeclaredcategory into amirror-deletiondeparture (ADR-0131 D10 fate 2). That edit is ceremony behaviour (the apply executes departures), so this PR does not make it.sys_http_delivery: re-source the deleted mirrors' tails fromref_id = MIRROR_ROW_IDtodeclared:NAME, so history and redelivery follow the declaration.sys_secret: delete the cipher rows the deleted mirrors'signing_secret/headers_secretreferenced. They become orphans.sys_webhookcolumnsmanaged_by/customized: the mirror-row edit path "goes with the mirrors" (Q2 A). Once nopackagerow exists,customizedand thepackage/platformvalues ofmanaged_bydescribe nothing. Their help text still mentions boot seeding (left so the i18n bundles are not churned here).classifyDeclaredNameHolder, the mirror skip, the guard's mirror branch), the provenance stamp, andmigrateLegacyWebhookSecrets's package-row half.Tests
Run at head
6b3042a8(the final commit before this body).@objectstack/plugin-webhooks: 19 files, 212 tests, pass.typecheckexit 0 (check:test-typecheckOK).declared-webhooks.test.ts(new, 24): the fresh-install shape, including a plugin boot that records zerosys_webhookwrites; the ledger switch both ways; the ledger self-heal; a failed ledger read; the posture dimension; the 18.0 dual read (seven cases); the redelivery veto.webhook-secret-at-rest.test.ts: a declared webhook signs byte-identically with no row and no cipher stored, and needs no CryptoProvider. The at-rest, sweep and fail-closed pins now run over 18.0-seeded rows.bootstrap-declared-webhooks.test.ts, and the gate test of the seeder's own write path.@objectstack/runtime: full suite, 422 files, 6652 passed, 19 skipped.typecheckexit 0.catalog-activation-door.test.tsgains 7webhookpins.webhook-materialization.dogfood.test.ts: 1/1 pass. It now asserts nosys_webhookrow, plus a dispatch through the bounddeclaredWebhooksservice withobject→object_nameapplied.check:livenesspasses. The ledger is re-anchored to the registry read.labelanddescriptionare nowdead: their only reader was the seeded Setup row.Ablations (
scripts/ablation-replace.mjs; each leg restored, blob == HEAD,git diff HEADempty; the tests importsrc/relatively):doRefreshDeclaredWebhookSource.listdeploymentWideinadmitsOrganizationdeclared:branchdeclaredWebhookslookupFresh showcase boot at
6b3042a8,single:GET /data/sys_webhookreturns 0 rows.POST /security/_activation/webhook/showcase_task_changed {enabled:true}returns 200, and one ledger row (package_id: com.example.showcase).sys_http_deliveryrow withref_id: declared:showcase_task_changed, stamped with the Default Organization.The same steps on a fresh
mainshowcase gave the seeded row (organization_idstamped) and its delivery (ref_id: whk_…, same organization).Gates
Run locally at
6b3042a8. The commands are the 130 thatdispatch-gates --commandsderived, plus the 50-family artifact-roster block, 178 in all. Exit codes were captured to a file before any pipe.check:liveness,check:tenant-audit-census,check:engine-double-contract,check:objectql-double-limit,check:where-matcher,check:adr-0087-registration,check:changeset-no-major,check:platform-checklist, and the speccheck:generatedset.--ranreports 130 derived and 130 run, with 0 NOT-MEASURED.check-closing-target-claim,check-single-claim-pathsandcheck-governed-queue-guardneed PR or event context, which a local run does not have. CI runs them.check-partof-closing-keywordneeds the same. Run withPR_BODYset to this body, it is reported in the os-dev report on feat(plugin-webhooks): 18.x step 1 (8b-0g′): declared webhooks are read from the registry, the seeder retires, expand-first #24529.check-plugin-teardown-shape --self-testneeds the unshallowed history its fixture is pinned to. It is checker health only, not a PR verdict.Acceptance notes
packages/spec: liveness ledger rows, comments, and theWebhookSchemadoc block;packages/qa/dogfood: the webhook proof and its fixture (not 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);docs/qa/platform-checklist(revision 8);content/docs/automation/webhooks.mdx, plustenant-audit-census.mdxand its counts file (regenerated, 231 to 229: the seeder's two writes);scripts/*ledgers, re-keyed for the renamed test file or shrunk for the deleted source.organization-ownership-inventory.tsis unchanged: see the ceremony debt above.sys_webhookrow under a declared name, assharing_rulehas. Expand-first adds no new refusal. Such a row answers as itself and the declaration is withheld, which is what the 18.0 seeder did with it.label/descriptionof a declared webhook have no reader. Enforce-or-remove (ADR-0049) is owed: a Setup surface that lists declared webhooks, or retirement.Generated by Claude Code