Skip to content

A declared webhook in a scaffolded app is silently inert: os validate passes without the 'webhooks' and 'realtime' capabilities, and with 'webhooks' alone the activation answers enabled while nothing is ever enqueued (WARN only) #24910

Closed

Description

@objectstack-fleet

Ruled: 6303859748 · letter A · 2026-10-20T23:14Z

Blocked-by: #24997

Filing gate: ① product defect with a named location and a reproduction (a declared, activated delivery that is never made, with the loss logged at WARN only).

reach: in a create-objectstack@18.0.0 project, add webhooks: [defineWebhook(...)] to objectstack.config.ts; os validate, os dev, activation 200, then a record write enqueues nothing (SQLite, published npm 18.0.0).
QA-source: #24908 · smoke:npm-18.0.0 · E5
type: Bug

Reproduction (published 18.0.0, SQLite)

  1. npm create objectstack@18.0.0 my-app -- --skip-skills; cd my-app. The scaffold's requires is ['automation', 'triggers'].
  2. In objectstack.config.ts, add the import defineWebhook from '@objectstack/spec/automation' and the stack key
    webhooks: [defineWebhook({ name: 'my_app_note_changed', object: 'my_app_note', triggers: ['create','update','delete'], url: 'http://127.0.0.1:3999/hook', method: 'POST', isActive: true })]
  3. npx os validate: "Validation passed", including "Checking that every required capability has a provider". npx os build exit 0.
  4. npx os dev --no-watch -p PORT -d file:FRESH. The boot log says nothing about webhooks. Sign in as the dev admin.
  5. POST /api/v1/security/_activation/webhook/my_app_note_changed with body {"enabled": true}: 503 SERVICE_UNAVAILABLE "Cannot verify the declaration of webhook ... no declared-webhook reader is bound to this deployment".
  6. Change requires to ['automation', 'triggers', 'webhooks'], rebuild, and boot a fresh file.
    • Boot: one WARN "[webhook-auto-enqueuer] disabled — ObjectQL, Realtime, or Messaging service not available {hasEngine:true, hasRealtime:false, hasMessaging:true}".
    • Activation: 200 {"type":"webhook","name":"my_app_note_changed","enabled":true}.
    • POST a my_app_note (201), then PATCH it (200). Wait 5 s. GET /api/v1/data/sys_http_delivery: total 0.
  7. Add 'realtime' to requires as well: the same steps give 2 delivery rows (declared:my_app_note_changed, pending), which is the expected result.

What is wrong

  • The stack declares something that never happens, and every door says yes: validate passes, the activation door answers enabled:true, and the record writes answer 200. In step 6 the only trace is one WARN at boot, before any write. Under AGENTS.md "Degradation log levels", a dropped durable write is an error, said once, with its consequence and its fix.
  • Nothing ties the pieces together. A non-empty webhooks collection does not imply (or require) the 'webhooks' token, and the 'webhooks' token does not imply 'realtime', which its enqueuer cannot run without. automation/webhooks.mdx names neither token. The working combination is visible only in examples/app-showcase's requires list.

Acceptance: a stack whose metadata declares webhooks, but whose requires cannot dispatch them, is refused at os validate or at boot with the missing token named, or the tokens are implied. And the activation door does not answer enabled:true on a deployment whose enqueuer is disabled.

Activity

  1. added
    bugSomething isn't working
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    priority:p1High: required for production / M2
    on Oct 20, 2026
  2. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 13 · 2026-10-20T16:32Z
    Session: session_011TdZ8GjixNZFLz1xJD9zzK
    Account: os-justin (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24910-declared-webhook-capabilities
    Worktree: objectstack-issue-24910
    Domain: domain:services
    Seat: domain:services#1
    File surface: packages/plugins/plugin-webhooks/src/** (the outbox plugin's auto-enqueuer gate and its log level, the declared-webhook reader) and its tests; cross-lane into domain:cli, declared on #22648 at this claim: packages/cli/src/utils/capability-preflight.ts with the validate / serve call sites that read it, and packages/runtime/src/domains/catalog-activation.ts (the webhook activation subject), with their tests; content/docs/automation/webhooks.mdx; one new .changeset/24910-*.md. ⛔ Not packages/core/** (capability-providers.ts, capability-composition.ts, dependency-resolver.ts are domain:engine's), ⛔ not packages/spec/**, ⛔ no token implied or auto-added to requires (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default (opus), quoting this dispatch's --tier output "no path-derived mandate: the surface hits none of the 3 declared glob(s) … floor sonnet · default opus · ceiling fable"
    Clause-②: no
    Responsibility: plugin-webhooks disables its auto-enqueuer with one boot WARN while the activation door answers enabled:true, and the CLI's capability preflight does not tie a non-empty webhooks collection to the tokens its dispatch needs | none: the working requires combination is visible only in examples/app-showcase | every app author who declares a webhook per automation/webhooks.mdx in a scaffold whose requires is ['automation', 'triggers']
    Thread-read: none
    Serial constraints cleared: No open PR touches plugin-webhooks, capability-preflight.ts or catalog-activation.ts (all 14 open PRs' files read at this claim). #24835 (PR #24906, eee65afc89) and its round 1 (auto-enqueuer.ts, 3fbef41b4a) have landed. #24900 (domain:spec, pm:queue, the declared webhook's label/description keys) is not in flight.

    Seat ruling (third tier, PM discretion; the maintainer's veto window applies). Governing text: AGENTS.md "Degradation log levels — warn vs error"; the decision frame's third axis (a loud refusal at publish over consumer-side tolerance; a declared capability the runtime does not honour is never allowed). The card's acceptance offers refusal or implication, and these texts settle it for refusal.

    1. Route.
      • os validate refuses a stack whose webhooks collection is non-empty while requires lacks a token its dispatch needs, naming each missing token. serve reads the same rule.
      • The webhook activation subject does not answer enabled: true while the auto-enqueuer is disabled.
      • A disabled auto-enqueuer on a stack with declared webhooks is reported once, at error, with its consequence and its fix.
    2. Premises, each measured before any edit:
      • P1. The auto-enqueuer's only event source is the realtime service, so webhooks without realtime cannot deliver a declared webhook (webhook-outbox-plugin.ts:369–374 at 94842bad2a).
      • P2. os validate can read both the stack's webhooks collection and its requires without booting.
      • P3. The rule "token X's dispatch needs token Y" can be stated with no edit to packages/core or packages/spec.
    3. If any premise fails, report a fork. ⛔ No silent fallback to implying tokens.

    os validate's accept set narrows. That owes one contract-review-tier review before the queue, which the seat runs at review.

    Readings taken on origin/main 94842bad2a.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24910,
    "status": "needs_decision",
    "branch": "claude/issue-24910-declared-webhook-capabilities",
    "pr": "#24946",
    "session": "session_011TdZ8GjixNZFLz1xJD9zzK — this run's harness-stamped id (subagent of the seat; the claim comment 6295896264 names it)",
    "premise_still_valid": true,
    "summary": "Delivered the ruling's parts (b), (c) and the docs in draft PR #24946 (Part of #24910, so merging it does not close the card). (c): plugin-webhooks decides at kernel:ready whether its auto-enqueuer can run. When it cannot (no realtime) and the stack declares webhooks, the boot logs ONE error that names each declared webhook, the consequence (never delivered while writes succeed) and the fix (add 'realtime' to requires). With no declared webhook it stays the old warn, and autoEnqueue: false stays silent. (b): the declaredWebhooks reader is bound already knowing that verdict (dispatchUnavailable()). POST /api/v1/security/_activation/webhook/NAME refuses ON with 503 SERVICE_UNAVAILABLE and writes nothing there. OFF is still written, an undeclared name is still 404 and the authority gates still answer first. Part (a), the os validate refusal, is NOT implemented: P3 is falsified as stated. The honest home of 'a stack declaring webhooks names webhooks + realtime in requires' is packages/spec's defineStack. The same declared-capability class already lives there (validateTriggerCapability, validateHierarchyScopeCapability), and every door reads it. That is a hard stop, so this run reports a fork, with no token implied.",
    "tests": "All on head 02d22e1 (code identical to 96a99eb, where the ablation ran). (1) pnpm --filter @objectstack/plugin-webhooks exec vitest run --maxWorkers=2: Test Files 23 passed (23), Tests 231 passed (231). It includes the new src/auto-enqueuer-disabled-report.test.ts: a real LiteKernel, real ObjectQL over a real in-memory SqlDriver, the webhook from defineWebhook through engine.registerApp, and a real MessagingService; 4 cases: no realtime = 1 error + reader names 'realtime' + insert/update enqueue 0; CONTROL with realtime = 0 errors + null + 2 deliveries declared:my_app_note_changed created/updated; no declared webhook = warn only; autoEnqueue:false = silent + null. typecheck: exit 0 (check:test-typecheck OK, 0 debt). (2) pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2: Test Files 435 passed (435), Tests 7609 passed | 19 skipped (7628). catalog-activation-door.test.ts 71 passed (7 new cases). typecheck exit 0. (3) Ablation: one leg per changed behaviour, each through scripts/ablation-replace.mjs in WRAP mode (anchor x1 to x0, blob moved), each restore proven blob == HEAD with git diff HEAD empty. Subjects resolve through relative src imports, so no dist was involved. Leg A, the error branch never taken: 1 failed | 3 passed ('expected [] to have a length of 1'). Leg B, the reader born without the gap: 2 failed | 2 passed. B's FIRST attempt was a no-op: its replacement 'null,' already occurred in the anchor, the tool refused ('replacement count moved 1 -> 1') and restored, and B was re-run with a distinct replacement. Leg C, the door never asks: 4 failed | 67 passed (200 where 503 was expected). The card's own path, a published 18.0.0 scaffold outside the workspace: NOT MEASURED, reason: an npm install plus a boot on a shared box; the defect is reproduced in-tree by leg 1 against its control.",
    "gates": "Local, head 02d22e1. Closure build: pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-webhooks^...' --filter '@objectstack/runtime^...' build, VERDICT command-exit 0. The CLI closure was not built: packages/cli is untouched. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; change set = 7 files vs merge base 7169612; stderr names the tree at 02d22e1) printed 100 commands. Each ran once, sequentially, exit captured before any pipe (runner PID 2913, nohup, waited with tail --pid). First pass: 97 exit 0, 3 exit 3 PREREQUISITE NOT MET (spec check:skill-examples, check:dual-build-cjs-loads, check:i18n). After pnpm exec turbo run build --filter=!@objectstack/docs --concurrency=2 under the lock (76/76 tasks, VERDICT command-exit 0, tree still clean), the three re-ran and exited 0 (skill-examples '262 prose examples type-check'; i18n 'all bundles in sync'). --ran: '100 derived famil(ies) accounted for — 100 run, 0 NOT-MEASURED', exit 0. Not derived-family: check-platform-checklist was not run, since no exported symbol was renamed or deleted. CI on PR #24946 head 02d22e1 at report time, 25 check-runs: 'No other open PR may claim the same issue' success; '18.x upgrade harness (opt-in)' and 'Packed-tarball smoke (opt-in)' skipped; in_progress: Check Changeset, Check Documentation Links, Check PR Size, Flag docs affected by code changes, Governed Surface Queue Guard, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, The card this PR closes must claim this branch, Type Check · consumer gates / debt ledger / source gates / workspace, filter. Test Core and Build Core had not appeared yet.",
    "mcp_calls": "0 — no MCP tool was called",
    "api_writes": "2 — two relay strokes, each POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create, which the relay ran as POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/24946/assignees [os-justin], run 39465476791 success, body read back byte-identical; (2) comment, POST /repos//issues/24910/comments (this report). Plus 3 git pushes (the empty-branch probe, the WIP commit, the docs/changeset commit), which are not REST. No labels written.",
    "files_changed": [
    ".changeset/24910-declared-webhook-undeliverable.md",
    "content/docs/automation/webhooks.mdx",
    "packages/plugins/plugin-webhooks/src/auto-enqueuer-disabled-report.test.ts",
    "packages/plugins/plugin-webhooks/src/declared-webhooks.ts",
    "packages/plugins/plugin-webhooks/src/webhook-outbox-plugin.ts",
    "packages/runtime/src/domains/catalog-activation-door.test.ts",
    "packages/runtime/src/domains/catalog-activation.ts"
    ],
    "deviations": [
    "The PR's first line is 'Part of #24910', not the dispatch's 'Fixes #24910': part (a) is forked and still open. os-dev.md requires Part-of when merging must not close the card.",
    "Part (a) is not implemented (the P3 fork). capability-preflight.ts, validate.ts and serve.ts are untouched. From the #22648 declaration (comment 6295927555), only catalog-activation.ts and its test were touched.",
    "Changeset bumps: @objectstack/plugin-webhooks patch, @objectstack/runtime patch. Its Before/After covers the boot report and the activation door. It states that os validate / os build are unchanged, because the validate narrowing was not built.",
    "Clause-② was copied verbatim from the claim as 'Clause-②: no', into the PR body and the changeset. See open_questions on the arm."
    ],
    "open_questions": [
    {
    "question": "Where does part (a) live: the refusal of a stack whose webhooks collection is non-empty while requires lacks 'webhooks' and/or 'realtime'? Premises measured on origin/main 7169612. P1 holds: realtime.subscribe is the auto-enqueuer's only source, and messaging IS in PLATFORM_ALWAYS_ON_CAPABILITIES. P2 holds. H4: autoEnqueue:false has 0 production callers, only 3 plugin-webhooks tests and 1 dogfood test.",
    "options": [
    "A — a third defineStack declared-capability refusal in packages/spec/src/stack.zod.ts, beside validateTriggerCapability / validateHierarchyScopeCapability, with a new ADR-0112 code (e.g. STACK_WEBHOOK_CAPABILITY_REQUIRED); domain:spec. Business need: covers every door that loads a stack (os validate, os build, os serve/dev, verify's bootStack, cloud's runtime). Fixture triage: the dogfood webhook-materialization fixture declares a webhook with no requires and deliberately boots without realtime; 6 other lint/spec files spell webhooks: [ and must be scanned. Long-term: one class, one home, and 'serve reads the same rule' with no second call site. AI-error: refused at defineStack, even in a test. Startup scope: no new gate; one error code; defineStack's accept set narrows (Clause-② narrowing, minor). Cloud's resolver for 'webhooks' is NOT MEASURED; the trigger rule measured cloud's resolveCapabilityDependencies, so measure that first. Effort S–M.",
    "B — a rule in packages/cli/src/utils/capability-preflight.ts, read by validate/build through preflightDeclaredCapabilities and by serve through a new call; domain:cli. Business need: covers the CLI doors only. bootStack (an app's own tests) and cloud stay silent on the inert stack. Long-term: a second home for one class (two members in spec, one in the CLI). The preflight's charter is provider resolution ('Not a registry rule: it reads node_modules, not the stack', validate.ts step 3b). AI-error: loud at the CLI, silent in-process. Startup scope: no spec lane, no error code. Effort S.",
    "C — plugin-webhooks adds the realtime plugin to its dependencies: boot-time 'Missing dependency' only, invisible to os validate. It refuses a webhooks stack with no declared webhook and autoEnqueue:false hosts, which H4 forbids. Rejected."
    ],
    "recommendation": "A, because it joins the existing declared-capability class in the one place every door reads, so validate, build, serve, an app's bootStack tests and cloud all refuse in one vocabulary. Its extra cost, one cloud measurement and one error code, is smaller than B's permanent second home for one class. Under A or B alike, a host that wires WebhookOutboxPlugin through plugins[] without naming the tokens is refused: the token is the declaration, as for triggers, and H4 found no production host of that shape."
    },
    {
    "question": "Does the activation door's change take the narrowing arm? On a deployment without realtime, POST _activation/webhook/NAME with enabled:true moves from 200 (row written) to 503 SERVICE_UNAVAILABLE (nothing written). That is a REST accept-set narrowing on that composition.",
    "options": [
    "A — keep 'Clause-②: no' as the claim declared: a 200 that switched nothing is a defect, and the refusal is the fix (patch).",
    "B — re-declare 'Clause-②: no (narrowing)', following .changeset/24813-adopt-orphans-readonly-package.md (a REST door's 200 turned into a refusal was declared narrowing). The changeset would then need: runtime 'minor', a BREAKING FROM/TO block and an adr-0087 not-required (no-migration-prescription) marker."
    ],
    "recommendation": "B, by the #24813 precedent: a client that switched a declared webhook on gets a new status. The seat owns the line, though, so this run copied 'no'. The rewrite is the changeset file alone, plus the body's Clause line."
    }
    ],
    "out_of_scope_findings": [
    "carrier: #24910 (this card's open half; the seat) · noted, not filed — on the same composition ('webhooks' without 'realtime') an organization-authored sys_webhook row is never delivered either. With no declared webhook that stays at warn, as the ruling scoped (c). Known from code reading only (AutoEnqueuer never starts, so no row is dispatched); not measured through a public door, so it is not filable here. Dedupe words: authored sys_webhook inert without realtime, auto-enqueuer disabled warn authored rows, webhooks realtime requires authored.",
    "carrier: none · noted, not filed — under '--preset minimal' messaging is not mounted. plugin-webhooks' dependencies on com.objectstack.service.messaging would then refuse the boot in the kernel's dependency resolver ('Missing dependency', dependency-resolver.ts:212). NOT MEASURED; mentioned only because the new boot line's fix names that preset."
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24910,
    "round": 2,
    "status": "done",
    "branch": "claude/issue-24910-declared-webhook-capabilities",
    "pr": "#24946",
    "session": "session_011TdZ8GjixNZFLz1xJD9zzK — this run's harness-stamped id (subagent of the seat; the claim comment 6295896264 names it)",
    "premise_still_valid": true,
    "summary": "Round 2, per the seat's review. PR #24946 now carries part (c) and the docs only, and stays 'Part of #24910'. Removed: the activation door's refusal (packages/runtime is back to main's bytes, empty diff), and the declared-webhook reader's dispatchUnavailable() verdict with its DeclaredWebhookDispatch interface and constructor parameter. The door was the verdict's only consumer, and the boot report never read it. bootWebhookTable and the kernel:ready hook are byte-identical to main again. Kept as built: ONE error at boot naming each declared webhook, the consequence and the fix; warn when no webhook is declared; silent under autoEnqueue: false. What (c) needs in declared-webhooks.ts is readDeclaredWebhookNames() alone. The docs name 'webhooks' + 'realtime' and claim no refusal. The changeset is @objectstack/plugin-webhooks patch only, Clause-②: no, and its 'Unchanged' names os validate / os build and the activation door. Merged origin/main at feb09db first (packages/cli only, no overlap). Open questions 1 and 2 go to the maintainer, and the seat writes that decision.",
    "tests": "On head 9e0ec86. pnpm --filter @objectstack/plugin-webhooks exec vitest run --maxWorkers=2: Test Files 23 passed (23), Tests 231 passed (231). It includes the four-case real-kernel pin src/auto-enqueuer-disabled-report.test.ts (real LiteKernel, real ObjectQL over an in-memory SqlDriver, defineWebhook through engine.registerApp, real MessagingService): no realtime = 1 error naming the webhook and "add 'realtime' to requires", 0 enqueued; CONTROL with realtime = 0 errors, 2 deliveries declared:my_app_note_changed created/updated; no declared webhook = warn only; autoEnqueue:false = silent. pnpm --filter @objectstack/plugin-webhooks typecheck: exit 0 (check:test-typecheck OK, 0 debt). Ablation leg A, re-run on 9e0ec86 through scripts/ablation-replace.mjs in WRAP mode: anchor 'if (declared.length > 0) {' x1 to x0, blob 1ca4e8261438 to e5ba05b5a301; 'Tests 1 failed | 3 passed (4)', 'expected [] to have a length of 1 but got +0'. Restore: blob-equal 1ca4e82614386e5569c64d6db634b6f1eadc65e2 == HEAD, git diff HEAD empty. Legs B and C are retired along with the code they tested. packages/runtime was not re-tested: its diff against main is 0 lines (git diff feb09db -- packages/runtime).",
    "gates": "Local, head 9e0ec86, all under the verify lock. Closure: pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-webhooks^...' build, VERDICT command-exit 0. Full pnpm exec turbo run build --filter=!@objectstack/docs --concurrency=2: 76/76 tasks (62 cached), VERDICT command-exit 0, tree clean afterwards. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; stderr names the tree at 9e0ec86; 5 files vs merge base feb09db) printed 100 commands, the same set as round 1. A single runner (lock-wrapped, PID 7870, nohup, waited with tail --pid) ran each command once with its exit captured before any pipe; lock VERDICT command-exit 0 after 774s. Result: 100 of 100 exit 0. --ran: '100 derived famil(ies) accounted for — 100 run, 0 NOT-MEASURED', exit 0. CI: the round-1 head 02d22e1 completed with 42 check-runs, 36 success and 6 skipped, none red. The round-2 head 9e0ec86 had 25 check-runs at report time: 'No other open PR may claim the same issue' success; '18.x upgrade harness (opt-in)' and 'Packed-tarball smoke (opt-in)' skipped; the rest in_progress (Check Changeset, Check Documentation Links, Check PR Size, Flag docs affected by code changes, Governed Surface Queue Guard, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, The card this PR closes must claim this branch, Type Check · consumer gates / debt ledger / source gates / workspace, filter).",
    "mcp_calls": "0 — no MCP tool was called in either round",
    "api_writes": "1 this round: one relay stroke, POST /repos/objectstack-ai/objectstack/dispatches, running comment = POST /repos//issues/24910/comments (this round-2 report). Plus 1 git push (02d22e1..9e0ec86: the merge of origin/main and the round-2 commit), which is not REST. The PR body was not PATCHed: round2_pr_markdown below is for the seat to append. Round 1's writes (pr_create stroke and report comment 6296915123) are unchanged. No labels written. The PR now carries documentation, size/l, tests and tooling; those were set by the repo's labelers, not by this run.",
    "files_changed": [
    ".changeset/24910-declared-webhook-undeliverable.md",
    "content/docs/automation/webhooks.mdx",
    "packages/plugins/plugin-webhooks/src/auto-enqueuer-disabled-report.test.ts",
    "packages/plugins/plugin-webhooks/src/declared-webhooks.ts",
    "packages/plugins/plugin-webhooks/src/webhook-outbox-plugin.ts"
    ],
    "deviations": [
    "The PR body is now stale where it describes part (b) and dispatchUnavailable(). It was not PATCHed, per the patch-round order; round2_pr_markdown opens by saying so, for the seat to append.",
    "Local git worktree was removed after round 1 and recreated on the same branch for round 2 (no remote change in between)."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: #24910 (the open half; the seat) · noted, not filed — unchanged from round 1. On a 'webhooks'-without-'realtime' composition an authored sys_webhook row is never delivered either, and with no declared webhook that stays at warn. Known from code reading only; not measured through a public door."
    ],
    "round2_pr_markdown": "## Round 2 (head 9e0ec86422): the boot report and the docs only\n\nThe seat's review ruled that the activation door's 200 to 503 change 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 and dispatchUnavailable(), no longer describe this diff.\n\n- Removed:\n - The activation door's refusal. packages/runtime/src/domains/catalog-activation.ts and catalog-activation-door.test.ts are back to main's bytes, so the PR's packages/runtime diff is empty.\n - The declared-webhook reader's dispatchUnavailable() verdict, together with its DeclaredWebhookDispatch interface 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. bootWebhookTable and the kernel:ready hook are byte-identical to main again.\n- Kept, as built: ONE error at boot when the auto-enqueuer cannot run and the stack declares webhooks. It names each declared webhook, the consequence and the fix. The old warn stays when no webhook is declared, and autoEnqueue: false stays silent. The only addition to declared-webhooks.ts is readDeclaredWebhookNames(), which the report needs to name the webhooks.\n- Docs: webhooks.mdx §3.0 names 'webhooks' + 'realtime'. It claims no refusal by the activation door or by os validate; it says only that switching a declared webhook on delivers nothing while 'realtime' is missing.\n- Changeset: @objectstack/plugin-webhooks patch only, Clause-②: no. "Unchanged" names os validate / os build and the activation door.\n- Merged origin/main at feb09db14a; it touched only packages/cli, with no overlap.\n\nFiles 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.\n\nTests on 9e0ec86422:\n- pnpm --filter @objectstack/plugin-webhooks exec vitest run --maxWorkers=2: 23 files, 231 tests passed. typecheck exit 0.\n- The four-case real-kernel pin: no realtime gives 1 error and 0 enqueued; the CONTROL with realtime gives 2 deliveries; no declared webhook gives the warn only; autoEnqueue: false is silent.\n- Ablation leg A, re-run on this head: the error branch is never taken, and the run reads "1 failed | 3 passed" ("expected [] to have a length of 1"). The restore was proven by blob equality with HEAD and an empty git diff HEAD.\n\nGates on 9e0ec86422: dispatch-gates.mjs --commands --repo objectstack-ai/objectstack printed 100 commands (5 files vs the merge base feb09db14). All 100 ran under the verify lock and exited 0, after a full turbo run build (76/76). --ran reported "100 run, 0 NOT-MEASURED", exit 0. Round 1's head 02d22e1eec finished CI with 36 success and 6 skipped out of 42 check-runs, none red."
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24946 comment 6297908339 · domain:services seat 1 · session_011TdZ8GjixNZFLz1xJD9zzK · 2026-10-20T18:15Z


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed (part) · domain:services seat 1 · session_011TdZ8GjixNZFLz1xJD9zzK · 2026-10-20T18:44Z

    reading result
    PR #24946 timeline added_to_merge_queue 2026-10-20T18:23Z → merged 2026-10-20T18:43Z
    origin/main by content squash 19e5338cc8 (one parent, 8cfe1f94cb), 5 files, all under plugin-webhooks, webhooks.mdx and the changeset. declared-webhooks.ts exports readDeclaredWebhookNames. packages/runtime is untouched
    closing references the PR body's first line is Part of #24910, so this card stays open

    A correction to the record. The squash commit's title, "… and refused at the activation door", is round 1's PR title, which was never updated after the patch round. The activation door is NOT changed (the squash touches no packages/runtime file). The changeset, which the release notes are built from, says so correctly: "The activation door … answers exactly as before."

    What changes for users: when a stack declares webhooks but the deployment cannot dispatch them (no realtime), the boot logs ONE error naming each webhook, the consequence and the fix, instead of a bare warning. webhooks.mdx names the two tokens a declared webhook needs (ACCEPT 6297908339).

    What is left: parts (a), the defineStack refusal, and (b), the activation door. Both are protocol class. The decision is posted below, and the card moves to needs-user-decision.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    决策请求 · domain:services seat 1 · os-justin · session_011TdZ8GjixNZFLz1xJD9zzK · 2026-10-20T18:45Z

    依据:os-dev 报告 6296915123(本卡),读数基于 origin/main 71696121a6。席位在认领 6295896264 中的第三档裁决(响亮拒绝,⛔ 不隐式补 token),其前提 P3 被实测证伪。本卡能由席位落地的部分是 PR #24946(Part of #24910):启动时对每个声明的 webhook 报一次 error,并补文档。剩下两半都属协议类,交维护者裁。

    一句话问题: 应用作者照文档声明了一个 webhook,校验、构建、启动、激活全都说「好」,记录写入也成功,但 webhook 一次都不会投递。要不要在应用加载时就把这种配置拒掉?拒在哪一层?

    背景

    Governing text

    协议声明、是否改协议

    两半都改协议:

    • defineStack 的接受集收窄,并新增一个错误码;
    • 激活接口在该组合下从 200 改为 503。

    后者按 #24813 的先例(.changeset/24813-adopt-orphans-readonly-package.md)声明为 Clause-②: no (narrowing):minor 加 BREAKING 横幅,附 adr-0087 not-required 标记。

    前提(各带 re-check 命令)

    1. 自动入队唯一的事件来源是 realtime。re-check:git grep -n "tryGetService<IRealtimeService>" packages/plugins/plugin-webhooks/src/webhook-outbox-plugin.ts
    2. messaging 在常开能力清单里,无需声明。re-check:git grep -n "PLATFORM_ALWAYS_ON_CAPABILITIES" packages/spec/src/kernel/platform-capabilities.ts
    3. autoEnqueue: false 没有生产调用方。re-check:git grep -n "autoEnqueue: false" -- packages examples ':!*.test.ts',在 71696121a6 上命中 3 处:1 处 CLI 门禁脚本(check-app-nav-i18n.mjs:352)、2 处注释;其余只在测试里。
    4. cloud 对 webhooks 的能力解析未测量。trigger 规则当初测过 cloud 的 resolveCapabilityDependencies;选 A 时须先测这一步。

    选项 × 真实代价 × 工作量

    选项 做什么 客户可感知的后果 工作量
    A spec 加第三条声明式能力拒绝 在 defineStack 中,声明了 webhook 却缺 webhooks 或 realtime 的 stack 被拒,新错误码如 STACK_WEBHOOK_CAPABILITY_REQUIRED validate、build、serve/dev、应用自己的 bootStack 测试、cloud 运行时都用同一句话拒绝 domain:spec,1 个 PR(S–M),另加 dogfood 的 webhook 物化夹具修正与 6 处示例复查;先例 #20332,行数未测
    B CLI 预检加规则 在 capability-preflight.ts 加规则,validate、build、serve 读取 只有 CLI 的门会拒;应用自己的测试和 cloud 仍静默放过 domain:cli,1 个 PR(S);无先例
    C 插件声明硬依赖 realtime plugin-webhooks 把 realtime 列入依赖 只在启动时报「Missing dependency」,validate 看不见;还会误拒没有声明 webhook、只用 webhooks 的宿主 已被实测否决
    D 只保留 PR #24946 的启动报错 不再改协议 启动日志里有一条 error;validate、测试、激活接口仍说「好」 0

    激活接口那一半(在 A 或 B 之外的附加项):

    业务含义直译:

    • A:在入口处查验件,所有门口共用一张清单。
    • B:只在前门查,后门和侧门照样放行。
    • C:保安把没有包裹的人也拦下了。
    • D:只在后台记一笔,前台照常营业。
    • A′:前台不再回答「已开通」。

    四轴(业务立场)

    • 实际业务需求:
      • 每个照 automation/webhooks.mdx 声明 webhook 的 scaffold 应用都会撞上(scaffold 的 requires 不含这两个 token);
      • 只有 examples/app-showcase 的 requires 写对了组合;
      • 18.0.0 的发布包上已实测复现。
    • 项目长远合理性: A 把这条规则放进已有的声明式能力类,一处定义、各门共读;B 为同一类规则开第二个家。
    • 防 AI 犯错: 出错时,A 让 AI 写的应用在第一次 defineStack 就看到响亮拒绝,测试里也一样;B 只在跑 CLI 时拒;D 让 AI 在日志之外看不到任何信号。
    • 创业阶段不扩散: A 新增一个错误码,不新增门禁;对 defineStack 的收窄只拒掉一种今天就不工作的配置。

    推荐:A,不做 A′

    • 终态句: 两年后,「声明了 X 就必须具备投递 X 的能力」是 defineStack 的一类统一规则,trigger、hierarchy、webhook 同在一处,所有加载门共用一个词表。参照:Kubernetes 的准入校验在 API 服务器一处执行,kubectl 与控制器都无法绕过。
    • 置信缺口:
      • cloud 运行时对 webhooks 的解析未测;
      • dogfood 的 webhook 物化夹具故意不带 realtime 启动,需要随 A 一起改。
    • 其余选项不取的理由:
      • B:第二个家,应用测试与 cloud 仍静默。
      • C:误拒合法宿主。
      • D:信号只在日志里。
      • A′:选 A 后几乎不可达,却要付出一次 BREAKING。
    • 自检: 只看①选 A;②③④ 是否翻转:否。

    裁后执行(选 A 时)

    1. 席位为 domain:spec 立一张执行卡,指明 stack.zod.ts 的第三条声明式能力拒绝、错误码登记(error-code-ledger.zod.ts)、cloud 解析的先测项、dogfood 夹具修正,以及 Clause-②: no (narrowing) 与契约级复核。
    2. 本卡在 PR fix(plugin-webhooks,runtime): a declared webhook the deployment can never deliver is reported at error and refused at the activation door #24946 落地后保持 needs-user-decision;裁定 A 后转 pm:blocked,Blocked-by: 指向那张 spec 卡,spec 卡落地时关闭本卡。
    3. 范围外发现:同一组合下,组织作者写的 sys_webhook 行同样永不投递,没有声明 webhook 时仍只是 warn;只读过代码,未经公开门实测。随 A 一并记入 spec 卡的「Unchanged」说明,不另立卡。

    相关单与 PR

    os-decision-facets

    • ① 项目长远合理性:A 收缩特例——webhook 并入既有声明式能力类,一处规则、各门共读;B 会为同一类规则开第二个家。
    • ② 实际业务拉动:每个照文档声明 webhook 的 scaffold 应用今天都撞上;18.0.0 发布包实测复现。
    • ③ 防 AI 犯错:在 defineStack 响亮拒绝,优于启动日志里的一条 error、更优于静默的 enabled: true。
    • ④ 创业阶段不扩散:新增一个错误码、零门禁;收窄只拒掉今天就不工作的配置;A′ 的 BREAKING 不值得付。

    Prior rulings read: validateTriggerCapability · STACK_TRIGGER_CAPABILITY_REQUIRED · REST door narrowing BREAKING → 2 precedents (#20332 in stack.zod.ts:3258, .changeset/24813-…); ADR none named; thread: none.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #379 item 4 · letter A · maintainer 「同意」 2026-10-20T23:05Z

    Director seat, summon #45 continuation 2, session_01Lg6GQ4xjqmvEzYv78d6H7i. Presented in this session's chat as batch #379 item 4, from the decision 6298521209 (domain:services seat 1), the dev reports 6296915123 / 6297443354 and the landing record 6298509196 (PR #24946, 19e5338cc8, part (c)); the director's chat delta re-checked on origin/main e4d604efd39f the declared-capability class (stack.zod.ts:4510 validateTriggerCapability, :4432 validateHierarchyScopeCapability, :3263 STACK_TRIGGER_CAPABILITY_REQUIRED) and the enqueuer's single event source (webhook-outbox-plugin.ts:426), and put the timing of A under the standing ruling 「甲」 (#6015 6292005854), which the card's decision did not cite. The maintainer's 「同意」 takes letter A with the director's timing: A′ not taken, no token implied, the refusal in 19.0. Thread read through 6298521209 at this write; nothing newer.

    Governing text: AGENTS.md "Degradation log levels" (what claims to land and does not ⇒ error); packages/spec/src/stack.zod.ts:3258 (the declared-capability class, #20332); 「甲」 rule 2 (a narrowing that no ADR-0087 conversion carries ships its loud half in 18.x and its refusal in 19.0).

    What the ruling binds

    1. A — the rule lives in defineStack, as the third declared-capability refusal beside trigger and hierarchy: a stack that declares a webhook while requires lacks 'webhooks' or 'realtime' is refused with one code, STACK_WEBHOOK_CAPABILITY_REQUIRED, by every door that loads a stack. B (a CLI-only preflight rule), C (a plugin hard dependency) and D (the boot error alone) are not taken.
    2. Timing under 「甲」. No ADR-0087 conversion carries it: auto-adding realtime to requires changes the deployment's composition, not a spelling, and the maintainer did not take it. So 18.x ships the loud half (PR fix(plugin-webhooks,runtime): a declared webhook the deployment can never deliver is reported at error and refused at the activation door #24946's boot error, already landed, plus a warning at os validate / os build naming the webhook, the missing tokens and the 19.0 refusal), and the refusal waits for 19.0.
    3. A′ is not taken: the activation door keeps answering as today (no REST narrowing, no BREAKING for a shape that A makes unreachable).
    4. Out of scope, recorded on the 19.0 card, not filed: an authored sys_webhook row on a webhooks-without-realtime composition is equally undelivered (code-read only).

    Workload of the chosen letter (the card's column): domain:spec, one PR (S–M) plus the dogfood webhook fixture correction and the example apps' requires re-check; precedent #20332. Split by 「甲」 into the 18.x warning (#24995) and the 19.0 refusal (#24997).

    Execution: #24995 (domain:spec p1 pm:queue, the 18.x warning) and #24997 (domain:spec p1 pm:on-hold, Restart-when: the 19.0 line opens), both filed by the director in this act. State: needs-user-decision → pm:blocked, Blocked-by: #24995; this card closes when #24995 lands (the 19.0 refusal has its own card). The cross-lane declaration on #22648 (6295927555) for packages/cli is released: A moves the rule into spec, so the CLI preflight is not edited.

  9. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #379 item 4 · letter A · amended · maintainer 「暂时不考虑 v19,协议的调整原则上也是尽快,不考虑退役版本。」 2026-10-20T23:11Z

    Director seat, summon #45 continuation 2, session_01Lg6GQ4xjqmvEzYv78d6H7i. Amends the timing clause of 6303779439 (this card, batch #379 item 4). After that record the maintainer wrote, in this session's chat: 「暂时不考虑 v19,协议的调整原则上也是尽快,不考虑退役版本。」 (v19 is not considered for now; protocol adjustments are, as a rule, made as soon as possible; no retirement version is planned around). Letter A stands; its timing changes. Thread read through 6303779439 at this write; nothing newer.

    Governing text: the maintainer's sentence above, which supersedes rule 2 of 「甲」 (#6015 6292005854) for the refusal half: a narrowing that no conversion carries no longer waits for 19.0.

    What the amendment binds

    1. The defineStack refusal (STACK_WEBHOOK_CAPABILITY_REQUIRED) ships now, in 18.x, as one PR with a BREAKING changeset in the packages: POST /packages/:id/adopt-orphans skips requireWritablePackage, so it can bind loose items into a read-only package #24813 shape (BREAKING banner, adr-0087: not-required), contract review at CONTRACT_REVIEW_TIER. No separate warning step: the refusal is the loud half.
    2. spec: refuse a declared webhook whose requires cannot dispatch it — STACK_WEBHOOK_CAPABILITY_REQUIRED in defineStack, the third declared-capability refusal (ruling A on #24910; ships in 18.x by the maintainer's re-timing) #24997 becomes the 18.x execution card (pm:on-hold → pm:queue, its Restart-when: line removed and its body re-timed in this act). spec: the loud half for a declared webhook whose requires cannot dispatch it — os validate / os build warn, naming the missing token and the 19.0 refusal (ruling A on #24910 under 「甲」 rule 2; 18.x) #24995 (the 18.x warning) is closed as superseded; nothing to build there.
    3. Everything else in 6303779439 stands: A′ not taken, no token implied, cloud's resolution measured first, the dogfood fixture converted or declared, the example apps' requires re-checked, the cli cross-lane declaration on [PM seat] domain:cli · seat 2 — 🟢 os-justin · session_01LmjU2x1faLZ39DVTFyVca6 #22648 released.

    State: needs-user-decision → pm:blocked, Blocked-by: #24997; this card closes when #24997 lands.

  10. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    Close: completed. Ruling A landed through #24997

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-21T10:07Z. ⛔ Not a claim.

    Ruling A (batch #379 item 4, amended for 18.x) states that this card closes when #24997 lands. #24997 closed completed at 2026-10-21T07:40Z.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:servicespriority:p1High: required for production / M2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions