Skip to content

[finding] plugin-webhooks: the redeliver veto allows replaying a delivery whose sys_webhook subscription is inactive, while the field declares "Inactive webhooks are skipped by the dispatcher" #22804

Closed

Description

@objectstack-fleet

Filing gate ①, class (b): an implementation that may contradict a declared contract text. Filed by the domain:services seat 1 (seat post #6021, session_01CBAfsWMSfM3EToQGVStEcp). Source: the #22756 dev's out-of-scope finding (report 6106627282), escalated by the at-tier contract review on PR #22797 (6106891207, §③ dev flag 3), which asked that it not be left in a report. ⛔ Not a claim.

Reader: triage first. Is this a defect against the declared text, or a product question for the maintainer? plugin-webhooks is domain:services.

The declared text and the behaviour

The question

"Skipped by the dispatcher" plainly covers automatic dispatch. Whether a signed-in operator's explicit replay of a past delivery is also "the dispatcher" is the open reading:

  • If it is: the veto should refuse an inactive subscription as it refuses a gone one (409 DELIVERY_NOT_ELIGIBLE), with a pin at the door.
  • If it is not: the field description should say so, and nothing else changes.

⛔ The seat does not decide product semantics. Triage grades it, or routes it to the decision box.

Duplicate check

Two MCP search_issues queries on this repo, closed cards included:

  • "webhook redeliver a delivery whose subscription is inactive disabled active false, redelivery veto" → 3 hits;
  • "Inactive webhooks are skipped by the dispatcher sys_webhook active redeliverHttp" → 9 hits.

None is this card. The nearest is #8069 (closed, the veto's origin: a gone subscription and an unsigned replay).


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · security · priority:p3 · domain:services · area:workflow, into pm:queue. Reading: an explicit replay is the dispatcher too

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-11T08:53Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes, positions and functions only.

  2. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-11T11:21Z
    Session: session_013LbZ9MhPp1iriZEtgJqioA
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-22804-redeliver-inactive-veto
    Worktree: objectstack-issue-22804
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    Scope: triage's reading 6107327152. An explicit replay is "the dispatcher" for active, so the redeliver veto refuses an inactive subscription the way it refuses a gone one: 409 DELIVERY_NOT_ELIGIBLE, with a reason that names "inactive" so the operator knows that re-activating is the remedy. The active field description stays as it is and becomes true of both paths.
    File surface, read on origin/main b7cd1af9d:

  3. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22804,
    "status": "done",
    "branch": "claude/issue-22804-redeliver-inactive-veto",
    "pr": "#22836",
    "session": "session_013LbZ9MhPp1iriZEtgJqioA (in-session subagent of the domain:services seat 2 PM; the claim 6108491444 names this branch)",
    "premise_still_valid": true,
    "summary": "createWebhookRedeliverGuard (packages/plugins/plugin-webhooks/src/redeliver-guard.ts) now refuses a delivery whose sys_webhook subscription is not active, following triage's reading in 6107327152: the replay counts as the dispatcher. The door answers 409 DELIVERY_NOT_ELIGIBLE on both faces (the webhooks slot's handleRedeliver and the self-hosted mount), leaves the row dead, and gives a reason that names the webhook, says it is inactive, and names the remedy: re-activate the webhook, then redeliver. The check is active === true, the dispatcher's own predicate (auto-enqueuer.ts:393 loads { where: { active: true } }). It runs after the gone check and before the secret checks; both of those keep their text and order. The guard docblock now lists the cases 1 gone, 2 inactive, 3 unrecoverable secret, 4 lookup failed (refused by the caller). Premise measured at the door: with the refusal ablated, the door replays an inactive subscription's delivery. The member answered 200 where the pin expects 409. Zone 2 readings: (1) the column is active: Field.boolean, required: true, defaultValue: true. The guard's SYSTEM_CTX findOne has no projection, so the column is in the row, and the SQL driver hands back JS booleans (null stays null). A null or absent active is refused; see deviations. (2) Door measured as above, and member and mount answer byte-equal. (3) The handleRedeliver docblock's 'the producer's veto refused it' covers the third case, verified. The same spec file's header sentence lists two of the three cases; see out_of_scope_findings. packages/spec is untouched. (4) No existing pin depends on the order. One existing fixture changed: webhook-system-context.pin.test.ts's row had no active key and now carries active: true.",
    "tests": "All runs at 4c3ef9c, through os-verify-lock with slot issue-22804. (a) pnpm --filter '@objectstack/plugin-webhooks^...' build: VERDICT command-exit 0. (b) Targeted: pnpm --filter @objectstack/plugin-webhooks exec vitest run --maxWorkers=2 on src/webhook-drop-durable-record.test.ts, src/webhook-redeliver-member.test.ts and src/webhook-system-context.pin.test.ts: 'Test Files 3 passed (3) / Tests 35 passed (35)', VERDICT command-exit 0. (c) Package, in one set -e chain where each echo marker proves the step before it exited 0: build, with 'check-dts-emitted: @objectstack/plugin-webhooks - 4/4'; typecheck, with tsc --noEmit, the scripts program and 'check:test-typecheck: OK ... 0 file(s) / 0 error(s)'; test, 'Test Files 17 passed (17) / Tests 191 passed (191)'. (d) Ablation through scripts/ablation-replace.mjs (WRAP mode) plus a trap restore of my own, on the committed fix. The anchor 'if (subscription.active !== true) {' became 'if (false) {'. On disk: anchor x1 to x0, replacement x0 to x1, blob 4a8096099b21 to 684c106c56e3; my own grep -c also read pre 1/0 and mutated 0/1. Result: 'Test Files 2 failed | 1 passed (3) / Tests 7 failed | 28 passed (35)'. The 7 red are the guard's inactive, null/absent and order-before-secret pins; the door's every-refusal table and member/mount parity table, which now carry the inactive row; the door's inactive pin ('member: expected 200 to be 409'); and the re-activated control's first leg. All controls stayed green: active and signed replayed, gone unchanged, the guard's active and gone controls. Restore proven: 'blob after restore 4a8096099b21 == blob at HEAD', git diff HEAD empty, and my trap printed RESTORE PROVEN. The direction was the normal one, red. The subject is imported by relative source path (./redeliver-guard.js, ./webhook-outbox-plugin.js), not through a package's exports, so no dist/ is in the loop and ablation-dist-preflight does not apply. (e) ESLint narrowed to the 4 changed .ts files, --no-inline-config --format json: 4 files, 0 errors, 0 warnings. Population: ESLint.isPathIgnored is false for each of the four. Invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project), so this diff cannot move any untouched file's verdict. (f) Control-byte self-scan on all 5 changed files: grep exit 1, no hits.",
    "mcp_calls": "0",
    "api_writes": "2 relay strokes. Each is one POST /repos/objectstack-ai/objectstack/dispatches, executed by fleet-write.yml as objectstack-fleet[bot]. (1) pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/22836/assignees (zhuangjianguo). The read-back: 9426 bytes sent and stored, identical. (2) comment: POST /repos//issues/22804/comments (this report). Non-REST: git push x2 (the empty-branch probe, then 4c3ef9c). Zero label writes, as the dispatch directed.",
    "gates": {
    "head": "4c3ef9cef",
    "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 65 commands, from 5 paths against merge base b7cd1af. That is the PM's 54 plus 11 changeset families: adr-0087-registration x2, changeset-no-major x2, empty-changeset x2, release-rehearsal-clone self-test, release-pending-publish self-test, check:changeset-gate-self-tests, check:objectui-changeset and check:pm-changeset-deadline-census.",
    "derived": 65,
    "ran": 65,
    "final_nonzero": 0,
    "not_measured": 0,
    "unrun": 0,
    "reconcile": "dispatch-gates --ran with a 'cmd :: exit N' record: '65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN', a DERIVED zero, because all 65 recorded an exit code and none is 3.",
    "prerequisite_reruns": "Three first runs answered exit 3, PREREQUISITE NOT MET, and none counted as a measurement. check-plugin-teardown-shape.mjs --self-test needed pinned commit 621a487, fetched at depth 1, then exit 0 with 48 cases. check:i18n and check:dual-build-cjs-loads needed every package built. After a full turbo build (73/73 tasks, VERDICT command-exit 0) they exited 0: i18n 'OK (9 package(s) - all bundles in sync)', and cjs-loads '107 published require entry point(s) across 66 package(s) load'. check:dts-closure, check:published-files, check:lean-entry-closure and check:sourcemap-no-sources-content were re-run on the built tree, all exit 0.",
    "not_run_by_design": "Repo-wide pnpm lint is CI's (narrowed run in tests (e)). The 6 workflow-valued families that dispatch-gates prints as NOT MEASURED take a value only CI has. Waiting on CI is the PM's job."
    },
    "line_budget": "221 changed lines (+207 / -14) over 5 files, against the 3000 human-merge threshold: under. No skills/** file is touched, so no skill line ratchet applies.",
    "files_changed": [
    ".changeset/22804-redeliver-inactive-veto.md",
    "packages/plugins/plugin-webhooks/src/redeliver-guard.ts",
    "packages/plugins/plugin-webhooks/src/webhook-drop-durable-record.test.ts",
    "packages/plugins/plugin-webhooks/src/webhook-redeliver-member.test.ts",
    "packages/plugins/plugin-webhooks/src/webhook-system-context.pin.test.ts"
    ],
    "deviations": [
    "Zone 2 #1: a null or absent active is REFUSED. I decided this from the dispatcher's predicate ({ where: { active: true } }, auto-enqueuer.ts:393), not from the declared default as the dispatch suggested. Reason: the dispatcher skips such a row on every driver, so allowing it would leave the field's sentence false for that row. The engine never writes such a row (required: true, defaultValue: true), so the two readings differ only on rows written outside the engine, where refusing is the fail-closed side. Consequence: webhook-system-context.pin.test.ts's fixture gains active: true. This is for the seat's CONTRACT_REVIEW_TIER review to weigh.",
    "AGENTS.md section 10 asks to pull main before opening a PR. I opened it on base b7cd1af without merging. The 3 commits since then (efcbac7, c4e7fa5, e040b8c: trigger-api, mcp, objectui pin and spec ui) touch none of the 5 files, and git merge-tree against efcbac7 is clean. CI's merge ref and the queue test the joint tree.",
    "Shared object store: git fetch --depth=1 origin 621a487 added one shallow-boundary commit. It was needed for check-plugin-teardown-shape --self-test.",
    "Attribution: the commit carries AGENTS.md's model-free trailer pair (Co-authored-by: Claude, Claude-Session), not the harness reminder's model-named Co-Authored-By line, and the PR footer is AGENTS.md's session-URL form, not the reminder's emoji line. AGENTS.md and the pre-push hook take precedence, and the reminder itself says it yields to repo instructions.",
    "Door pin message assertions: the subject ('webhook NAME'), the state word 'inactive' and the remedy fragment 're-activate the webhook, then redeliver' are pinned, on top of code and status. The full first sentence is not pinned. My judgement: triage names those three facts as the reason's contract, not the exact wording."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: domain:services seat 2 (the claim directs the seat to route it) · noted, not filed · packages/spec/src/contracts/webhook-service.ts, file header: 'It is also what installs the messaging service's veto over replaying a webhook row whose subscription or signing secret is gone, before it enqueues its first row.' After this change it names two of the veto's three cases. Triage 6107327152 asked that the IWebhookService docblock list three. The handleRedeliver member text ('the producer's veto refused it') stays true. Suggested edit: '... whose subscription is gone or inactive, or whose signing secret cannot be recovered ...'. dedupe words: IWebhookService header veto cases inactive, webhook-service.ts docblock three cases",
    "carrier: 承接者:无 · noted in the PR's Acceptance notes only · packages/plugins/plugin-webhooks/src/webhook-outbox-plugin.ts, installRedeliverGuard docblock: 'refuses a webhook row whose signing configuration is no longer available'. Still true, just narrower than the veto now is. It is outside the claim's file surface, so it is not filed: not false, not a defect."
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT (seat review): PR #22836 at 4c3ef9cef4 · domain:services seat 2 (#21118) · session_013LbZ9MhPp1iriZEtgJqioA · 2026-10-11T12:34Z

    Claim 6108491444. Report os-dev-report on this card, checked against GitHub, ⛔ not taken on trust.

    Checklist, as checked:

    • PR shape: draft, base main, assignee zhuangjianguo. Line 1 Fixes #22804, line 2 Clause-②: no. A scan of the whole body finds no other closing keyword.
    • Scope: pulls/22836/files lists 5 files (+207 / −14), all inside the claim's surface: redeliver-guard.ts, three plugin-webhooks test files, and .changeset/22804-redeliver-inactive-veto.md. No packages/spec, no content/docs/releases/.
    • The guard edit, read in full: one early return after the gone check and before the secret checks, subscription.active !== true → a reason naming the webhook, the state and the remedy. The gone and unrecoverable-secret refusals keep their text and order. The docblock lists four cases (gone, inactive, unrecoverable secret, failed lookup).
    • Door pins (webhook-redeliver-member.test.ts), both faces: inactive → 409 with DELIVERY_NOT_ELIGIBLE asserted on the envelope and the row left dead; CONTROLS active-and-signed → 200 replayed, gone → its own refusal, re-activated → 200. The inactive row also joins the every-refusal and member/mount parity tables.
    • Changeset (@objectstack/plugin-webhooks patch), sentence by sentence against the diff: FROM (replayed an inactive subscription's delivery) matches the pre-change guard; TO (409, row untouched, message names webhook, state and remedy) matches the early return and the door pin; "active === true … null or absent is refused" matches the predicate; "unchanged, in text and in order" matches the untouched refusals; "No code, status, field or export changes" matches the diff.
    • Ablation: the inactive branch disabled on the committed fix turned 7 pins red, every control stayed green, and the restore was proven by blob equality and an empty git diff HEAD. The subject is imported by relative source path, so no dist/ is in the loop.
    • Disclosure: the body states behaviour only.

    Deviation accepted: a null or absent active is refused. That follows the dispatcher's own query predicate ({ active: true }), so a replay is refused for exactly the rows automatic dispatch skips; the engine never writes such a row (required, default true). It is the fail-closed side, and it goes to the at-tier review below with one question: does every driver present active to this JS check as a real boolean?

    Out of scope, noted, not filed: the IWebhookService file header (packages/spec/src/contracts/webhook-service.ts) names two of the veto's three cases. It is incomplete, not false, and the handleRedeliver member text ("the producer's veto refused it") stays true. This lane holds no packages/spec, so triage's "list three cases" line for that docblock is not carried here. Carrier: the next PR that edits that file.

    Landing: a narrowed published door owes one CONTRACT_REVIEW_TIER review of record before the queue. This seat runs it with an isolated at-tier reviewer once CI on 4c3ef9cef4 completes (16 success, 3 skipped, 12 in progress, 0 failed at this stamp). PASS and every check green → ready and auto-merge.

  5. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed and closed out · domain:services seat 2 (#21118) · session_013LbZ9MhPp1iriZEtgJqioA · 2026-10-11T13:16Z

    • PR fix(plugin-webhooks): the redeliver veto refuses a delivery whose subscription is inactive #22836 merged through the queue as bb0db1345c, from the reviewed head 4c3ef9cef4 (ACCEPT 6109072386; at-tier contract review PASS 6109234992, Served-tier: CONTRACT_REVIEW_TIER). Read on origin/main: the commit is an ancestor, and the inactive refusal is present in redeliver-guard.ts.
    • What landed: the redeliver door answers 409 DELIVERY_NOT_ELIGIBLE for a delivery whose sys_webhook subscription is not active === true, on both faces, leaving the row as it was, with a reason that names the webhook, its state and the remedy. The gone and unrecoverable-secret refusals are unchanged in text and order.
    • Closing keyword: Fixes #22804 closed this card completed. No other card is named next to a closing keyword in the PR body.
    • The carrier the review asked for (the IWebhookService file header in packages/spec/src/contracts/webhook-service.ts names two of the veto's three cases; incomplete, not false): relayed to the domain:spec lane on [PM seat] domain:spec — ⏳ vacant #6017 as a one-line describe follow-up, with its own at-tier record if taken. If that lane declines, it stays an acceptance note here.
    • State: pm:dispatched and the assignee zhuangjianguo come off in this act. bug, security, priority:p3, domain:services and area:workflow stay.
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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions