Repository navigation
spec: declare the forward target for trigger-api's inbound hook (POST /automation/hooks/:flowName/:hookId) — an optional, transport-neutral member a dispatcher domain can reach (trigger-api segment 1 of ruling A on #22757) #22772
Description
Activity
- addedenhancementNew feature or requestNew feature or requestpriority:p2Medium: important, M3Medium: important, M3area:workflowApprovals and automation — the work that runs without a person driving itApprovals and automation — the work that runs without a person driving itand removed
on Oct 11, 2026 objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsClaim: PM loop round 2 · 2026-10-11T05:14Z
Session:session_016njDy8ozy9B9Ns5Y8kAWEK
Account:marchtian(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-22772-inbound-hook-member
Worktree:objectstack-issue-22772
Domain:domain:spec
Seat:domain:spec#2
File surface (atorigin/main996aa86e0a; stop on breach and explain in the report):- One service contract under
packages/spec/src/contracts/gains ONE optional, transport-neutral member that accepts an inbound hook delivery, with its docblock.- Which contract is this segment's call.
- It is decided on the four axes, after the spec(approvals): declare the approvals service's optional, transport-neutral action-page member (segment 1 of ruling A on #22438) #22575 / 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 precedents. - trigger-api owns no service slot today;
automationis the slot it registers its trigger into.
- A type-level pin for the member, and the contracts index export if the contract is new.
- One
.changeset/22772-*.md:@objectstack/specminor,Clause-②: yes (widening). - ⛔ The verifier stays in the trigger (ruling A condition 2). The member carries the raw body and headers through; it does not verify.
- ⛔ No runtime, core or plugin code. Segments 2 (runtime + core: an exact
POST /automation/hooks/:flowName/:hookIddispatcher domain and its parameterised ADR-0069 allow-list row, anonymous through to the trigger's HMAC verifier only (trigger-api segment 2 of ruling A on #22757) #22773) and 3 (trigger-api: implement the declared inbound-hook member through the same verifier, readhttp.serverbefore the alias, and keep the self-hosted raw-app mount byte-unchanged (trigger-api segment 3 of ruling A on #22757) #22774) read this member's shape. - ⛔ Stop-and-report:
contracts/approval-service.ts(this seat's approvals: a request opened underonEmptyApprovers: 'admin_rescue'is in no one's pending queue —listRequests/countRequestshave no arm that returns it to the callers who can decide it #22725, in flight),contracts/data-engine.ts(PR feat(spec,objectql,metadata-protocol,client): a write answer carries its advisory validation-rule hits as warnings #22749, enqueued) andcontracts/webhook-service.ts(PR feat(spec): declare IWebhookService.handleRedeliver, the optional transport-neutral webhook redeliver member #22767, enqueued).
Container & model:S,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; floor sonnet · default opus · ceiling fable). A new published contract member, so the contract review atCONTRACT_REVIEW_TIERis owed before enqueue.
Clause-②: yes (widening)
Responsibility: no service slot owns inbound hook delivery.ApiTriggerPluginregisters its trigger intoautomationand mountsPOST /api/v1/automation/hooks/:flowName/:hookIdon the raw app only (packages/triggers/trigger-api/src/plugin.ts:78–:92) | none | a hosted tenant kernel (nohttp.server): the route answers its sender 401 there (ruling6105447950)
Thread-read: none
Prior rulings read:IWebhookService,trigger-api,inbound hook,hooks,ServiceSlotContracts,handleRedeliver,handleActionPage→ 10 ADR term hits (ADR-0029 D1, ADR-0041 §5, ADR-0076 D6/D8, ADR-0092 D2, ADR-0093 D2; none rules this slot). - ADR-0041 §5 binds the answer: the trigger enqueues and ACKs 202, never runs the flow in-band.
- The governing ruling is
6105447950on [Decision] 托管环境上 trigger-api 的入站 webhook(HMAC 签名)要不要在/automation匿名门槛上开一个精确到路由的口子,与自托管一致? #22757 (A, with three conditions). - The segment-1 precedents are spec(approvals): declare the approvals service's optional, transport-neutral action-page member (segment 1 of ruling A on #22438) #22575 (closed) and 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 (PR feat(spec): declare IWebhookService.handleRedeliver, the optional transport-neutral webhook redeliver member #22767, enqueued).
Serial constraints: - PR feat(spec): declare IWebhookService.handleRedeliver, the optional transport-neutral webhook redeliver member #22767 (enqueued) touches
contracts/index.ts,api-surface/contracts.jsonandexport-origins/contracts.json.- If this segment mints a contract, it merges
origin/mainafter feat(spec): declare IWebhookService.handleRedeliver, the optional transport-neutral webhook redeliver member #22767 lands and regenerates those files with the generators, never by hand.
- If this segment mints a contract, it merges
- No other of the 10 open PRs touches
packages/spec/src/contracts/orpackages/triggers/trigger-api/. Their file lists were read in this act.
Generated by Claude Code
- One service contract under
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22772,
"status": "done",
"branch": "claude/issue-22772-inbound-hook-member",
"pr": "#22779",
"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/automation/hooks/:flowName/:hookId. It is a new contract, ITriggerApiService (packages/spec/src/contracts/trigger-api-service.ts), for thetrigger-apiservice slot, with one optional, transport-neutral member: handleInboundHook?(request: Request, hook: { readonly flowName: string; readonly hookId: string }), which returns a Promise of Response. The docblock names the slot key and carries the handler's measured status contract (404 RESOURCE_NOT_FOUND, 503 SERVICE_UNAVAILABLE x2, 401 INVALID_SIGNATURE, 400 INVALID_REQUEST, 202 { accepted, messageId }, 503 ENQUEUE_FAILED). It also states ruling A condition 2 as binding on the implementer: verify inside the member, on the raw body it reads from the request itself, with a per-hook secret and constant-time comparison, before anything is enqueued; the caller never reads, parses, re-serialises or pre-checks the body. It adds no timestamp or nonce field (condition 3 is #22769). Slot call: measurement assumptions 1 and 2 held. ApiTriggerPlugin registers no service, the automation engine only holds the trigger registry (registerTrigger is not even on IAutomationService), and every step of the door is ApiTrigger.handleRequest, so the #22754 reasoning applies and a new trigger-api-owned contract was minted, with no ServiceSlotContracts entry. Member shape: the precedents' Request-to-Response form, plus a second argument carrying the router-matched path parameters, because this is the first bridged door whose address is in the path and the dispatcher URL still carries the API prefix and an /environments/:id scope. The four-axis reasoning for both calls is in the PR body. Merged origin/main after #22767 landed (e84aeb3). The os-regen driver had dropped main's IWebhookService line from both contracts.json shards, and the generators restored it; the net delta is +1 line per shard.",
"tests": "All at head 6c636dc unless noted. (1) pnpm --filter @objectstack/spec build: 'VERDICT command-exit 0'. In dist, handleInboundHook has 2 .d.ts/.d.mts hits and 0 in any .js/.mjs/.cjs, so the change is type-only. (2) Full spec suite, pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: 'Test Files 646 passed (646)', 'Tests 19223 passed | 1 todo (19224)', VERDICT command-exit 0. (3) pnpm --filter @objectstack/spec typecheck: VERDICT command-exit 0, with 'check:test-typecheck: OK ... 246 error(s)' equal to the ledger. tsc -p tsconfig.test.json --listFiles lists trigger-api-service.ts and trigger-api-service.test.ts, with 0 diagnostics in either. (4) Ablation from the committed 476fd79 via scripts/ablation-replace.mjs (wrap mode). The import is relative to src, so no dist leg was needed; the merge changed neither file. A, member made required: anchor 1 to 0, blob b44b329d9b6a to f61056dc7c08. Expected red, observed red: test(36,44) TS2344 InboundHookIsOptional and (57,15) TS2741 on the value pin. B, a rawBody field added to the address: blob to d4254ab8327f. Expected red, observed red: (40,5) TS2344 InboundHookTakesTheRequestAndTheAddress and (44,62) TS2344 HookAddressCarriesOnlyThePathParameters. Both restore legs: blob == HEAD b44b329d9b6a and git diff HEAD empty. (5) dispatch-gates --commands --repo objectstack-ai/objectstack (merge base e84aeb3, 6 paths, 215 lines) derived 87 commands. 86 exited 0, among them check:api-surface ('public API surface + factory signatures unchanged'), check:generated ('All 14 generated artifacts are up to date'), check:export-origins, check:slot-lookup, check:doc-authoring, check-issue-citations ('every citation this change adds resolves'), check-adr-0087-registration ('no declared-breaking changeset'), check:nul-bytes, check-spec-docblock-symbol-anchors and check:type-check-debt. doc-formula-expressions and lean-entry-closure first exited 3 (prerequisite) and were re-run green after a turbo build of formula, lint and objectql. NOT MEASURED: check:dual-build-cjs-loads, reason: it needs a full workspace build (88 packages without dist), and the diff emits no JS; CI runs it. --ran: '87 derived famil(ies) accounted for — 86 run, 1 NOT-MEASURED'. CI on the PR: in_progress at report time.",
"mcp_calls": "0",
"api_writes": "3 REST writes in 2 fleet-write relay strokes (each stroke is one POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]). Stroke 1, pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/22779/assignees (marchtian). Stroke 2: POST /repos//issues/22772/comments (this report). Plus git push (not REST). Zero label writes: the dispatch named none, and skip-changeset does not apply.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted in PR Acceptance notes, not filed · the trigger registry that every trigger plugin calls (automation.registerTrigger / unregisterTrigger) is not declared on IAutomationService. Each plugin types its own structural slice instead (trigger-api plugin.ts:12, AutomationTriggerRegistry). This is an undeclared cross-package seam with no measured wrong answer at any public door, so it is not card-worthy under the filing classes. Dedupe words: registerTrigger not on IAutomationService; AutomationTriggerRegistry structural slice; trigger registry contract."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsSeat order on PR #22779 at
6c636dca6e: patch round 1, text only. The docblock's "no oracle for which flows exist" overclaims a security propertydomain:specseat 2 (#18549) ·marchtian· sessionsession_016njDy8ozy9B9Ns5Y8kAWEK· 2026-10-11T06:56Z · holder of claim6105742415. Report read:6106223618.Where review stands. The at-tier contract review of
6c636dca6efound the diff right on all three sections:- the
trigger-apislot and the newITriggerApiService; - the second
hookargument (the dispatcher strips the prefix and/environments/:id; the router-matched params are the one authority); - ruling A's condition 2, bound on all six points;
- condition 3, with nothing pre-committed;
- the status contract, line by line against
ApiTrigger.handleRequest; - the absence rule;
- the two shards (+1 each, with
IWebhookServiceintact); - the semver.
It escalated one sentence. This card carries
security, so the seat adopts that escalation as a patch round rather than landing a contract that states a security property the runtime does not hold. The same seat did the same for PR #22775's falselimitsentence.The order: one sentence, in
packages/spec/src/contracts/trigger-api-service.ts(the first bullet of "What it serves").- It says an unknown flow and a wrong hook id answer identically, "so the door is no oracle for which flows exist".
- The first half is true. The conclusion is not:
- the hook check runs before the signature check;
- a hook id defaults to
defaultwhenconfig.hookIdnames none (api-trigger.ts:159); - so an unsigned anonymous post to
/<flow>/defaultanswers401for a flow armed under the default id and404for one that is not armed; - that tells a prober which flows exist.
- Rewrite it to what holds at the head:
- unknown flow ≡ wrong hook id →
404 RESOURCE_NOT_FOUND; - the hook is matched before the signature is verified, so the hook id, not the flow name, is what keeps an armed flow unprobeable;
- a flow left on the default id can be probed;
- a uniform refusal is the hardening card trigger-api: the inbound-hook HMAC signs the body alone, so its signed material carries no timestamp or tolerance window (the replay-protection hardening card condition 3 of ruling 6105447950 names) #22769's question.
- unknown flow ≡ wrong hook id →
- Read the code before writing it. ⛔ No other contract text, ⛔ no code, ⛔ no changeset line unless the fix makes one false.
- The same overclaim sits in
api-trigger.ts's own code comment. ⛔ Not this PR: it is segment 3's file (trigger-api: implement the declared inbound-hook member through the same verifier, readhttp.serverbefore the alias, and keep the self-hosted raw-app mount byte-unchanged (trigger-api segment 3 of ruling A on #22757) #22774), and the seat carries it there.
Also update the PR body if it repeats the sentence. If it does not, leave the body alone.
The seat's answers on the review's other escalations. No change is needed in this PR:
- the pointers to runtime + core: an exact
POST /automation/hooks/:flowName/:hookIddispatcher domain and its parameterised ADR-0069 allow-list row, anonymous through to the trigger's HMAC verifier only (trigger-api segment 2 of ruling A on #22757) #22773 and trigger-api: implement the declared inbound-hook member through the same verifier, readhttp.serverbefore the alias, and keep the self-hosted raw-app mount byte-unchanged (trigger-api segment 3 of ruling A on #22757) #22774 (slot key, member, the allow-list normalisation pin) are posted by the seat at the ACCEPT; - the services-lane note goes beside trigger-api: the inbound-hook HMAC signs the body alone, so its signed material carries no timestamp or tolerance window (the replay-protection hardening card condition 3 of ruling 6105447950 names) #22769.
Writes budget:
git push;- at most ONE PR body edit (
issue_patch), only if the body repeats the sentence; - ONE
os-dev-reportcomment on this card.
Merge
origin/mainfirst if it moved under the PR's files. Gates: the spec typecheck and suite,check:generated, anddispatch-gates --ranon the final diff.
Generated by Claude Code
- the
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22772,
"status": "done",
"branch": "claude/issue-22772-inbound-hook-member",
"pr": "#22779",
"session": "session_016njDy8ozy9B9Ns5Y8kAWEK (mode:subagent: the dispatching PM session's id, as the harness Claude-Session line names it)",
"premise_still_valid": true,
"summary": "Patch round 1 (seat order 6106448417), text only, at head ad45100. In packages/spec/src/contracts/trigger-api-service.ts, the first bullet of 'What it serves' no longer concludes that the door is no oracle. It now states four things, each read from the code: an unknown flow and a wrong hook id answer one 404 RESOURCE_NOT_FOUND; the hook is matched BEFORE the signature is verified (api-trigger.ts:225-230 ahead of the secret read and verifySignature at :247), so an unsigned post naming an armed flow and its hook id gets the 503 or 401 instead of that 404, and the hook id, not the flow name, is what keeps an armed flow unprobeable; a flow left on the default hook id (default, api-trigger.ts:159) can be probed that way; and whether every unverified post should get one uniform refusal is the hardening card's question (#22769). It names the secret-unavailable 503 beside the 401 because that answer also precedes verification. No other contract text, no code and no changeset line changed: the changeset says only that the two answer the same 404, which still holds. api-trigger.ts's own comment is untouched (segment 3's file). The PR body's parenthesis '(identical, so the door is no oracle)' was replaced in one issue_patch. origin/main moved 3 commits (165587e, a2e94c2, a8f24b0), none touching the PR's 6 paths, so there was no merge; both IWebhookService and ITriggerApiService remain in both contracts.json shards (2 hits each). DEVIATION, platform: the PR body footer is now ABSENT. Following platform-readings ('the edit side sends no footer; PATCH /issues/{n} on a PR appends one bare footer, +58 bytes'), the patch sent the body without its footer block. The relay stored exactly the 18005 bytes sent, and two REST read-backs (07:37:10Z and 07:37:32Z) show 0 'Generated by' lines: no append happened on this channel. The body therefore carries no attribution footer, and that reading row (lines 339-342) is contradicted by this measurement. Restoring the footer needs one more body write, outside this round's budget, so it is left for the seat. The body's Verification section still names 6c636dc, because the order allowed no other body change; the readings at ad45100 are below.",
"tests": "At head ad45100. (1) pnpm --filter @objectstack/spec build: VERDICT command-exit 0. (2) pnpm --filter @objectstack/spec check:generated: exit 0, 'All 14 generated artifacts are up to date'; api-surface/contracts.json and export-origins/contracts.json each hold both IWebhookService and ITriggerApiService. (3) pnpm --filter @objectstack/spec typecheck: VERDICT command-exit 0, with 'check:test-typecheck: OK ... 246 error(s)' equal to the ledger. (4) Full spec suite, vitest run --project local --maxWorkers=2: 'Test Files 646 passed (646)', 'Tests 19223 passed | 1 todo (19224)', VERDICT command-exit 0. (5) dispatch-gates --commands --repo objectstack-ai/objectstack (merge base e84aeb3, 6 paths, 221 lines) derived the same 87 commands as last round. 86 exited 0, among them check:api-surface ('public API surface + factory signatures unchanged'), check:generated, check-issue-citations ('every citation this change adds resolves'), check-adr-0087-registration ('no declared-breaking changeset'), check:doc-authoring, check-spec-docblock-symbol-anchors, check:slot-lookup, check:query-options-erasure and check:type-check-debt; formula, lint and objectql were built first, so doc-formula-expressions and lean-entry-closure ran at once. NOT MEASURED: check:dual-build-cjs-loads (exit 3), reason: it needs a full workspace build, and the diff emits no JS. --ran: '87 derived famil(ies) accounted for — 86 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)'. Declared: dispatch-gates flagged the tree as 3 commits behind origin/main, where 4 family files differ (.github/workflows/ci.yml, check-ci-filter-parity, check-type-check-coverage, publish-smoke-pack); per the order there was no merge, because none of the PR's paths moved, and CI runs the merged ref with the new copies. No ablation this round: the change is docblock prose, which no pin reads.",
"mcp_calls": "0",
"api_writes": "2 REST writes in 2 fleet-write relay strokes (each one POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): PATCH /repos//issues/22779 (issue_patch, the PR body; 18005 bytes sent, 18005 stored) and POST /repos//issues/22772/comments (this report). Plus one git push (ad45100).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: domain:spec seat 2 (holder of the PR) · noted, not filed · the PR body of #22779 now has no attribution footer, because the documented edit-side append (+58 bare footer) did not happen on the relay's PATCH /issues/22779 today: sent 18005, stored 18005, 0 footer lines on two read-backs. platform-readings.md rows 339-342 need a re-measure. Dedupe words: PR body edit footer not appended; issue_patch footer absent; platform-readings edit-side footer."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actions✅ ACCEPT — PR #22779 at
ad45100733(Fixes #22772). Contract review PASS; ready and enqueueddomain:specseat 2 (#18549) ·marchtian· sessionsession_016njDy8ozy9B9Ns5Y8kAWEK· 2026-10-11T07:45Z · holder of claim6105742415. Reports:6106223618(build round), round-1os-dev-report(patch round 1, seat order6106448417).Checked against the diff and the records, not the reports:
- The slot call: a NEW
ITriggerApiServicefor thetrigger-apislot, rather than a member onIAutomationService.- The automation service holds only the trigger registry (
registerTriggeris not on its contract). The whole door isApiTrigger.handleRequest, so the spec: declare the forward target forPOST /api/v1/webhooks/redeliver— an optional, transport-neutral service member a dispatcher domain can reach (webhooks segment 1 of #22564's stage 2) #22754 reasoning applies. - It is declared before it is registered, the same sanctioned window as
webhooks. There is noServiceSlotContractsentry, per the ledger's evidence rule.
- The automation service holds only the trigger registry (
- The member:
handleInboundHook?(request: Request, hook: { flowName, hookId })returns a Promise of Response.- The second argument carries the router-matched path parameters. The dispatcher URL still carries the API prefix and an
/environments/:idscope its router strips, so the member never re-parses the URL.
- The second argument carries the router-matched path parameters. The dispatcher URL still carries the API prefix and an
- Ruling A, condition 2, binds the implementer on all six points:
- it verifies inside the member, on the raw bytes it reads from
request; - it uses a per-hook secret;
- it compares in constant time;
- it verifies before anything is enqueued;
- the caller never reads, parses, re-serialises or pre-checks the body;
- there is one verifier.
- it verifies inside the member, on the raw bytes it reads from
- Condition 3 pre-commits nothing (trigger-api: the inbound-hook HMAC signs the body alone, so its signed material carries no timestamp or tolerance window (the replay-protection hardening card condition 3 of ruling 6105447950 names) #22769).
- The status contract matches
handleRequestline by line: 404 / 503 / 401 / 400 (empty body →{}) / 202 / 503 / 503ENQUEUE_FAILED, with thex-idempotency-keypass-through and thesha256=header. - Patch round 1 (text only, +8/−2): the docblock no longer claims the door "is no oracle for which flows exist". It now states what holds:
- an unknown flow and a wrong hook id answer one
404; - the hook is matched before the signature, so the hook id, not the flow name, keeps an armed flow unprobeable;
- a flow on the default id
defaultcan be probed; - a uniform refusal is trigger-api: the inbound-hook HMAC signs the body alone, so its signed material carries no timestamp or tolerance window (the replay-protection hardening card condition 3 of ruling 6105447950 names) #22769's question.
- The PR body's matching parenthesis was corrected. The seat restored the body's attribution footer (the PATCH had dropped it) and corrected its stale "final head" line.
- an unknown flow and a wrong hook id answer one
- Published surface:
api-surfaceandexport-originscontracts.jsonare +1 each, and both shards carryIWebhookServiceandITriggerApiServiceonce. - Semver:
@objectstack/specminor,Clause-②: yes (widening). No ADR-0087 marker is owed. - Contract review, at tier:
6c636dca6ePASS was held (its escalation became patch round 1). PASS6106795278onad451007333980b6df9668828b40beffaeb0b18d. - CI on
ad45100733: 49 runs, 42 success, 7 skipped, 0 failure.- The 49 include two PR-event re-fires from the body edits.
- Every required context is green.
mergeable_stateisclean.
- Paths: 6 files, +221 / −0. Not governed. The only closing keyword is
Fixes #22772.
Carried, with pointers posted with this ACCEPT:
- runtime + core: an exact
POST /automation/hooks/:flowName/:hookIddispatcher domain and its parameterised ADR-0069 allow-list row, anonymous through to the trigger's HMAC verifier only (trigger-api segment 2 of ruling A on #22757) #22773 (runtime + core):- the forward target is
kernel.getService('trigger-api')?.handleInboundHook(request, { flowName, hookId }), cited to this docblock; - condition 1's self-test should pin that the allow-list's segment normalisation and the domain route agree on every spelling, and that the member receives decoded segments.
- the forward target is
- trigger-api: implement the declared inbound-hook member through the same verifier, read
http.serverbefore the alias, and keep the self-hosted raw-app mount byte-unchanged (trigger-api segment 3 of ruling A on #22757) #22774 (plugin):- register exactly the slot key
trigger-api, beside theServiceSlotContractsentry that segment owns; api-trigger.ts:226's own code comment carries the same "no oracle" overclaim, and that segment narrows it.
- register exactly the slot key
- trigger-api: the inbound-hook HMAC signs the body alone, so its signed material carries no timestamp or tolerance window (the replay-protection hardening card condition 3 of ruling 6105447950 names) #22769 (hardening): a note that the contract now cites it for the uniform-refusal question.
Acceptance notes (carrier: none):
- The out-of-scope finding (
registerTrigger/unregisterTriggerundeclared onIAutomationService) is an unwritten seam. ADR-0041 records structural mirrors as the family's deliberate pattern, and it is not card-worthy. - Platform reading (round-1 dev): the relay's
PATCH /issues/{n}on a PR body appended NO footer today (sent 18005, stored 18005, 0 footer lines on two read-backs).platform-readings.mdrows 339–342 say an edit appends one. The skill text is.claude/**, a governed path, so this goes to the maintainer in the seat's round report.
Landing: the PR goes ready and auto-merge is enabled now. The landing record follows the merge.
Generated by Claude Code
- The slot call: a NEW
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsLanded: PR #22779 →
5fc57b382e.ITriggerApiService(slottrigger-api, optionalhandleInboundHook) is onmain. The card closedcompleteddomain:specseat 2 (#18549) ·marchtian· sessionsession_016njDy8ozy9B9Ns5Y8kAWEK· 2026-10-11T08:15Z · holder of claim6105742415.- Landed: through the merge queue as
5fc57b382e(2026-10-11T08:15Z). It has one parent,179f7bf6c2, and is an ancestor oforigin/main. - Content check:
- All 6 PR paths are blob-equal to the reviewed head
ad45100733. - Both
contracts.jsonshards at the merge carryIWebhookServiceandITriggerApiService. - Review chain:
- ACCEPT
6106807013; - at-tier contract review PASS
6106795278on the head; - the
6c636dca6ePASS was held, because its escalation became patch round 1 (seat order6106448417).
- ACCEPT
- All 6 PR paths are blob-equal to the reviewed head
- What now holds (
@objectstack/specminor):packages/spec/src/contracts/trigger-api-service.tsdeclaresITriggerApiServicewith one optional, transport-neutral member:handleInboundHook?(request: Request, hook: { flowName, hookId }), which returns a Promise of Response.- Its docblock binds ruling A's condition 2 on the implementer and states the trigger's status contract. It also states the probe surface as it is: the hook id, not the flow name, keeps an armed flow unprobeable, and a flow on the default id
defaultcan be probed. - It is exported from
@objectstack/spec/contracts. NoServiceSlotContractsentry is added: that belongs to segment 3, beside the registration.
- Unblocked: runtime + core: an exact
POST /automation/hooks/:flowName/:hookIddispatcher domain and its parameterised ADR-0069 allow-list row, anonymous through to the trigger's HMAC verifier only (trigger-api segment 2 of ruling A on #22757) #22773 (the runtime dispatcher domain and the parameterised allow-list row) and trigger-api: implement the declared inbound-hook member through the same verifier, readhttp.serverbefore the alias, and keep the self-hosted raw-app mount byte-unchanged (trigger-api segment 3 of ruling A on #22757) #22774 (the trigger-api member) carriedBlocked-by: #22772, and that blocker is now closed. Their unlock is the triage seat's scan.- The pointers
6106817103(runtime + core: an exactPOST /automation/hooks/:flowName/:hookIddispatcher domain and its parameterised ADR-0069 allow-list row, anonymous through to the trigger's HMAC verifier only (trigger-api segment 2 of ruling A on #22757) #22773) and6106820278(trigger-api: implement the declared inbound-hook member through the same verifier, readhttp.serverbefore the alias, and keep the self-hosted raw-app mount byte-unchanged (trigger-api segment 3 of ruling A on #22757) #22774) name the slot, the member, the allow-list normalisation self-test and theapi-trigger.tscomment to narrow. - trigger-api: the inbound-hook HMAC signs the body alone, so its signed material carries no timestamp or tolerance window (the replay-protection hardening card condition 3 of ruling 6105447950 names) #22769 holds the uniform-refusal question (note
6106824480).
- The pointers
- Mis-close scan: the only closing keyword is
Fixes #22772, and it closed this card and nothing else.
This act removes
pm:dispatched;Fixes #22772closed the card.
Generated by Claude Code
- Landed: through the merge queue as
Filing gate ③: a segment the maintainer's ruling directs. Filed by the triage seat (seat post #6015,
session_01AavokzJ5DndAwitDXvKy4U) under ruling A on #22757 (director record6105447950, maintainer 「同意」), which directs triage to split the trigger-api member into the #22438 segment shapes (#22575–#22578). ⛔ Not a claim.Scope (the #22575 convention)
202accepted, or its typed refusal).Clause-②: yes, since it is a new optional member.Done when
The member and its docblock are on
main, with a type-level pin, and segments 2 and 3 can name it.domain:spec· p2 ·security(it is on the boundary's path).