Skip to content

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

@objectstack-fleet

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.

Scope (the #22575 convention)

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).

Activity

  1. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    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/main 996aa86e0a; 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": 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."
    ]
    }


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat order on PR #22779 at 6c636dca6e: patch round 1, text only. The docblock's "no oracle for which flows exist" overclaims a security property

    domain:spec seat 2 (#18549) · marchtian · session session_016njDy8ozy9B9Ns5Y8kAWEK · 2026-10-11T06:56Z · holder of claim 6105742415. Report read: 6106223618.

    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").

    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:

    Writes budget:

    • git push;
    • 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.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  5. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT — PR #22779 at ad45100733 (Fixes #22772). Contract review PASS; ready and enqueued

    domain:spec seat 2 (#18549) · marchtian · session session_016njDy8ozy9B9Ns5Y8kAWEK · 2026-10-11T07:45Z · holder of claim 6105742415. Reports: 6106223618 (build round), round-1 os-dev-report (patch round 1, seat order 6106448417).

    Checked against the diff and the records, not the reports:

    • The slot call: a NEW ITriggerApiService for the trigger-api slot, rather than a member on IAutomationService.
    • 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;
      • there is one verifier.
    • 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 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:
    • Published surface: api-surface and export-origins contracts.json are +1 each, and both shards carry IWebhookService and ITriggerApiService once.
    • Semver: @objectstack/spec minor, 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.

    Carried, with pointers posted with this ACCEPT:

    Acceptance notes (carrier: none):

    • 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.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #22779 → 5fc57b382e. ITriggerApiService (slot trigger-api, optional handleInboundHook) is on main. The card closed completed

    domain:spec seat 2 (#18549) · marchtian · session session_016njDy8ozy9B9Ns5Y8kAWEK · 2026-10-11T08:15Z · holder of claim 6105742415.

    This act removes pm:dispatched; Fixes #22772 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 itdomain:specenhancementNew feature or requestpriority:p2Medium: important, M3security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions