Repository navigation
fix(plugin-webhooks,runtime): a declared webhook the deployment can never deliver is reported at error and refused at the activation door - #24946
Conversation
…t dispatch is reported at error and refused at the activation door Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check11 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 4 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 d7d1ebcf766a393b1a6110a59479ecc518d89a33 && git checkout d7d1ebcf766a393b1a6110a59479ecc518d89a33
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin feb09db14a6b84dd91ca756160ef101222bdd1dc 9e0ec86422df88182665719fc18acaf44495360d && git checkout -B drift-repro feb09db14a6b84dd91ca756160ef101222bdd1dc && git merge --no-ff 9e0ec86422df88182665719fc18acaf44495360d
node scripts/docs-audit/affected-docs.mjs --json feb09db14a6b84dd91ca756160ef101222bdd1dc |
…clared-webhook-capabilities
…tivation door's refusal and the reader verdict go The activation door's 200 -> 503 narrowing is the maintainer's to rule, so packages/runtime is back to main's bytes, and the declared-webhook reader's dispatch verdict (its only consumer was that door) is removed. Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK Co-authored-by: Claude <noreply@anthropic.com>
席位核对 · ACCEPT ·
|
Part of #24910 — this PR delivers the ruling's parts (b), (c) and the docs. Part (a), the
os validaterefusal, is left open as a fork for the seat: its honest home ispackages/spec, a hard stop for this dispatch (see "Part (a): the fork" below). Merging this PR must not close the card.Clause-②: no
What changes
The webhook auto-enqueuer reads record events only from the realtime service. A stack with
requires: ['webhooks']and without'realtime'therefore never delivers a declared webhook, while every door said yes.@objectstack/plugin-webhooks. The plugin decides atkernel:readywhether the auto-enqueuer can run, from three prerequisites: the engine, realtime and messaging. When it cannot and the stack declares at least one webhook, the boot logs oneerrorin place of the oldwarn. The line names each declared webhook and says none of them will be delivered while record writes keep succeeding. It names the missing service and the fix ("add 'realtime' torequires, then restart"). A stack that declares no webhook keeps the oldwarn.autoEnqueue: false(the host's own choice) logs nothing, as before.@objectstack/runtime. The declared-webhook reader (thedeclaredWebhooksservice) is now bound already knowing that verdict:dispatchUnavailable()returns the same sentence, ornullwhen the enqueuer runs.POST /api/v1/security/_activation/webhook/NAMEasks the reader before switching ON. On a deployment that delivers no declared webhook it answers503witherror.code: 'SERVICE_UNAVAILABLE'and writes nothing. That is the shape the door already uses when it cannot consult a reader. A reader that cannot say whether it dispatches is refused the same way.404; the declaration is confirmed first.content/docs/automation/webhooks.mdx§3.0 names the two tokens a declared webhook needs ('webhooks','realtime') and what each one's absence looks like.Files:
packages/plugins/plugin-webhooks/src/{webhook-outbox-plugin,declared-webhooks}.ts, newauto-enqueuer-disabled-report.test.ts;packages/runtime/src/domains/catalog-activation.tsand itscatalog-activation-door.test.ts; the doc;.changeset/24910-declared-webhook-undeliverable.md(patchfor both packages).Cross-lane: the seat's declaration on #22648, comment 6295927555, named
capability-preflight.ts(with itsvalidate/servecall sites) andcatalog-activation.ts. This PR touches onlycatalog-activation.tsand its test.capability-preflight.ts,validate.tsandserve.tsare untouched, because of the fork below.Premises, measured on
origin/main71696121a6AutoEnqueuer's only event source isIRealtimeService.subscribe(auto-enqueuer.tsstart(): three subscriptions, nothing else).webhook-outbox-plugin.ts's old gate disabled it without engine, realtime or messaging.messagingIS on the always-on slate (PLATFORM_ALWAYS_ON_CAPABILITIES,packages/spec/src/kernel/platform-capabilities.ts), which is why the card's step 6 readhasMessaging: true. Under--preset minimalit is not mounted, and the plugin'sdependencieson messaging then refuses the boot in the kernel's resolver; that path is not measured.os validateloads the stack (config.webhooks,config.requiresand eachpackages[]body) without booting, andpreflightDeclaredCapabilitiesalready readsrequiresfrom both places.packages/spec. See the fork.autoEnqueue: falsehas zero production callers in this tree, only tests:plugin-webhooks(3 sites) and one dogfood test. It is an option of an exported class, so this PR treats it as a supported host choice: no report, no door refusal. Its redelivery still reads the switch (redeliver-guard.tsjudgeDeclared).Part (a): the fork (needs the seat's decision)
The rule "a stack that declares webhooks names
'webhooks'and'realtime'inrequires" belongs to a class that already exists: the declared-capability refusals ofdefineStackinpackages/spec/src/stack.zod.ts. Its two members arevalidateHierarchyScopeCapability(STACK_HIERARCHY_SCOPE_CAPABILITY_REQUIRED) andvalidateTriggerCapability(STACK_TRIGGER_CAPABILITY_REQUIRED). The trigger rule states the same token-needs-token fact in spec text ("triggersinstalls the trigger INTO the automation service … ⛔triggersnever IMPLIESautomation"), and its docblock calls itself "an ENFORCED capability class, exactly like the hierarchy scopes above". Every door that loads a stack runs those refusals:os validate,os build,os serve/os dev,@objectstack/verify'sbootStack, and cloud's runtime. So "serve reads the same rule" holds without a second call site. The ruling's hard stop says: if the honest home ispackages/spec, report a fork. ⛔ No token is implied.defineStackrefusal in spec, beside the trigger rule (domain:spec)bootStack. Fixture triage:packages/qa/dogfood/test/fixtures/webhook-materialization-fixture.tsdeclares a webhook with norequiresand deliberately boots without realtime. Six other files in lint and spec spellwebhooks: [(five tests andconversions/registry.ts; some are a connector's retired nested key, not a stack's), to scan.defineStack, even inside a test.defineStack's accept set narrows (Clause-②narrowing,minor). Cloud's resolver forwebhooksis NOT MEASURED (the trigger rule measured cloud'sresolveCapabilityDependencies); measure it first.capability-preflight.ts, domain:cli)os validate/os build, andos serveafter a new call site. MissesbootStack, so an app's tests stay green on the inert stack, and misses cloud's runtime.node_modules, not the stack",validate.tsstep 3b).plugin-webhooksdeclares the realtime plugin independenciesos validate. It refuses awebhooksstack with no declared webhook and anautoEnqueue: falsehost, which H4 rules out.Recommendation: A. It joins the existing class in the one place every door reads, and its extra cost (one cloud measurement and one error code) is smaller than giving one class two homes. Under A and under B alike, a host that wires
WebhookOutboxPluginthroughplugins[]without naming the tokens is refused: the token is the declaration, as for triggers. H4 measured no production host of that shape.Tests (head
02d22e1eec)auto-enqueuer-disabled-report.test.ts. A realLiteKernelboot runs the plugin's realkernel:ready. It uses a realObjectQLover a real in-memorySqlDriver, the webhook fromdefineWebhookthrough the engine's realregisterAppdecomposition, and messaging's realMessagingService. Only realtime is a stand-in.errornaming the webhook and the fix, nodisabledwarn, anddispatchUnavailable()names'realtime'. An insert plus an update enqueue nothing.null, and the same writes enqueue two deliveries (declared:my_app_note_changed, created then updated).warn, never the error.autoEnqueue: false: nothing reported, andnull.catalog-activation-door.test.tsadds 7 cases:SERVICE_UNAVAILABLEnaming the webhook and the reader's reason, with nothing written. An empty body (the default, ON) is refused the same way.dispatchUnavailablegets 503 on ON and is written on OFF. A reader that throws gets 503.pnpm --filter @objectstack/plugin-webhooks exec vitest run --maxWorkers=2: 23 files, 231 tests passed.typecheckexit 0.pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2: 435 files, 7609 passed, 19 skipped.typecheckexit 0.scripts/ablation-replace.mjs(WRAP mode, anchor x1 to x0, blob moved), and each restore was proven by blob equality with HEAD and an emptygit diff HEAD. The subjects resolve through relativesrcimports, so nodistwas involved.Local gates (head
02d22e1eec)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths. The change set is the 7 files since the merge base71696121a6, and the tool printed 100 commands. Each ran once, sequentially, with its exit captured before any pipe.PREREQUISITE NOT MET):check:skill-examples(client SDK not built),check:dual-build-cjs-loadsandcheck:i18n(dists not built).turbo run buildunder the verify lock (76/76 tasks), all three re-ran and exited 0. The working tree was still clean after the build.node scripts/pm/dispatch-gates.mjs --rananswered "100 derived famil(ies) accounted for — 100 run, 0 NOT-MEASURED", exit 0.pnpm lintfirst among them) and the families only CI runs are left to this PR's checks.Acceptance notes
sys_webhookrows on the same composition. A stack with'webhooks'and without'realtime'delivers no authored row either. With no declared webhook that stays awarn, as the ruling scoped (c). Known from code reading only; it is not measured through a public door here.redeliver-guard.tsreads the switch). The door now refuses that ON too; adding'realtime'is the fix.Clause-②. This PR copies the claim'sClause-②: no. The activation door's change is a REST accept-set narrowing on such a deployment (200to503). By the precedent of.changeset/24813-adopt-orphans-readonly-package.md, that may readClause-②: no (narrowing), with aminorruntime bump, a BREAKING note and an ADR-0087 marker in the changeset. That is the seat's call.Round 2 (head
9e0ec86422): the boot report and the docs onlyThe seat's review ruled that the activation door's
200to503change is a REST accept-set narrowing, which is protocol class. That goes to the maintainer together with part (a), so this PR now carries part (c) and the docs. The sections above that describe (b), the activation door's refusal anddispatchUnavailable(), no longer describe this diff.packages/runtime/src/domains/catalog-activation.tsandcatalog-activation-door.test.tsare back tomain's bytes, so the PR'spackages/runtimediff is empty.dispatchUnavailable()verdict, together with itsDeclaredWebhookDispatchinterface and constructor parameter. The door was its only consumer. The boot report never read it: it builds its sentence from the wiring it resolves itself.bootWebhookTableand thekernel:readyhook are byte-identical tomainagain.errorat boot when the auto-enqueuer cannot run and the stack declares webhooks. It names each declared webhook, the consequence and the fix. The oldwarnstays when no webhook is declared, andautoEnqueue: falsestays silent. The only addition todeclared-webhooks.tsisreadDeclaredWebhookNames(), which the report needs to name the webhooks.webhooks.mdx§3.0 names'webhooks'+'realtime'. It claims no refusal by the activation door or byos validate; it says only that switching a declared webhook on delivers nothing while'realtime'is missing.@objectstack/plugin-webhookspatchonly,Clause-②: no. "Unchanged" namesos validate/os buildand the activation door.origin/mainatfeb09db14a; it touched onlypackages/cli, with no overlap.Files now:
.changeset/24910-declared-webhook-undeliverable.md,content/docs/automation/webhooks.mdx,packages/plugins/plugin-webhooks/src/{auto-enqueuer-disabled-report.test,declared-webhooks,webhook-outbox-plugin}.ts.Tests on
9e0ec86422:pnpm --filter @objectstack/plugin-webhooks exec vitest run --maxWorkers=2: 23 files, 231 tests passed.typecheckexit 0.autoEnqueue: falseis silent.git diff HEAD.Gates on
9e0ec86422:dispatch-gates.mjs --commands --repo objectstack-ai/objectstackprinted 100 commands (5 files vs the merge basefeb09db14). All 100 ran under the verify lock and exited 0, after a fullturbo run build(76/76).--ranreported "100 run, 0 NOT-MEASURED", exit 0. Round 1's head02d22e1eecfinished CI with 36 success and 6 skipped out of 42 check-runs, none red.Generated by Claude Code