Skip to content

spec: declare the forward target for POST /api/v1/webhooks/redeliver — an optional, transport-neutral service member a dispatcher domain can reach (webhooks segment 1 of #22564's stage 2) #22754

Closed

Description

@objectstack-fleet

Filing gate ③: a segment the maintainer's ruling directs. Filed by the triage seat (seat post #6015, session_01AavokzJ5DndAwitDXvKy4U), splitting stage 2 of #22564 (ruling 「A + 扫类」 on #22438, 6079645593 item 2) as #22438 was split (#22575–#22578). ⛔ Not a claim.

Why

  • Stage 1 (6093135788, accepted at 6093165613) measured plugin-webhooks' POST /api/v1/webhooks/redeliver. On a kernel with no http.server (the hosted shape) it answers 404 ROUTE_NOT_FOUND, because it mounts only on the raw app.
  • Neither producer owns a service slot today:
    • plugin-webhooks registers only webhook.autoEnqueuer;
    • redeliverHttp lives undeclared on the messaging service. It has 0 hits in packages/spec/src on origin/main d7b26df5.

Scope (the #22575 convention)

  • Declare one optional, transport-neutral member that redelivers a webhook delivery, on the service contract this segment chooses: a webhooks service contract, or the messaging service's.
  • Choosing the slot is this segment's contract call. If it cannot choose on the four axes, it returns needs_decision.
  • Clause-②: yes, since it is a new optional member.
  • ⛔ No runtime or plugin code here: segments 2 and 3 read this member's shape.

Done when

  • The member and its docblock are on main, with a type-level pin.
  • Segments 2 and 3 can name it.

domain:spec · p2.

Activity

  1. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 · 2026-10-11T03:16Z
    Session: session_016njDy8ozy9B9Ns5Y8kAWEK
    Account: marchtian (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-22754-webhooks-redeliver-member
    Worktree: objectstack-issue-22754
    Domain: domain:spec
    Seat: domain:spec#2
    File surface (at origin/main 149294c02c; stop on breach and explain in the report):


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22754,
    "status": "done",
    "branch": "claude/issue-22754-webhooks-redeliver-member",
    "pr": "#22767",
    "session": "session_016njDy8ozy9B9Ns5Y8kAWEK (mode:subagent: the dispatching PM session's id, as the harness Claude-Session line names it)",
    "premise_still_valid": true,
    "summary": "Declared the forward target for POST /api/v1/webhooks/redeliver: a new contract IWebhookService (packages/spec/src/contracts/webhook-service.ts) for the webhooks service slot, with one optional, transport-neutral member handleRedeliver?(request: Request), which returns a Promise of Response. It follows the #22575 shape, docblock structure, pin form and changeset level. It is exported from @objectstack/spec/contracts, has type-level pins, regenerates api-surface and export-origins (one line each), and ships a minor changeset with Clause-②: yes (widening). No runtime or plugin code. Slot call, made on the four axes, departs from the PM's suggested route (messaging). Reasons: (1) The measured premise splits. The messaging service owns the replay operation (redeliverHttp), but the bridged thing is the door, and plugin-webhooks owns it: it decides who may press it, takes the tenant from the active organization, maps refusals to statuses, and installs the webhook veto. (2) No messaging contract exists either (no IMessagingService, no messaging entry in ServiceSlotContracts), so both routes mint one contract. (3) A door on the messaging slot would exist on kernels without plugin-webhooks, where the veto is never installed. It would also put a second copy of the door's HTTP rules in packages/runtime instead of forwarding the way /auth and /approvals/act do. The ruling's bridge shape is a forwarding domain, which the webhooks slot keeps.",
    "slot_decision": "webhooks slot, new IWebhookService, member handleRedeliver. Axes: business need, the door exists exactly where self-hosted has it; long-term, one owner of the door's rules for both kernel shapes, runtime only forwards (ADR-0076 D11, the ruling's 'a dispatcher domain to the service slot'); AI-error, one copy of the rules, typed 501 on an empty slot, the [#10740] typed tenant stays at plugin-webhooks' one call site; startup, one contract with one optional member either way, the slot key is registered by the plugin segment (the window handleActionPage had), while the messaging route would need an extra presence probe that no existing registration answers (webhook.autoEnqueuer registers only when auto-enqueue starts).",
    "mechanism_assumptions": [
    "1 confirmed: plugin-webhooks registers only webhook.autoEnqueuer (webhook-outbox-plugin.ts:317); redeliverHttp 0 hits in packages/spec/src (git grep exit 1). Slot keys: webhooks (capability token; nothing registers it yet) and messaging (service-messaging registers messaging and notification, messaging-service-plugin.ts:213, :222).",
    "2 split: the operation is the messaging service's (MessagingService.redeliverHttp: reset to pending, tenant wall, parked-row refusal, per-source veto, dispatcher wake); the door is plugin-webhooks' (session auth, active-org tenant, JSON body, code-to-status mapping, installRedeliverGuard). There is no messaging contract to declare on.",
    "3 read: the handler takes session user.id and session.activeOrganizationId and a JSON body with deliveryId. It answers 401 UNAUTHENTICATED, 400 INVALID_REQUEST, 400 MISSING_REQUIRED_FIELD, 404 RESOURCE_NOT_FOUND, 409 DELIVERY_NOT_ELIGIBLE or DELIVERY_NEVER_SENT, 500 INTERNAL_ERROR, and 200 with the row's id and status. The docblock states that contract."
    ],
    "tests": "Final head 06119a9 (two merges of origin/main, e865300 then 7098aca; the second touched no packages/spec file). Build: pnpm --filter @objectstack/spec build at 7402777 and bbac332, VERDICT command-exit 0 both times; handleRedeliver has 1 hit each in dist/contracts/index.d.ts and .d.mts and 0 in emitted JS. After merge 2, core/cli/plugin-email/service-sms/verify rebuilt: 'Tasks: 59 successful, 59 total'. check:generated at 06119a9: 'All 14 generated artifacts are up to date' (before regeneration: exactly api-surface, '0 breaking (removed/narrowed), 1 added', and export-origins stale). Typecheck at 06119a9: pnpm --filter @objectstack/spec typecheck, VERDICT command-exit 0, check:test-typecheck OK; tsc -p tsconfig.test.json --listFiles lists webhook-service.ts and webhook-service.test.ts, with 0 diagnostics in either. Tests at 06119a9: vitest run --maxWorkers=2 on webhook-service, approval-service and core-service-contracts tests, 'Test Files 3 passed (3)', 'Tests 19 passed (19)'. Full spec suite at bbac332 (vitest run --project local --maxWorkers=2): 'Test Files 644 passed (644)', 'Tests 19213 passed | 1 todo', VERDICT command-exit 0. Reverse verification from committed 7402777 via scripts/ablation-replace.mjs (wrap mode plus an outer trap): handleRedeliver?( made required; on disk the anchor went 1 to 0 and the replacement 0 to 1, blob 3ed2ead393de to 28539f95c0f1. The subject is imported relatively, so src is read and no dist rebuild applies. Expected red, observed red: webhook-service.test.ts(30,42) TS2344 (RedeliverIsOptional) and (46,15) TS2741 (value pin). Restore: blob back to 3ed2ead393de, equal to HEAD; git diff HEAD empty.",
    "gates": "At 06119a9: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; merge base 7098aca; 6 paths; 164 changed lines) derived 87, and all 87 exited 0. These include check:api-surface ('public API surface + factory signatures unchanged'), check:export-origins, check:generated, check:docs, check:authorable-surface, check:exported-any, check:spec-docblock-symbol-anchors, check:adr-0087-registration ('this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)'), check:changeset-no-major, check:empty-changeset, check:doc-authoring, check:issue-citations (5 of 5 resolve), check:nul-bytes, check:dual-build-cjs-loads and check:lean-entry-closure. --ran on an exit-annotated record: '87 derived famil(ies) accounted for — 87 run, 0 NOT-MEASURED'. On the first pass at bbac332, check:doc-formula-expressions and check:lean-entry-closure exited 3 (PREREQUISITE NOT MET, unbuilt formula/lint/objectql: NOT MEASURED, not a finding). After building those, they exited 0, and both exited 0 again at the final head. Not run, CI's: the artifact-roster block (50), the wide-population families, the CI type-check lanes, the path-scheduled jobs, and the three PR-context guards. ADR-0087: minor with the widening arm is non-breaking, so no disposition marker is owed and none is written. CI: in_progress, not awaited.",
    "line_budget": "164 changed lines (+164 / -0) over 6 files, under the 3000 human-merge threshold. No skills/** or governed path.",
    "files_changed": [
    ".changeset/22754-spec-webhooks-redeliver-member.md",
    "packages/spec/api-surface/contracts.json",
    "packages/spec/export-origins/contracts.json",
    "packages/spec/src/contracts/index.ts",
    "packages/spec/src/contracts/webhook-service.test.ts",
    "packages/spec/src/contracts/webhook-service.ts"
    ],
    "deviations": [
    "Slot: the PM's suggested route (Section 3, optional) was the messaging service's contract. This PR picks a new webhooks contract instead, for the reasons in summary and slot_decision. The card's Section 1 lets this segment make the slot call on the four axes; the PM's mechanism assumption 2 measured as split, not as stated.",
    "ServiceSlotContracts gains no webhooks entry: nothing registers the slot yet, and the ledger admits only evidenced bindings. The approvals precedent has none either. The entry belongs to the plugin segment or to the first segment that types the lookup.",
    "Two stray files were written outside the scratchpad by a shell-variable scoping slip. /ablate-tsc.log was moved into the scratchpad. /suite.pid (6 bytes, a pid number) is still at the filesystem root: rm was refused by a built-in safety check, and I did not work around it. It is harmless, but the user may delete it.",
    "The harness asked for a model-named Co-Authored-By trailer and an emoji PR footer. Per AGENTS.md, commits carry the model-free pair (Claude-Session + Co-authored-by: Claude), and the PR body ends with the session-URL footer form.",
    "Labels: zero writes. The dispatch's budget named no label, and the diff has a changeset, so skip-changeset does not apply."
    ],
    "mcp_calls": "0. No GitHub MCP tools were used, and no other MCP calls were made.",
    "api_writes": "2 relay strokes (each one POST /repos/objectstack-ai/objectstack/dispatches, 204, executed by fleet-write.yml as objectstack-fleet[bot]) carrying 3 REST writes. Stroke 1, pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft, #22767), then POST /repos//issues/22767/assignees (marchtian); the read-back gave 14554 bytes sent and 14554 stored, identical. Stroke 2: POST /repos//issues/22754/comments (this report, via scripts/pm/post-stamped.mjs). There were also 5 git pushes, which are not REST: the empty-branch probe, 2 commits and 2 merges. Reads: gh api GET of issues 22754, 22575 and 22767, PR 22608, and comments 6104945556, 6079645593, 6093135788 and 6093165613.",
    "cleanup": "After this comment is posted: rm -rf the worktree's node_modules, then git worktree remove ../objectstack-issue-22754 (no --force). Background processes I started (install, suite, gate runners, builds) have all exited; no dev server was started.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: #22564's plugin-webhooks segment, which edits registerAdminRoutes and bootAutoEnqueue · noted, not filed. The self-hosted redeliver route mounts whenever the http-server slot and the messaging service are present. The webhook redeliver veto (installRedeliverGuard) is installed only inside bootAutoEnqueue, which returns early when autoEnqueue is false or realtime is absent, so on those compositions the self-hosted door runs without the veto. This is read only: whether a webhook-sourced row can exist there was not measured, so there is no reach. The plugin segment's slot registration should sit beside the veto. Dedupe words: redeliver guard not installed; autoEnqueue false redeliver; webhook veto realtime absent; registerAdminRoutes guard ordering."
    ]
    }


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT — PR #22767 at 06119a91ff (Fixes #22754). Contract review PASS; ready and enqueued

    domain:spec seat 2 (#18549) · marchtian · session session_016njDy8ozy9B9Ns5Y8kAWEK · 2026-10-11T05:07Z · holder of claim 6104945556. Report: 6105492881.

    Checked against the diff and the record, not the report:

    • The slot call (this segment's own, per the card): a NEW IWebhookService contract for the webhooks slot, with one optional, transport-neutral member handleRedeliver?(request: Request) returning a Promise of Response. This is not the messaging service the seat's dispatch suggested, and the reasoning holds against the code:
      • the replay operation is MessagingService.redeliverHttp;
      • the DOOR belongs to plugin-webhooks: session auth, the active-org tenant, the status mapping, and the installRedeliverGuard veto;
      • no messaging contract exists either, so both routes mint one contract;
      • a messaging-slot member would exist on kernels where the veto is never installed.
    • Declared before registered is the ruling's sanctioned window: spec(approvals): declare the approvals service's optional, transport-neutral action-page member (segment 1 of ruling A on #22438) #22575 did the same with handleActionPage. No ServiceSlotContracts entry is added, which is right under the ledger's evidence rule. The precedent added none either.
    • The docblock's status contract (401 / 400 / 400 / 404 / 409 ×2 / 500 / 200) matches the handler at the head. /webhooks is not on the ADR-0069 allow-list.
    • Shape parity with spec(approvals): declare the approvals service's optional, transport-neutral action-page member (segment 1 of ruling A on #22438) #22575: the same signature form, docblock skeleton, pin form and changeset level.
    • Published surface: one interface export, type-only (no JS emitted). api-surface and export-origins are +1 each ("1 added, 0 breaking").
    • Semver: @objectstack/spec minor, Clause-②: yes (widening). No ADR-0087 marker is owed.
    • Contract review: at-tier PASS 6105684440 at 06119a91ffd262d5c2880b310a42c423c72f4c55.
    • CI on 06119a91ff: 35 runs: 32 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)), 0 failure. mergeable_state is clean.
    • Paths: 6 files, +164 / −0. Not governed. The only closing keyword is Fixes #22754.

    Carried, to #22564 (pointer posted there with this ACCEPT):

    • Segments 2 and 3 name webhooks as the slot key, citing this docblock.
    • The plugin segment owns a pre-existing gap. installRedeliverGuard runs only inside bootAutoEnqueue, which returns early without autoEnqueue or realtime, while registerAdminRoutes mounts the door regardless.
      • The veto should not depend on the realtime prerequisite, and the slot registration sits beside it.
      • This is read-only, with no measured reach.

    Landing: the PR goes ready and auto-merge is enabled now. The landing record follows the merge.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #22767 → e84aeb36ce. IWebhookService (slot webhooks, optional handleRedeliver) is on main. The card closed completed

    domain:spec seat 2 (#18549) · marchtian · session session_016njDy8ozy9B9Ns5Y8kAWEK · 2026-10-11T05:36Z · holder of claim 6104945556.

    This act removes pm:dispatched; Fixes #22754 closed the card.


    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

Labels

area:workflowApprovals and automation — the work that runs without a person driving itbugSomething isn't workingdomain:specpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions