Skip to content

feat(spec)!: defineStack refuses a declared webhook whose requires lacks webhooks or realtime (STACK_WEBHOOK_CAPABILITY_REQUIRED) - #25057

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-24997-webhook-capability-required
Oct 21, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-24997-webhook-capability-required

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #24997

Refs #24910

Clause-②: no (narrowing: a stack declaring a webhook without the capability tokens is refused; ships now under the amended 「甲」, #6015 6303865025)

What the ruling binds

Ruling A on #24910 (record 6303779439, amended by 6303859748: 「暂时不考虑 v19,协议的调整原则上也是尽快,不考虑退役版本。」), as the card states it:

What changed

  • packages/spec/src/stack.zod.ts: validateWebhookCapability, the third declared-capability refusal, follows the validateTriggerCapability precedent exactly. It runs right after the trigger check and throws StackWebhookCapabilityRequiredError (code: 'STACK_WEBHOOK_CAPABILITY_REQUIRED', status: 422, the StackRefusalError envelope) with the header defineStack webhook capability validation failed (N issues): and one issues entry per declared webhook. Each entry names the webhook, the missing token or tokens, and a prescription that is the whole fix for the requires it was given:
    • realtime present, webhooks missing: add 'webhooks';
    • webhooks present, realtime missing: add 'realtime';
    • neither (an absent requires included): add both, ['webhooks', 'realtime'].
  • Every declared webhook counts, isActive: false included. Unlike a retired flow, an inactive declaration is one activation-door write away from being switched on (the ledger row overrides the declared isActive, ADR-0131 D6), and the switch would then deliver nothing. Only the top-level webhooks: collection is read, the one the declared-webhook reader dispatches. Nothing is implied or added; strict: false skips the judgement by contract, as for every sibling.
  • packages/spec/src/api/error-code-ledger.zod.ts: the new code is registered under @objectstack/spec beside the other STACK_* rows, with door: 'none' on their reading. The generated content/docs/references/api/error-code-ledger.mdx and contract.mdx are regenerated (check:generated --fix, which proved only those stale).
  • content/docs/automation/webhooks.mdx §3.0 now says that such a stack is refused, names 'webhooks' and 'realtime', says that every door stops with the same sentence, and says that the boot error remains for a host that wires a stack by hand.
  • Changeset .changeset/24997-webhook-capability-required.md: @objectstack/spec minor, BREAKING banner, Clause-②: no (narrowing), the adr-0087: not-required (no-migration-prescription) marker. Same shape as the 24813 changeset.

Producers measured and fixed

producer (tree 3b3830ca22) declares a webhook requires action
examples/app-showcase yes (allWebhooks) carries webhooks and realtime none; still loads
examples/app-crm, app-todo, app-multi-package, embed-objectql no (app-crm names webhooks in a comment only) n/a none
create-objectstack blank template no ['automation', 'triggers'] none; refused once a webhook is added, which is the point
packages/qa/dogfood/test/fixtures/webhook-materialization-fixture.ts yes absent declared, see below
packages/spec/src/stack-duplicate-entry.test.ts webhook row yes absent row context now carries requires: ['webhooks', 'realtime']
packages/qa/dogfood/test/webhook-meta-door-credential-redaction.dogfood.test.ts (landed on main after the branch point) yes absent declared via strict: false, because its "without plugin-webhooks" composition is what the redactor pins need. Same choice as the materialization fixture. Commit 779ed238e5

Search used to find producers: every file that calls defineStack / bootStack / composeStacks and names webhooks, plus defineWebhook and webhooks: [ repo-wide. The plugin-webhooks tests build declarations through DeclaredWebhookSource, never through defineStack, so they are unaffected.

The dogfood fixture is declared, not converted. It is now built with defineStack(..., { strict: false }), and a docblock says why. The proof exists to show that the declared-webhook reader binds on the data engine alone, with NO realtime service. If the fixture declared the pair, bootStack would mount realtime the way serve does, and a reader re-gated behind the dispatch prerequisites would then pass unseen. So the fixture models the one composition defineStack cannot reach: a hand-wired host. That is the population the boot error of #24946 serves. The precedent for a dogfood fixture built strict: false on purpose is flow-node-config-values-at-registration.dogfood.test.ts. Its second consumer, wall-declared-webhook-deployment-wide.dogfood.test.ts, hand-wires realtime and is unaffected. Both suites pass.

Cloud's resolveCapabilityDependencies: NOT MEASURED. Reason: the cloud repository is not in this container. The refusal sits in defineStack, so the cloud runtime refuses with the same sentence on any stack it loads through the producer. Whether its resolver implies realtime from webhooks changes nothing here, because no token is implied by ruling.

The doors, measured (scaffold-shaped project, requires: ['automation', 'triggers'] plus one declared webhook; CLI from source, bin/run-dev.js)

requires os validate os build --json os dev --no-watch --fresh
['automation', 'triggers'] exit 1, --json "code": "STACK_WEBHOOK_CAPABILITY_REQUIRED", finding: webhook 'blank_note_changed' is declared but requires does not include 'webhooks' or 'realtime' exit 1, "success": false, "code": "STACK_WEBHOOK_CAPABILITY_REQUIRED" exit 1 at Loading objectstack.config.ts..., same header and finding
[..., 'webhooks'] exit 1, finding names 'realtime' not run not run
[..., 'realtime'] exit 1, finding names 'webhooks' not run not run
['automation', 'triggers', 'webhooks', 'realtime'] (CONTROL) exit 0 exit 0, "success": true Server is ready, /api/v1/health 200, WebhookOutboxPlugin and RealtimeServicePlugin in the plugin list; stopped by its recorded PID

An app's bootStack tests hit the same refusal at import, because the throw is in defineStack itself.

Tests (pinned arms, control, ablation)

  • New packages/spec/src/stack-webhook-capability.test.ts. It pins: the three arms (no webhooks, no realtime, both missing; for both missing, requires of ['automation', 'triggers'], [] and absent), each with code, status: 422, the header, one issues entry and the finding text naming the webhook and the token(s); the CONTROL (the scaffold tokens plus the pair, the pair in either order, among other tokens); each prescription applied once is accepted; no token is implied (an accepted stack keeps the requires it declared); a stack with no webhook owes nothing; an inactive declaration still owes the pair; every webhook reported, one entry each; the map form; the trigger sibling reports first; strict: false skips.
  • stack-refusal-envelopes.test.ts: a webhook row (refused + accepted control); the census is now nine sites and nine distinct codes. stack-conversions-record.test.ts: a webhook row; its census is now nine codes.
  • Ablation, red: node scripts/ablation-replace.mjs replaced the call const webhookErrors = validateWebhookCapability(data); with an empty list. The tool's own on-disk evidence: anchor 1 to 0, blob 3d8624dc4fa1 to 5898d7c8a60e. With the call gone, the three spec files gave 18 failed / 76 passed. Restore: blob equals HEAD 3d8624dc4fa1 and git diff HEAD is empty. No dist rebuild was needed, because these tests import ./stack.zod from source.

Verification

Run at 5107a88d13 (before the origin/main merge) unless stated otherwise:

  • pnpm --filter @objectstack/spec test: 703/704 files passed on the first run. The one failure was the conversions-record census (8 to 9), fixed; the four affected files then gave 232/232. test:repo: 72 files / 1236 tests passed. typecheck: passed (check:test-typecheck: OK). check:generated: all 14 artifacts up to date after --fix regenerated the two reference pages.
  • @objectstack/dogfood: webhook-materialization.dogfood.test.ts and wall-declared-webhook-deployment-wide.dogfood.test.ts, 9/9 passed.
  • @objectstack/cli --project unit: 313/316 files passed. The 3 failures (published-subpath-console.pin, published-subpath-hook-body.pin, scaffold-emission-typechecks) read packed .d.ts, and an OS_SKIP_DTS closure build had removed them (TS7016 "Could not find a declaration file"). After a full declaration build of @objectstack/cli..., the three files gave 42/42. Integration layer: declared to CI (this diff touches no cli file).
  • Lint: eslint --no-inline-config --format json over the 7 touched .ts files gave 7 files, 0 errors, 0 warnings. Population: all 7 resolve a config (--print-config). Invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project), so this diff cannot move the verdict on any untouched file. Repo-wide pnpm lint belongs to CI.
  • node scripts/pm/dispatch-gates.mjs --commands: 116 families derived at 5107a88d13, all 116 run, all exit 0. Seven of them first answered PREREQUISITE NOT MET because of the same skip-DTS build, and gave exit 0 on re-run after the declaration build: check:api-surface, check:dual-source-exports, check:entry-nameability, check:exported-any, check:skill-examples, check:dts-closure, check:dual-build-cjs-loads.
  • After merging origin/main (14 commits, file-disjoint from this diff): spec build + check:generated + spec test were re-run on the merge head; see the report comment for that reading.

Acceptance notes

  • Text-mode os validate and os dev print the refusal's message, header and finding, but not the code. That is how every STACK_* refusal surfaces today, the trigger sibling included; the code is on --json (os validate --json, os build --json). Carrier: none. The CLI's text renderer is not this card's surface.
  • skills/objectstack-automation/SKILL.md, the capability table, still reads webhooks, absent, "nothing delivered outbound", and does not name realtime. Since this change, a stack in that state is refused with the fix named, so this is no silent trap. skills/** is governed (Tier H) and outside the claim's file surface, so it is not edited here. Carrier: none.
  • Contract review at CONTRACT_REVIEW_TIER is owed on this PR (the claim and the ruling both say so).

Generated by Claude Code

…nnot dispatch it (STACK_WEBHOOK_CAPABILITY_REQUIRED)

Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
Co-authored-by: Claude <noreply@anthropic.com>
…d fixture's hand-wired composition; docs and changeset

Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 21, 2026
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 5 documentable anchor(s).

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/api/error-catalog.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/api/error-handling-server.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/automation/webhooks.mdx (via STACK_WEBHOOK_CAPABILITY_REQUIRED (literal, a string literal in ERROR_CODE_LEDGER; a string literal in StackWebhookCapabilityRequiredError))
  • content/docs/kernel/contracts/data-engine.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/releases/v17/17-1.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/releases/v17/17-4.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4bf079c6935b34b68da6d52acc67cf64cb4a55ac → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 86335e0c4488ac16a45415bdd621b16da692c7fa — the merge of head 779ed238e57bd2a678b3c35cbda06cb0690be707 into base 4bf079c6935b34b68da6d52acc67cf64cb4a55ac, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 86335e0c4488ac16a45415bdd621b16da692c7fa && git checkout 86335e0c4488ac16a45415bdd621b16da692c7fa
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4bf079c6935b34b68da6d52acc67cf64cb4a55ac 779ed238e57bd2a678b3c35cbda06cb0690be707 && git checkout -B drift-repro 4bf079c6935b34b68da6d52acc67cf64cb4a55ac && git merge --no-ff 779ed238e57bd2a678b3c35cbda06cb0690be707

node scripts/docs-audit/affected-docs.mjs --json 4bf079c6935b34b68da6d52acc67cf64cb4a55ac

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 4bf079c6935b34b68da6d52acc67cf64cb4a55ac → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…the FlowInnerRefusal code set now lists STACK_WEBHOOK_CAPABILITY_REQUIRED

Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
Co-authored-by: Claude <noreply@anthropic.com>
…composition (strict: false) — it declares a webhook without requires on purpose

The without-plugin-webhooks composition is what the redactor pins need; declaring
requires: ['webhooks', 'realtime'] would have bootStack mount the plugin and
remove that composition. Same disposition as webhook-materialization-fixture.

Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 779ed238e57bd2a678b3c35cbda06cb0690be707
Local-runs: none
Scope: packages/spec/src/stack.zod.ts, packages/spec/src/api/error-code-ledger.zod.ts, packages/spec/src/stack-*.test.ts, packages/qa/dogfood/test/, content/docs/ + .changeset/24997-webhook-capability-required.md

① Derived judgments

  • The refusal sits at the single producer every door loads through (defineStack → buildDefinedStack), in the post-parse chain right after the trigger sibling. So os validate, os build, os serve / os dev and an app's bootStack all refuse with one sentence.
  • The spec has exactly one webhook surface: top-level webhooks (stack.zod.ts:860). Its map form is normalised before the judgement (stack.zod.ts:4993-5000).
  • composeStacks only concatenates requires and webhooks (stack.zod.ts:1422, :1432) and accepts only defineStack-provenanced inputs, so composition cannot remove a token.
  • No capability implication exists on this tree: realtime and webhooks map to separate packages (core/src/capability-providers.ts:144, :222). A misspelled token is refused first as STACK_CAPABILITY_UNKNOWN (stack.zod.ts:5038-5044). The check is therefore complete, with no false positives.
  • strict: false skips every post-parse judgement uniformly (stack.zod.ts:5004-5014), the same as the other STACK_* refusals.
  • Producers:
    • examples/app-showcase already carries both tokens;
    • the two dogfood fixtures are declared with strict: false, with docblocks, as the card permits;
    • no template, example, skill or doc snippet declares a webhook in a defineStack.
  • The tests are real: every arm fails with the call removed, and the census counts pin the ninth refusal.

② Semver level

@objectstack/spec minor with the BREAKING banner and Clause-②: no (narrowing): the accepted set of defineStack shrinks and no new authorable surface is added. ADR-0087 not-required (no-migration-prescription) is the only disposition consistent with the ruling on #24910, which forbids auto-adding tokens. STACK_WEBHOOK_CAPABILITY_REQUIRED is registered in the error-code ledger (error-code-ledger.zod.ts:1557-1563). Each of the three generated pages gains exactly one row, and the envelope is StackRefusalError (422).

③ Boundary flags

Implemented-by: claude/issue-24997-webhook-capability-required
Reviewed-by: session_01D6F9axfoPzR7t8G1J7Ytzz

VERDICT: PASS

Seat re-read of the check-runs at landing: all 49 runs on 779ed238e5 have completed: 41 succeeded, the rest were skipped, and none is red.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 21, 2026 07:01
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 21, 2026 07:01
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit baedcec Oct 21, 2026
50 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24997-webhook-capability-required branch October 21, 2026 07:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants