You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
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
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 record 6105447950, maintainer 「同意」), which directs triage to split the trigger-api member into the #22438 segment shapes (#22575–#22578). ⛔ Not a claim.
Declare one optional, transport-neutral member that accepts an inbound hook delivery: the flow name, the hook id, the raw body bytes and the headers. Its result is the trigger's own answer (202 accepted, or its typed refusal).
Claim: PM loop round 2 · 2026-10-11T05:14Z
Session: session_016njDy8ozy9B9Ns5Y8kAWEK
Account: marchtian (the seat's linked user as GET /user answers 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 (at origin/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.
os-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 the trigger-api service 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."
]
}
Where review stands. The at-tier contract review of 6c636dca6e found the diff right on all three sections:
the trigger-api slot and the new ITriggerApiService;
the second hook argument (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 IWebhookService intact);
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 false limit sentence.
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 default when config.hookId names none (api-trigger.ts:159);
so an unsigned anonymous post to /<flow>/default answers 401 for a flow armed under the default id and 404 for 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;
at most ONE PR body edit (issue_patch), only if the body repeats the sentence;
ONE os-dev-report comment on this card.
Merge origin/main first if it moved under the PR's files. Gates: the spec typecheck and suite, check:generated, and dispatch-gates --ran on the final diff.
os-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."
]
}
It is declared before it is registered, the same sanctioned window as webhooks. There is no ServiceSlotContracts entry, per the ledger's evidence rule.
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/:id scope its router strips, so the member never re-parses the URL.
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;
The status contract matches handleRequest line by line: 404 / 503 / 401 / 400 (empty body → {}) / 202 / 503 / 503 ENQUEUE_FAILED, with the x-idempotency-key pass-through and the sha256= 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;
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.
Published surface:api-surface and export-originscontracts.json are +1 each, and both shards carry IWebhookService and ITriggerApiService once.
Semver:@objectstack/specminor, Clause-②: yes (widening). No ADR-0087 marker is owed.
Contract review, at tier:6c636dca6e PASS was held (its escalation became patch round 1). PASS 6106795278 on ad451007333980b6df9668828b40beffaeb0b18d.
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_state is clean.
Paths: 6 files, +221 / −0. Not governed. The only closing keyword is Fixes #22772.
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 out-of-scope finding (registerTrigger / unregisterTrigger undeclared on IAutomationService) 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.md rows 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.
Landed: through the merge queue as 5fc57b382e (2026-10-11T08:15Z). It has one parent, 179f7bf6c2, and is an ancestor of origin/main.
Content check:
All 6 PR paths are blob-equal to the reviewed head ad45100733.
Both contracts.json shards at the merge carry IWebhookService and ITriggerApiService.
Review chain:
ACCEPT 6106807013;
at-tier contract review PASS 6106795278 on the head;
the 6c636dca6e PASS was held, because its escalation became patch round 1 (seat order 6106448417).
What now holds (@objectstack/specminor):
packages/spec/src/contracts/trigger-api-service.ts declares ITriggerApiService with 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 default can be probed.
It is exported from @objectstack/spec/contracts. No ServiceSlotContracts entry is added: that belongs to segment 3, beside the registration.
Activity
objectstack-fleet commentedon Oct 11, 2026
Claim: PM loop round 2 · 2026-10-11T05:14Z
Session:
session_016njDy8ozy9B9Ns5Y8kAWEKAccount:
marchtian(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-22772-inbound-hook-memberWorktree:
objectstack-issue-22772Domain:
domain:specSeat:
domain:spec#2File surface (at
origin/main996aa86e0a; stop on breach and explain in the report):packages/spec/src/contracts/gains ONE optional, transport-neutral member that accepts an inbound hook delivery, with its docblock.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.automationis the slot it registers its trigger into..changeset/22772-*.md:@objectstack/specminor,Clause-②: yes (widening).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.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).6105447950on [Decision] 托管环境上 trigger-api 的入站 webhook(HMAC 签名)要不要在/automation匿名门槛上开一个精确到路由的口子,与自托管一致? #22757 (A, with three conditions).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:
contracts/index.ts,api-surface/contracts.jsonandexport-origins/contracts.json.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.packages/spec/src/contracts/orpackages/triggers/trigger-api/. Their file lists were read in this act.Generated by Claude Code
objectstack-fleet commentedon Oct 11, 2026
os-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 the
trigger-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 commentedon Oct 11, 2026
Seat 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:trigger-apislot and the newITriggerApiService;hookargument (the dispatcher strips the prefix and/environments/:id; the router-matched params are the one authority);ApiTrigger.handleRequest;IWebhookServiceintact);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").defaultwhenconfig.hookIdnames none (api-trigger.ts:159);/<flow>/defaultanswers401for a flow armed under the default id and404for one that is not armed;404 RESOURCE_NOT_FOUND;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:
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;Writes budget:
git push;issue_patch), only if the body repeats the sentence;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
objectstack-fleet commentedon Oct 11, 2026
os-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 commentedon Oct 11, 2026
✅ 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:
ITriggerApiServicefor thetrigger-apislot, rather than a member onIAutomationService.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.webhooks. There is noServiceSlotContractsentry, per the ledger's evidence rule.handleInboundHook?(request: Request, hook: { flowName, hookId })returns a Promise of Response./environments/:idscope its router strips, so the member never re-parses the URL.request;handleRequestline by line: 404 / 503 / 401 / 400 (empty body →{}) / 202 / 503 / 503ENQUEUE_FAILED, with thex-idempotency-keypass-through and thesha256=header.404;defaultcan be probed;api-surfaceandexport-originscontracts.jsonare +1 each, and both shards carryIWebhookServiceandITriggerApiServiceonce.@objectstack/specminor,Clause-②: yes (widening). No ADR-0087 marker is owed.6c636dca6ePASS was held (its escalation became patch round 1). PASS6106795278onad451007333980b6df9668828b40beffaeb0b18d.ad45100733: 49 runs, 42 success, 7 skipped, 0 failure.mergeable_stateisclean.Fixes #22772.Carried, with pointers posted with this ACCEPT:
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):kernel.getService('trigger-api')?.handleInboundHook(request, { flowName, hookId }), cited to this docblock;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):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.Acceptance notes (carrier: none):
registerTrigger/unregisterTriggerundeclared onIAutomationService) is an unwritten seam. ADR-0041 records structural mirrors as the family's deliberate pattern, and it is not card-worthy.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
objectstack-fleet commentedon Oct 11, 2026
Landed: 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.5fc57b382e(2026-10-11T08:15Z). It has one parent,179f7bf6c2, and is an ancestor oforigin/main.ad45100733.contracts.jsonshards at the merge carryIWebhookServiceandITriggerApiService.6106807013;6106795278on the head;6c636dca6ePASS was held, because its escalation became patch round 1 (seat order6106448417).@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.defaultcan be probed.@objectstack/spec/contracts. NoServiceSlotContractsentry is added: that belongs to segment 3, beside the registration.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.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.6106824480).Fixes #22772, and it closed this card and nothing else.This act removes
pm:dispatched;Fixes #22772closed the card.Generated by Claude Code