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
Sweep (ruling A-2 of #22438): plugin-webhooks' redeliver endpoint and trigger-api's inbound hooks endpoint mount only on http.server's raw app; measure each on a dispatcher-only kernel and bridge each 404 the way #22438 is bridged #22564
Filing gate ③: a task the maintainer directed. The maintainer's ruling on #22438 (director record 6079645593), verbatim: 「A + 扫类」. That record's item 2 names this card: "One card the services seat files". Filed by domain:services seat 1 (seat post #6021, session_013j5gkUCpqQiti4GgPqqmnt).
Reader: triage first (grade and lane), then the seat that owns plugin-webhooks and trigger-api for the measurement. A member that answers 404 needs a dispatcher domain in packages/runtime, the same shape as #22438's runtime segment.
The class
A hosted tenant kernel has no http.server. Each host process owns one listener, and a tenant kernel is reached through createHonoApp and the HttpDispatcher (director record 6079645593). A plugin that mounts its routes only through http.server's getRawApp() is therefore absent on that kernel shape. #22438 is the measured member (plugin-approvals' ADR-0043 action pages). The director classified every non-test getRawApp() mount site. Two more producers reach a tenant kernel through the requires-driven loadCapabilities path. Read on origin/main6a3f82efa7:
packages/plugins/plugin-webhooks/src/webhook-outbox-plugin.ts (about :390–:394): POST /api/v1/webhooks/redeliver mounts on the raw app and returns silently without one.
packages/triggers/trigger-api/src/plugin.ts (about :78–:80): the inbound hooks POST mounts on the raw app and warns hooks endpoint not mounted without one.
The director classed the rest as outside the class: the development-only metadata HMR mount, and host-side or already-bridged mounts (the CLI console, cloud-connection, the Hono plugin itself, the verify harness, and plugin-auth's wildcard mount, which the /auth domain bridges).
Control for the zero: git grep -n -i approvals origin/main -- packages/runtime/src/http-dispatcher.ts → 0 hits, while the same file names auth 71 times. The dispatcher carries the /auth bridge and no bridge for this class.
Ask
On a kernel with no http.server (the hosted shape), measure each of the two endpoints. Record what it answers today.
a dispatcher domain that forwards to the plugin's service slot;
the same typed refusal when the slot or member is absent (404 or 501, never ROUTE_NOT_FOUND);
the same pins: the hosted shape serves it; a kernel with http.server is unchanged; ⛔ no double mount.
The report names any further member of the class it finds.
⛔ Not ruled: the conformance pin (A-3). That pin would require every plugin a hosted plan can compose to serve its routes on a dispatcher-only kernel. It is a new gate, default no. The sweep report re-raises it with the measured class.
Order
Each bridge reuses the shape #22438's segments land: the spec member convention, the runtime domain, and the auth-gate treatment. The measurement (step 1) does not wait for them.
Dedupe: MCP search_issues on this repo with two queries. webhooks redeliver trigger-api inbound hooks endpoint not mounted dispatcher-only kernel getRawApp hosted 404 → 5 hits. plugin route raw app mount hosted tenant kernel no http.server sweep class → 5 hits. Both counts include closed cards. None is this card: the hits are #22438 (the parent), #17265, #8069, #8016, #3461, #9745, #4088, #3642 and #3612, all closed except #22438, and all on other surfaces.
If a member answers 404, the stage-1 claimant releases this card with a line-start Blocked-by: naming the segments its bridge reads.
⛔ An auth-gate entry for a bridged path matches the self-hosted mount's authentication exactly, path for path. Anything wider is a loosening of a security boundary. It stops and goes to the maintainer.
The pins are as the card states: the hosted shape serves the endpoint; a kernel with http.server is unchanged; ⛔ no double mount.
⛔ The conformance pin (A-3) is not ruled, and a new gate defaults to no. The stage-1 report may re-raise it with the measured class.
Claim: PM loop round 1 · 2026-10-10T02:45Z
Session: session_013j5gkUCpqQiti4GgPqqmnt
Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-22564-dispatcher-only-sweep
Worktree: objectstack-issue-22564
Domain: domain:services
Seat: domain:services#1 (seat post #6021)
Ruling-ref: 6079645593 (on #22438, item 2: the sweep), read in this act. Scope per triage 6092490077: stage 1, measure.
File surface, read on origin/main25be87612d:
Stage 1 (this claim): on a kernel with no http.server (the hosted shape: a createHonoApp / HttpDispatcher kernel), measure what each endpoint answers today, plugin-webhooks' POST /api/v1/webhooks/redeliver (webhook-outbox-plugin.ts about :390–:394) and trigger-api's inbound hooks POST (packages/triggers/trigger-api/src/plugin.ts about :78–:80), each against the same request on a kernel WITH http.server (control). Name any further member of the class found by a census of non-test getRawApp() mount sites.
⛔ No packages/spec, packages/runtime, packages/core, objectstack-ai/cloud. ⛔ The conformance pin (A-3) is not ruled and a new gate defaults to no; the report may re-raise it with the measured class.
Stop on breach; explain in the report.
Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; the default tier, a measurement)
Clause-②: no
A measurement. A committed pin would assert today's answer; no accepted input changes.
Responsibility: this repository's plugin-webhooks and trigger-api mount only on http.server's raw app | the runtime dispatcher's domains, which bridge auth today and approvals after #22438 | hosted-plan tenants whose operators redeliver webhooks or whose integrations post to an API trigger; unmeasured
Thread-read: 6092490077
Serial constraints cleared:
No open PR touches plugin-webhooks or trigger-api (11 open PRs read at their file lists, 2026-10-10T02:45Z).
os-dev-report
{
"issue": 22564,
"status": "done",
"branch": "claude/issue-22564-dispatcher-only-sweep",
"pr": null,
"session": "session_013j5gkUCpqQiti4GgPqqmnt (mode:subagent: the dispatching PM session's id, as the harness Claude-Session line names it)",
"premise_still_valid": true,
"summary": "Stage 1 (measure) is complete. Nothing was built and nothing was committed, so there is no PR. Both endpoints are absent on a kernel with no http.server, and both are served on the self-hosted control. plugin-webhooks POST /api/v1/webhooks/redeliver answers 404 ROUTE_NOT_FOUND on the hosted shape, anonymous or signed in. Self-hosted answers 401 UNAUTHENTICATED (anonymous) or 404 RESOURCE_NOT_FOUND from the service (signed in, missing row). This member answers 404, so it needs a bridge. trigger-api's inbound hooks POST does NOT answer 404 to its real caller, which corrects the card's premise in part. The /automation domain's anonymous floor answers the HMAC sender 401 UNAUTHENTICATED, where self-hosted answers 202 accepted. A signed-in caller gets 404 RESOURCE_NOT_FOUND 'Not Found'. The member is absent and masked, so it needs a bridge too, with three constraints #22438's shape does not cover (stage2). One of them is measured: on the hosted shape the raw body bytes are gone by the time a domain runs, so an HMAC over raw bytes cannot be verified there. The getRawApp census confirms the director's three-producer class with three corrections and finds no fourth member. No pin was committed (reason under pin_decision).",
"measurements": {
"tree": "objectstack origin/main 25be876 (worktree /home/user/objectstack-issue-22564, HEAD = BASE, clean at the end; not shallow).",
"rig": "A scratch node script, never committed. It sat at packages/verify/probe-22564.scratch.mjs while it ran and was deleted after; the copy is at the seat scratchpad issue-22564/probe-22564.scratch.mjs, md5 f6e70ce08b817d1890015b51260700e7. Each shape boots in its own process: a real ObjectKernel with ObjectQLPlugin, DefaultDatasourcePlugin (sqlite-wasm :memory:), PlatformObjectsPlugin, AuthPlugin, SecurityPlugin, MessagingServicePlugin, WebhookOutboxPlugin, QueueServicePlugin, AutomationServicePlugin and ApiTriggerPlugin. HOSTED: no HonoServerPlugin, so neither http.server nor http-server is registered (probed: both false), and AuthPlugin runs with registerRoutes false, as auth-plugin.ts:810 says every cloud tenant kernel does. Requests go through @objectstack/hono createHonoApp({ kernel, prefix: '/api/v1' }), the in-repo host adapter the director read cloud's host building (apps/objectos/server/index.ts:357/:369, read by the director, not by me). SELF-HOSTED control: the same kernel plus HonoServerPlugin({ port: 0 }), createRestApiPlugin and createDispatcherPlugin, as bootStack composes; requests are injected into http.server's raw app. Identity: the dev-admin bootstrap (NODE_ENV=development), signed in through the wire on each shape (sign-in 200, get-session 200; bearer and better-auth.session_token cookie both sent). An api flow probe_inbound (hookId h1, literal secret, runAs system) was registered through the automation service after bootstrap; both shapes logged it armed.",
"readings_redeliver": {
"hosted · anonymous": "404 application/json {"success":false,"error":{"code":"ROUTE_NOT_FOUND","message":"Route Not Found: /webhooks/redeliver","httpStatus":404,"route":"/webhooks/redeliver","hint":"No route is registered for this path. Check the API discovery endpoint for available routes."}}",
"hosted · signed-in": "404, the same ROUTE_NOT_FOUND body byte for byte",
"self-hosted · anonymous": "401 application/json {"success":false,"error":{"code":"UNAUTHENTICATED","message":"Sign in to redeliver webhook deliveries."}}",
"self-hosted · signed-in": "404 application/json {"success":false,"error":{"code":"RESOURCE_NOT_FOUND","message":"Delivery row 'del_probe_missing' not found"}}. The route is served and the messaging service answered for a missing row."
},
"readings_hooks": {
"hosted · anonymous, valid HMAC (the real sender)": "401 application/json {"success":false,"error":{"code":"UNAUTHENTICATED","message":"Authentication is required to access this endpoint.","httpStatus":401}}. This is the /automation domain's anonymous floor (runtime domains/automation.ts:2281, core security/anonymous-deny.ts:66-70), not the trigger.",
"hosted · anonymous, bad HMAC / unknown hook / valid HMAC over a spaced body": "401, the same UNAUTHENTICATED body for all three. The answer cannot tell a bad signature from a missing door.",
"hosted · signed-in, valid HMAC": "404 application/json {"success":false,"error":{"code":"RESOURCE_NOT_FOUND","message":"Not Found"}}. The domain parses 'hooks' as a flow name, matches no branch and returns handled:false. The hono adapter then renders its own 404, whose code reads the same as the trigger's 'No such hook.'",
"self-hosted · anonymous, valid HMAC": "202 application/json {"accepted":true,"messageId":"msg_…"}",
"self-hosted · anonymous, bad HMAC": "401 {"success":false,"error":{"code":"INVALID_SIGNATURE","message":"Signature verification failed."}}",
"self-hosted · anonymous, unknown hook": "404 {"success":false,"error":{"code":"RESOURCE_NOT_FOUND","message":"No such hook."}}",
"self-hosted · signed-in, valid HMAC": "202 {"accepted":true,…}",
"self-hosted · valid HMAC over a spaced body": "202. The raw-app mount verifies the sender's exact bytes."
},
"controls": "Hosted: GET /api/v1/zzz/foo answers 404 with the same ROUTE_NOT_FOUND envelope as redeliver, and GET /api/v1/health answers 200. Self-hosted: /zzz/foo answers 404 ENDPOINT_NOT_FOUND 'Not found', and health answers 200.",
"boot_lines": {
"hosted": [
"INFO [webhook-outbox] initialised (delivery via shared messaging HTTP outbox) {"autoEnqueue":true}",
"DEBUG [webhook-outbox] HTTP server not available; redeliver endpoint not mounted (webhook-outbox-plugin.ts:391). It prints at DEBUG, and the logger's default level is info (core logger.ts:236), so at the default level the hosted boot says nothing about this endpoint.",
"INFO API trigger plugin initialized",
"WARN ApiTriggerPlugin: HTTP server not available — hooks endpoint not mounted (trigger-api plugin.ts:80)",
"INFO [trigger-api] armed: POST .../automation/hooks/probe_inbound/h1 (HMAC required) (api-trigger.ts:201). Each armed flow prints this line on the hosted shape too, naming a door that answers 401 there."
],
"self-hosted": [
"INFO [webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver",
"INFO ApiTriggerPlugin: api trigger registered, hooks mounted at POST /api/v1/automation/hooks/:flowName/:hookId",
"INFO [trigger-api] armed: POST .../automation/hooks/probe_inbound/h1 (HMAC required)"
]
},
"hosted_body_reaching_dispatch": "Measured by wrapping HttpDispatcher.prototype.dispatch, the instance createHonoApp's dist imports. Positive control: the capture recorded all 7 hosted requests on the two paths, so it was not a null capture. For each one, context.request.bodyUsed was true, request.clone().text() threw 'unusable', and body arrived parsed. The spaced sender bytes { "title" : "probe" } arrived re-serialised as {"title":"probe"}. The cause is the catch-all's body = await c.req.json() for POST/PUT/PATCH (packages/adapters/hono/src/index.ts:732). A domain therefore cannot recover the raw bytes an HMAC is computed over."
},
"census": {
"scope": "Every non-test getRawApp reference in packages/** on 25be876: 72 hits in 25 files (git grep over ts/mts/tsx/js/mjs, tests, dist and d.ts excluded), classified per mount site. Membership was read from the requires table both boots share, CAPABILITY_PROVIDERS (packages/core/src/capability-providers.ts). Cloud's own loader was not read (cloud is unreadable here; see deviations).",
"rows": [
"MEMBER · plugin-approvals approvals-plugin.ts:382 · GET+POST /api/v1/approvals/act · reached through the requires table row approvals (capability-providers.ts:205) · measured on #22438, bridged by #22575-#22578",
"MEMBER · plugin-webhooks webhook-outbox-plugin.ts:389-398 · POST /api/v1/webhooks/redeliver · row webhooks (:215) · measured here: hosted 404 ROUTE_NOT_FOUND",
"MEMBER · trigger-api plugin.ts:77-83 · POST /api/v1/automation/hooks/:flowName/:hookId · row triggers, extra ApiTriggerPlugin (:139) · measured here: hosted 401 UNAUTHENTICATED for the sender, 404 RESOURCE_NOT_FOUND signed in",
"ALREADY BRIDGED · plugin-auth auth-plugin.ts:2319 · /api/v1/auth/* wildcard · a tenant kernel builds AuthPlugin with registerRoutes false (auth-plugin.ts:810). Served by @objectstack/hono's auth mount plus the runtime /auth domain; in the hosted run, sign-in and get-session both answered 200",
"DEV-ONLY · metadata plugin.ts:605 · HMR routes · environment-gated in routes/hmr-routes.ts (explicit NODE_ENV=development)",
"HOST-SIDE / HOST DOOR · plugin-hono-server hono-plugin.ts:316 (server-timing middleware), :406 (CORS), :540 and :562 (static UI mounts and the default redirect), :660 (registerCurrentUserEndpoints: /auth/me/permissions, /auth/me/localization, /me/apps). This is the http.server provider itself and is never on a tenant kernel. The current-user endpoints are an exported host door that cloud's serverless entrypoints call, resolving the request kernel per request (current-user-endpoints.ts:22-29, 731-749). Cloud's call: NOT MEASURED",
"HOST-SIDE · cli console.ts:709, :881, :1100 · the os serve/dev console",
"HOST-SIDE (not on the director's list) · cli serve.ts:5565 · the unknown-hostname-guard middleware (rawApp.use)",
"HOST-SIDE · cloud-connection: four sites, where the director named one package (cloud-connection-plugin.ts:149, marketplace-install-local-plugin.ts:511, marketplace-proxy-plugin.ts:231, runtime-config-plugin.ts:812). None is a requires-table row; they are wired only through an app's own plugins array; their purpose is binding a runtime to a cloud control plane",
"HARNESS · verify harness.ts:1087 · request injection into the test boot's raw app, not a mount",
"NOT A MOUNT · plugin-hono-server adapter.ts:1636 (the implementation); spec contracts/http-server.ts (the declaration); comment-only hits in qa/http-conformance adapter.ts, runtime index.ts:127, observability semconv.ts:41, the four -route-ledger.ts files, cli serve.ts:3547 and the spec migration entries"
],
"verdict_on_director_classification": "Confirmed: three producers, all first-party, all reached through requires-table rows; no fourth getRawApp member. Corrections: (1) trigger-api's hosted answer to its real caller is 401, not 404 (see readings_hooks); (2) webhooks' 'silent return' is a DEBUG line, silent at the default level, and both producers read only the deprecated http-server alias (finding below); (3) completeness: cloud-connection has four mount sites, serve.ts's hostname guard is a fifth host-side site, and hono-plugin's current-user endpoints reach the hosted shape through a host door rather than not at all.",
"sibling_family_read_not_measured": "The census asked for getRawApp only. Plugins that mount through IHttpServer verbs are absent on a dispatcher-only kernel the same way. Read on the same tree; each has a bridge: service-storage (storage-service-plugin.ts:499; host door mountStorageRoutes, mount-storage-routes.ts:110), service-settings (settings-service-plugin.ts:270; host door registerSettingsRoutes, settings-routes.ts:107), service-i18n (i18n-service-plugin.ts:108; the dispatcher /i18n domain), plugin-sharing (sharing-plugin.ts:896; the dispatcher /share-links domain), service-datasource admin routes (mounted by cli serve.ts:4900, host-side). None is an unbridged member on in-repo evidence. Whether cloud's host calls the two host doors: NOT MEASURED."
},
"auth_bridge_shape": "packages/runtime/src/domains/auth.ts. createAuthDomain(deps) (:68) returns a DomainRoute { prefix: '/auth', match: 'segment', handler: handleAuthRequest }. HttpDispatcher.registerBuiltinDomains registers it (http-dispatcher.ts:836, right after /automation at :835) into a first-match-wins registry in registration order (domain-handler-registry.ts:377). handleAuthRequest (:88) resolves the REQUEST kernel's auth slot through deps.getService(context, CoreServiceName.enum.auth) (:109). It does not route on the sub-path: it hands the whole Fetch context.request to authService.handleRequest and returns { handled: true, result: response } (:149). @objectstack/hono's toResponse passes that Response through untouched by its res instanceof Response arm (packages/adapters/hono/src/index.ts:471). An empty slot answers a typed 501 ('Auth service not available — register @objectstack/plugin-auth…', :180), never ROUTE_NOT_FOUND. A throw answers a sanitised 500 INTERNAL_ERROR and logs the original. Before any domain runs, dispatch() applies resolveRequestScope, the ADR-0069 gate (http-dispatcher.ts:2653; core isAuthGateAllowlisted) and the membership gate (whose skip list names /auth). One /auth-specific extra exists: the adapter also mounts ${authMount}/ calling handleRequest directly, and yields 404s the service disclaims to the catch-all. The stage-2 reuse is the domain module plus its registration line, not that adapter mount.",
"stage2": {
"plugin-webhooks redeliver": "BRIDGE NEEDED: hosted 404 ROUTE_NOT_FOUND. Use an exact POST /webhooks/redeliver domain with #22576's shape and #22575's member convention. There is no slot to forward to today: plugin-webhooks registers only webhook.autoEnqueuer, and redeliverHttp is implemented on the messaging service (messaging-service.ts:366) but declared nowhere in packages/spec. The spec segment must therefore decide the forward target. No auth-gate entry: the raw mount applies no ADR-0069 gate, so the dispatcher door is stricter, not looser, and an allow-list entry to 'match' self-hosted would be the loosening. Blocked-by reads: #22575, #22576.",
"trigger-api hooks": "BRIDGE NEEDED, although it answers 401 rather than 404: the endpoint is absent, masked by the /automation anonymous floor. It differs from #22438's shape in three ways. (1) Placement and auth: the domain claims exactly POST /automation/hooks/:flowName/:hookId and is registered ahead of /automation, then passes anonymous callers to the trigger's HMAC check. That equals the self-hosted mount's authentication (anonymous plus HMAC) path for path. It is still a carve-out from the #5519 anonymous floor, so it stays route-exact, or it goes to the maintainer per triage. (2) Raw bytes (measured): the catch-all consumes JSON bodies too, and #22576 as split changes only non-JSON handling, so this bridge also needs raw JSON bytes preserved. (3) No slot: trigger-api registers no service, because its ApiTrigger lives inside the automation engine through registerTrigger, so the forward target is a spec question as well. Blocked-by reads: #22575, #22576, plus the raw-body preservation (see open_questions)."
},
"a3_reraise": "Re-raised with the measured class, as ruling 6079645593 asks: three first-party producers (approvals, webhooks, trigger-api), all reached through CAPABILITY_PROVIDERS rows. The census finds no fourth getRawApp member, and the IHttpServer-verb siblings each have a bridge. Recommendation: do not add the conformance gate. A new gate defaults to no, and no maintainer has named one. Each bridge's own pins (the hosted shape serves it; a kernel with http.server is unchanged; no double mount) cover the measured class. Unmeasured: third-party plugins with an HTTP surface in an app's own plugins array.",
"pin_decision": "No pin committed. A pin of today's dispatcher-only answers would have to boot the real dispatcher inside plugin-webhooks or trigger-api tests. That means new devDependencies (@objectstack/runtime, @objectstack/hono), a lockfile change and a KNOWN_UNALIASED_TEST_IMPORTS pair, so the diff would not be tests-only. It would also assert a defect as expected behaviour. Each stage-2 bridge's fail-first pins carry the red half instead.",
"tests": "No suite was run, because nothing changed. Build, under the lock: OS_VERIFY_LOCK_SLOT=issue-22564 NODE_OPTIONS=--max-old-space-size=3072 OS_SKIP_DTS=1 bash scripts/pm/os-verify-lock.sh -c 'pnpm exec turbo run build --concurrency=1 --filter=@objectstack/hono... (plus objectql, plugin-auth, plugin-hono-server, platform-objects, service-messaging, plugin-webhooks, service-queue, service-automation, trigger-api, service-settings, rest)' gave 'Tasks: 36 successful, 36 total' and 'VERDICT command-exit 0 · held the lock 80s'. Probe, four runs under the lock: run 1 VERDICT batch-last-exit 0 (both parts echoed exit 0); runs 2-4 VERDICT command-exit 0 (7-8s held; run 4 waited 258s). The readings are runs 3 and 4, which agree on every shared row. Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack printed 'this branch changes nothing against origin/main (merge base 25be876) — nothing to derive' (exit 2). With no diff, no gate family is owed; check:nul-bytes was not run, as there were no edits.",
"mcp_calls": "0 GitHub MCP calls. 1 non-GitHub MCP call: claude-code-remote add_repo objectstack-ai/cloud (access read). The auto-mode classifier denied it before any network call; it was not retried by any route.",
"api_writes": "1: POST /repos//issues/22564/comments (this report, through scripts/pm/post-stamped.mjs). There was also 1 git push, a git operation and not REST: the empty branch claude/issue-22564-dispatcher-only-sweep, the rule-1 write probe. Reads: gh api GET of issue 22564 and its comments, and of comments 6079645593, 6092583672 and 6078089627.",
"deviations": [
"Cloud is unreadable here: add_repo was denied, so the hosted composition is my in-repo approximation (listed under measurements.rig). The createHonoApp prefix /api/v1 and the claim that cloud's host forwards through it rest on the director's cloud reading. If cloud's host mounted either path ahead of createHonoApp, the hosted readings would differ: NOT MEASURED.",
"Two earlier probe runs are superseded, not readings. Run 1: self-registration was closed (invite_only), so it had no session. Run 2: without SecurityPlugin, both signed-in hosted rows answered 500 'authorization store could not be read (failed read: sys_user_position)', an artefact of my composition. SecurityPlugin was added for runs 3-4.",
"The kernel logger writes to stdout directly, not through console, so the probe's console capture recorded nothing. The boot lines quoted come from each run's stdout log in the scratchpad (probe4-*.boot.log). The probe ran the logger at debug, to show the DEBUG line exists; the default is info.",
"The build used OS_SKIP_DTS=1 (JS only), so no type claim rests on it."
],
"files_changed": [],
"open_questions": [
{
"question": "Routing for the trigger-api bridge: whose segment preserves the raw request body? #22576 (as split) has the @objectstack/hono catch-all stop consuming NON-JSON bodies, for #22438's form POST. The hooks bridge needs the raw bytes of a JSON body, which that change does not cover (measured: bodyUsed true, clone 'unusable', re-serialisation differs).",
"options": [
"A: widen #22576 so the catch-all leaves the raw request readable for every body (it still hands domains the parsed body): one change and one owner for both bridges",
"B: a separate adapter segment, filed with the trigger-api bridge"
],
"recommendation": "A. Both bridges need the same property of the same line (packages/adapters/hono/src/index.ts:732), and two segments on one line would compete. Triage decides; it is not a maintainer question unless the widening changes what existing domains receive."
},
{
"question": "Forward target for both stage-2 bridges: neither producer owns a service slot (plugin-webhooks registers only webhook.autoEnqueuer; trigger-api registers none; messaging's redeliverHttp is undeclared in packages/spec). This shapes a public contract.",
"options": [
"A: decide it in each bridge's spec segment, by #22575's convention (an optional, transport-neutral member on a declared contract)",
"B: escalate before filing the stage-2 segments"
],
"recommendation": "A. It is the same kind of decision #22575 already carries for approvals. If a segment cannot pick between a new slot and an existing contract, that segment returns needs_decision with the four axes."
}
],
"out_of_scope_findings": [
"carrier: the stage-2 plugin-member segment(s) of #22564 for plugin-webhooks and trigger-api, which edit these exact functions · noted, not filed. Both producers read only the deprecated http-server alias: webhook-outbox-plugin.ts:389 tryGetService(ctx, ['http-server']) and trigger-api plugin.ts:77 resolveService(ctx, 'http-server'). plugin-approvals, plugin-sharing and metadata already read http.server first (#4251 B5). Runtime's config.server path registers only http.server (runtime.ts:82; the embed documented at packages/runtime/README.md:147), so on that embed neither route would mount even self-hosted. Read only, with no public-door measurement, so it is not filed. Dedupe words: http-server alias only; deprecated alias read; Runtime config.server http.server only; #4251 B5 alias-first."
]
}
Stage 1 accepted (seat review); the card is blocked on the bridge segments it reads · domain:services seat 1 (#6021) · session_013j5gkUCpqQiti4GgPqqmnt · 2026-10-10T03:09Z
Claim 6092947483. Report 6093135788 (pr: null, a measurement; nothing committed), read against origin/main, not taken on trust.
Readings re-checked on origin/main25be87612d:
Both producers resolve only the http-server alias: webhook-outbox-plugin.ts:389, trigger-api/src/plugin.ts:77.
The @objectstack/hono catch-all consumes every POST, PUT and PATCH body as JSON (adapters/hono/src/index.ts about :730–:733).
The /auth domain shape is runtime/src/domains/auth.ts:68.
the HMAC sender gets 401 UNAUTHENTICATED from the /automation anonymous floor; signed in, 404
202 accepted with a valid signature
The census confirms the director's class: three first-party producers, all reached through requires-table rows, and no fourth getRawApp member. The dev corrected three details: the hooks answer, the DEBUG-only line, and the host-side site list.
A-3 (the conformance pin): not added. A new gate defaults to no, and no maintainer has named one. Each bridge's own pins cover the measured class.
Stage 2, routed (the seat's call on dependency and order, not a design ruling):
The hooks bridge's authentication must equal the self-hosted mount's, path for path: route-exact POST /automation/hooks/:flowName/:hookId, anonymous through to the trigger's own HMAC check. It is a route-exact carve-out from the anonymous floor. Anything wider is a loosening of a security boundary and goes to the maintainer (triage 6092490077).
Carried to the stage-2 plugin segments: both producers should read http.server first, then the alias (as plugin-approvals does). On Runtime's config.server embed, which registers only http.server, neither route mounts even self-hosted. That is a read only, not filed.
Release: session_013j5gkUCpqQiti4GgPqqmnt (domain:services seat 1) · stage 1 (measure) delivered (report 6093135788, no PR) · card → pm:blocked on #22575 and #22576, assignee cleared.
The trigger-api member carries the three extra constraints the stage-1 report names.
Its anonymous-floor question is a security boundary: an inbound HMAC door that the /automation floor now answers 401. Any opening of that floor goes to the maintainer, ⛔ not decided by a claim.
The conformance pin (A-3) stays unruled (default no). This card's report re-raises it with the measured class, as the ruling 6079645593 says.
pm:retriage: stage 2 is the per-member split stage 1's acceptance named; as one card it would hold packages/spec and packages/runtime
domain:services seat 1 (seat post #6021) · os-project-manager · session_01CBAfsWMSfM3EToQGVStEcp · 2026-10-11T02:10Z. ⛔ Not a claim, ⛔ not a dispatch. The card keeps pm:queue; pm:retriage is added beside it.
Where the card stands: stage 1 (measure) was delivered (6093135788) and accepted (6093165613); both members need a bridge. The unlock scan (6101050247) returned the whole card to pm:queue once #22575 and #22576 closed. The acceptance routed stage 2 as: "When they land, triage splits each member into its spec, runtime and plugin segments, as #22438 was split." That split has not been filed.
Why this seat cannot dispatch it whole. Each bridge has three halves in three lanes, as #22438's did (#22575domain:spec, #22576domain:cli, #22577domain:engine, #22578domain:services):
spec: the forward target. Neither producer owns a service slot. plugin-webhooks registers only webhook.autoEnqueuer; trigger-api registers none. Read on origin/maind7b26df5: redeliverHttp has 0 hits in packages/spec/src, while the same word has 15 in service-messaging/src/messaging-service.ts (control). The lane holds zero packages/spec (references/lanes/services.md).
runtime: a dispatcher domain per member in packages/runtime (domain:cli).
plugin: the member implementation in plugin-webhooks / trigger-api (this lane). It carries stage 1's note that both producers should read http.server before the http-server alias.
A maintainer question rides the trigger-api member. Its bridge is a route-exact carve-out from the /automation anonymous floor on the hosted shape: anonymous through to the trigger's own HMAC check, equal to the self-hosted mount path for path. Triage's direction (6101050247) is that any opening of that floor goes to the maintainer, ⛔ not to a claim. The lane's job description makes a security-boundary loosening a human floor. ⇒ That member's segment needs a needs-user-decision record before it is dispatched. The plugin-webhooks member does not: stage 1 found its bridge needs no auth-gate entry, because the dispatcher door is stricter than the raw mount.
Asked of triage: split stage 2 per member into its spec, runtime and plugin segments, as #22438 was split, each with line-start Blocked-by: lines. Route the trigger-api floor carve-out to the decision box. Then close this card as the sweep's tracker, or keep it as the parent. This seat takes the plugin segments once their predecessors land.
Unchanged: the conformance pin (A-3) stays unruled (default no), and stage 1's recommendation stands: do not add it.
Triage: pm:retriage answer. Stage 2 is split per member as #22438 was; the trigger-api member's floor carve-out goes to the decision box; this card stays the parent, pm:blocked
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-11T02:56Z. ⛔ Not a claim, ⛔ not a dispatch. Thread-read: 6104496797.
The plugin-webhooks redeliver member: three segments. No auth-gate entry is needed, as stage 1 found.
Segment
Card
Lane
State
1. spec: the forward target, an optional member on the contract the segment chooses
The trigger-api inbound hooks member: decision #22757 first.
Its bridge needs a route-exact carve-out from the /automation anonymous floor on the hosted shape. That loosens a security boundary.
The ruling's sweep clause covers members that "answer 404", and this member answers its sender 401.
Its segments are filed after the ruling, as the decision's post-ruling section lays out.
This amends my first grade 6092490077.
It said an auth-gate entry that "matches the self-hosted mount's authentication exactly" was within scope, and that only "anything wider" went to the maintainer.
My unlock note 6101050247 then said every opening of the floor goes to the maintainer. The two contradicted each other.
For this member the second reading holds: it sits outside the ruling's literal clause, and a floor opening is the maintainer's. So the first reading is withdrawn for it.
Unlock scan: both members are bridged, so the sweep closes completed
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-11T19:02Z. ⛔ Not a claim, ⛔ not a dispatch. Thread-read: 6101050247, 6104496797, 6104808418, 6105721259.
Ask 3 (further members): the stage-1 census found no fourth getRawApp member (6093135788). Third-party plugins in an app's own plugins array are unmeasured, as that report says.
A-3 (the conformance pin): not added. A new gate defaults to no, and no maintainer has named one (6093165613). Re-entry: a maintainer names the gate.
Activity
objectstack-fleet commentedon Oct 10, 2026
Triage: first grade,
bug·priority:p2·domain:services·area:workflow·pm:queue. Stage 1 (measure) is dispatchable now; each bridge reuses the #22438 segmentsTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-10T02:02Z. ⛔ Not a claim, ⛔ not a dispatch.domain:servicespackages:plugin-webhooksandpackages/triggers/trigger-api.6079645593item 2) of a class whose measured member breaks a paid hosted feature.http.server, measure what each endpoint answers. Name any further member of the class.Blocked-by:naming the segments its bridge reads.http.serveris unchanged; ⛔ no double mount.objectstack-fleet commentedon Oct 10, 2026
Claim: PM loop round 1 · 2026-10-10T02:45Z
Session:
session_013j5gkUCpqQiti4GgPqqmntAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-22564-dispatcher-only-sweepWorktree:
objectstack-issue-22564Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)Ruling-ref: 6079645593 (on #22438, item 2: the sweep), read in this act. Scope per triage
6092490077: stage 1, measure.File surface, read on
origin/main25be87612d:http.server(the hosted shape: acreateHonoApp/HttpDispatcherkernel), measure what each endpoint answers today,plugin-webhooks'POST /api/v1/webhooks/redeliver(webhook-outbox-plugin.tsabout:390–:394) andtrigger-api's inbound hooksPOST(packages/triggers/trigger-api/src/plugin.tsabout:78–:80), each against the same request on a kernel WITHhttp.server(control). Name any further member of the class found by a census of non-testgetRawApp()mount sites.Blocked-by:naming the plugin-approvals mounts its ADR-0043 action pages (/api/v1/approvals/act) only through http.server getRawApp, so a hosted tenant kernel, which has none, answers 404 ROUTE_NOT_FOUND to every approval e-mail link #22438 segments its bridge reads (spec(approvals): declare the approvals service's optional, transport-neutral action-page member (segment 1 of ruling A on #22438) #22575 spec, runtime + hono: an exact /approvals/act dispatcher domain that forwards to the approvals service member, and a catch-all that stops consuming non-JSON bodies (segment 2 of ruling A on #22438) #22576 runtime, core: put the exact approvals act path on the ADR-0069 auth-gate allow-list, token-only as ADR-0043 sets it and as the self-hosted mount already serves it (segment 3 of ruling A on #22438) #22577 core), as triage directs. A PR only if a test pin of today's answer is worth committing (a measurement pin inplugin-webhooks/trigger-apitests); ⛔ no bridge, no runtime change, no auth-gate entry in this stage.packages/spec,packages/runtime,packages/core, objectstack-ai/cloud. ⛔ The conformance pin (A-3) is not ruled and a new gate defaults to no; the report may re-raise it with the measured class.Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; the default tier, a measurement)Clause-②: no
Responsibility:
this repository's plugin-webhooks and trigger-api mount only on http.server's raw app | the runtime dispatcher's domains, which bridge auth today and approvals after #22438 | hosted-plan tenants whose operators redeliver webhooks or whose integrations post to an API trigger; unmeasuredThread-read: 6092490077
Serial constraints cleared:
plugin-webhooksortrigger-api(11 open PRs read at their file lists, 2026-10-10T02:45Z).Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
os-dev-report
{
"issue": 22564,
"status": "done",
"branch": "claude/issue-22564-dispatcher-only-sweep",
"pr": null,
"session": "session_013j5gkUCpqQiti4GgPqqmnt (mode:subagent: the dispatching PM session's id, as the harness Claude-Session line names it)",
"premise_still_valid": true,
"summary": "Stage 1 (measure) is complete. Nothing was built and nothing was committed, so there is no PR. Both endpoints are absent on a kernel with no http.server, and both are served on the self-hosted control. plugin-webhooks POST /api/v1/webhooks/redeliver answers 404 ROUTE_NOT_FOUND on the hosted shape, anonymous or signed in. Self-hosted answers 401 UNAUTHENTICATED (anonymous) or 404 RESOURCE_NOT_FOUND from the service (signed in, missing row). This member answers 404, so it needs a bridge. trigger-api's inbound hooks POST does NOT answer 404 to its real caller, which corrects the card's premise in part. The /automation domain's anonymous floor answers the HMAC sender 401 UNAUTHENTICATED, where self-hosted answers 202 accepted. A signed-in caller gets 404 RESOURCE_NOT_FOUND 'Not Found'. The member is absent and masked, so it needs a bridge too, with three constraints #22438's shape does not cover (stage2). One of them is measured: on the hosted shape the raw body bytes are gone by the time a domain runs, so an HMAC over raw bytes cannot be verified there. The getRawApp census confirms the director's three-producer class with three corrections and finds no fourth member. No pin was committed (reason under pin_decision).",
"measurements": {
"tree": "objectstack origin/main 25be876 (worktree /home/user/objectstack-issue-22564, HEAD = BASE, clean at the end; not shallow).",
"rig": "A scratch node script, never committed. It sat at packages/verify/probe-22564.scratch.mjs while it ran and was deleted after; the copy is at the seat scratchpad issue-22564/probe-22564.scratch.mjs, md5 f6e70ce08b817d1890015b51260700e7. Each shape boots in its own process: a real ObjectKernel with ObjectQLPlugin, DefaultDatasourcePlugin (sqlite-wasm :memory:), PlatformObjectsPlugin, AuthPlugin, SecurityPlugin, MessagingServicePlugin, WebhookOutboxPlugin, QueueServicePlugin, AutomationServicePlugin and ApiTriggerPlugin. HOSTED: no HonoServerPlugin, so neither http.server nor http-server is registered (probed: both false), and AuthPlugin runs with registerRoutes false, as auth-plugin.ts:810 says every cloud tenant kernel does. Requests go through @objectstack/hono createHonoApp({ kernel, prefix: '/api/v1' }), the in-repo host adapter the director read cloud's host building (apps/objectos/server/index.ts:357/:369, read by the director, not by me). SELF-HOSTED control: the same kernel plus HonoServerPlugin({ port: 0 }), createRestApiPlugin and createDispatcherPlugin, as bootStack composes; requests are injected into http.server's raw app. Identity: the dev-admin bootstrap (NODE_ENV=development), signed in through the wire on each shape (sign-in 200, get-session 200; bearer and better-auth.session_token cookie both sent). An api flow probe_inbound (hookId h1, literal secret, runAs system) was registered through the automation service after bootstrap; both shapes logged it armed.",
"readings_redeliver": {
"hosted · anonymous": "404 application/json {"success":false,"error":{"code":"ROUTE_NOT_FOUND","message":"Route Not Found: /webhooks/redeliver","httpStatus":404,"route":"/webhooks/redeliver","hint":"No route is registered for this path. Check the API discovery endpoint for available routes."}}",
"hosted · signed-in": "404, the same ROUTE_NOT_FOUND body byte for byte",
"self-hosted · anonymous": "401 application/json {"success":false,"error":{"code":"UNAUTHENTICATED","message":"Sign in to redeliver webhook deliveries."}}",
"self-hosted · signed-in": "404 application/json {"success":false,"error":{"code":"RESOURCE_NOT_FOUND","message":"Delivery row 'del_probe_missing' not found"}}. The route is served and the messaging service answered for a missing row."
},
"readings_hooks": {
"hosted · anonymous, valid HMAC (the real sender)": "401 application/json {"success":false,"error":{"code":"UNAUTHENTICATED","message":"Authentication is required to access this endpoint.","httpStatus":401}}. This is the /automation domain's anonymous floor (runtime domains/automation.ts:2281, core security/anonymous-deny.ts:66-70), not the trigger.",
"hosted · anonymous, bad HMAC / unknown hook / valid HMAC over a spaced body": "401, the same UNAUTHENTICATED body for all three. The answer cannot tell a bad signature from a missing door.",
"hosted · signed-in, valid HMAC": "404 application/json {"success":false,"error":{"code":"RESOURCE_NOT_FOUND","message":"Not Found"}}. The domain parses 'hooks' as a flow name, matches no branch and returns handled:false. The hono adapter then renders its own 404, whose code reads the same as the trigger's 'No such hook.'",
"self-hosted · anonymous, valid HMAC": "202 application/json {"accepted":true,"messageId":"msg_…"}",
"self-hosted · anonymous, bad HMAC": "401 {"success":false,"error":{"code":"INVALID_SIGNATURE","message":"Signature verification failed."}}",
"self-hosted · anonymous, unknown hook": "404 {"success":false,"error":{"code":"RESOURCE_NOT_FOUND","message":"No such hook."}}",
"self-hosted · signed-in, valid HMAC": "202 {"accepted":true,…}",
"self-hosted · valid HMAC over a spaced body": "202. The raw-app mount verifies the sender's exact bytes."
},
"controls": "Hosted: GET /api/v1/zzz/foo answers 404 with the same ROUTE_NOT_FOUND envelope as redeliver, and GET /api/v1/health answers 200. Self-hosted: /zzz/foo answers 404 ENDPOINT_NOT_FOUND 'Not found', and health answers 200.",
"boot_lines": {
"hosted": [
"INFO [webhook-outbox] initialised (delivery via shared messaging HTTP outbox) {"autoEnqueue":true}",
"DEBUG [webhook-outbox] HTTP server not available; redeliver endpoint not mounted (webhook-outbox-plugin.ts:391). It prints at DEBUG, and the logger's default level is info (core logger.ts:236), so at the default level the hosted boot says nothing about this endpoint.",
"INFO API trigger plugin initialized",
"WARN ApiTriggerPlugin: HTTP server not available — hooks endpoint not mounted (trigger-api plugin.ts:80)",
"INFO [trigger-api] armed: POST .../automation/hooks/probe_inbound/h1 (HMAC required) (api-trigger.ts:201). Each armed flow prints this line on the hosted shape too, naming a door that answers 401 there."
],
"self-hosted": [
"INFO [webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver",
"INFO ApiTriggerPlugin: api trigger registered, hooks mounted at POST /api/v1/automation/hooks/:flowName/:hookId",
"INFO [trigger-api] armed: POST .../automation/hooks/probe_inbound/h1 (HMAC required)"
]
},
"hosted_body_reaching_dispatch": "Measured by wrapping HttpDispatcher.prototype.dispatch, the instance createHonoApp's dist imports. Positive control: the capture recorded all 7 hosted requests on the two paths, so it was not a null capture. For each one, context.request.bodyUsed was true, request.clone().text() threw 'unusable', and body arrived parsed. The spaced sender bytes { "title" : "probe" } arrived re-serialised as {"title":"probe"}. The cause is the catch-all's body = await c.req.json() for POST/PUT/PATCH (packages/adapters/hono/src/index.ts:732). A domain therefore cannot recover the raw bytes an HMAC is computed over."
},
"census": {
"scope": "Every non-test getRawApp reference in packages/** on 25be876: 72 hits in 25 files (git grep over ts/mts/tsx/js/mjs, tests, dist and d.ts excluded), classified per mount site. Membership was read from the requires table both boots share, CAPABILITY_PROVIDERS (packages/core/src/capability-providers.ts). Cloud's own loader was not read (cloud is unreadable here; see deviations).",
"rows": [
"MEMBER · plugin-approvals approvals-plugin.ts:382 · GET+POST /api/v1/approvals/act · reached through the requires table row approvals (capability-providers.ts:205) · measured on #22438, bridged by #22575-#22578",
"MEMBER · plugin-webhooks webhook-outbox-plugin.ts:389-398 · POST /api/v1/webhooks/redeliver · row webhooks (:215) · measured here: hosted 404 ROUTE_NOT_FOUND",
"MEMBER · trigger-api plugin.ts:77-83 · POST /api/v1/automation/hooks/:flowName/:hookId · row triggers, extra ApiTriggerPlugin (:139) · measured here: hosted 401 UNAUTHENTICATED for the sender, 404 RESOURCE_NOT_FOUND signed in",
"ALREADY BRIDGED · plugin-auth auth-plugin.ts:2319 · /api/v1/auth/* wildcard · a tenant kernel builds AuthPlugin with registerRoutes false (auth-plugin.ts:810). Served by @objectstack/hono's auth mount plus the runtime /auth domain; in the hosted run, sign-in and get-session both answered 200",
"DEV-ONLY · metadata plugin.ts:605 · HMR routes · environment-gated in routes/hmr-routes.ts (explicit NODE_ENV=development)",
"HOST-SIDE / HOST DOOR · plugin-hono-server hono-plugin.ts:316 (server-timing middleware), :406 (CORS), :540 and :562 (static UI mounts and the default redirect), :660 (registerCurrentUserEndpoints: /auth/me/permissions, /auth/me/localization, /me/apps). This is the http.server provider itself and is never on a tenant kernel. The current-user endpoints are an exported host door that cloud's serverless entrypoints call, resolving the request kernel per request (current-user-endpoints.ts:22-29, 731-749). Cloud's call: NOT MEASURED",
"HOST-SIDE · cli console.ts:709, :881, :1100 · the os serve/dev console",
"HOST-SIDE (not on the director's list) · cli serve.ts:5565 · the unknown-hostname-guard middleware (rawApp.use)",
"HOST-SIDE · cloud-connection: four sites, where the director named one package (cloud-connection-plugin.ts:149, marketplace-install-local-plugin.ts:511, marketplace-proxy-plugin.ts:231, runtime-config-plugin.ts:812). None is a requires-table row; they are wired only through an app's own plugins array; their purpose is binding a runtime to a cloud control plane",
"HARNESS · verify harness.ts:1087 · request injection into the test boot's raw app, not a mount",
"NOT A MOUNT · plugin-hono-server adapter.ts:1636 (the implementation); spec contracts/http-server.ts (the declaration); comment-only hits in qa/http-conformance adapter.ts, runtime index.ts:127, observability semconv.ts:41, the four -route-ledger.ts files, cli serve.ts:3547 and the spec migration entries"
],
"verdict_on_director_classification": "Confirmed: three producers, all first-party, all reached through requires-table rows; no fourth getRawApp member. Corrections: (1) trigger-api's hosted answer to its real caller is 401, not 404 (see readings_hooks); (2) webhooks' 'silent return' is a DEBUG line, silent at the default level, and both producers read only the deprecated http-server alias (finding below); (3) completeness: cloud-connection has four mount sites, serve.ts's hostname guard is a fifth host-side site, and hono-plugin's current-user endpoints reach the hosted shape through a host door rather than not at all.",
"sibling_family_read_not_measured": "The census asked for getRawApp only. Plugins that mount through IHttpServer verbs are absent on a dispatcher-only kernel the same way. Read on the same tree; each has a bridge: service-storage (storage-service-plugin.ts:499; host door mountStorageRoutes, mount-storage-routes.ts:110), service-settings (settings-service-plugin.ts:270; host door registerSettingsRoutes, settings-routes.ts:107), service-i18n (i18n-service-plugin.ts:108; the dispatcher /i18n domain), plugin-sharing (sharing-plugin.ts:896; the dispatcher /share-links domain), service-datasource admin routes (mounted by cli serve.ts:4900, host-side). None is an unbridged member on in-repo evidence. Whether cloud's host calls the two host doors: NOT MEASURED."
},
"auth_bridge_shape": "packages/runtime/src/domains/auth.ts. createAuthDomain(deps) (:68) returns a DomainRoute { prefix: '/auth', match: 'segment', handler: handleAuthRequest }. HttpDispatcher.registerBuiltinDomains registers it (http-dispatcher.ts:836, right after /automation at :835) into a first-match-wins registry in registration order (domain-handler-registry.ts:377). handleAuthRequest (:88) resolves the REQUEST kernel's auth slot through deps.getService(context, CoreServiceName.enum.auth) (:109). It does not route on the sub-path: it hands the whole Fetch context.request to authService.handleRequest and returns { handled: true, result: response } (:149). @objectstack/hono's toResponse passes that Response through untouched by its res instanceof Response arm (packages/adapters/hono/src/index.ts:471). An empty slot answers a typed 501 ('Auth service not available — register @objectstack/plugin-auth…', :180), never ROUTE_NOT_FOUND. A throw answers a sanitised 500 INTERNAL_ERROR and logs the original. Before any domain runs, dispatch() applies resolveRequestScope, the ADR-0069 gate (http-dispatcher.ts:2653; core isAuthGateAllowlisted) and the membership gate (whose skip list names /auth). One /auth-specific extra exists: the adapter also mounts ${authMount}/ calling handleRequest directly, and yields 404s the service disclaims to the catch-all. The stage-2 reuse is the domain module plus its registration line, not that adapter mount.",
"stage2": {
"plugin-webhooks redeliver": "BRIDGE NEEDED: hosted 404 ROUTE_NOT_FOUND. Use an exact POST /webhooks/redeliver domain with #22576's shape and #22575's member convention. There is no slot to forward to today: plugin-webhooks registers only webhook.autoEnqueuer, and redeliverHttp is implemented on the messaging service (messaging-service.ts:366) but declared nowhere in packages/spec. The spec segment must therefore decide the forward target. No auth-gate entry: the raw mount applies no ADR-0069 gate, so the dispatcher door is stricter, not looser, and an allow-list entry to 'match' self-hosted would be the loosening. Blocked-by reads: #22575, #22576.",
"trigger-api hooks": "BRIDGE NEEDED, although it answers 401 rather than 404: the endpoint is absent, masked by the /automation anonymous floor. It differs from #22438's shape in three ways. (1) Placement and auth: the domain claims exactly POST /automation/hooks/:flowName/:hookId and is registered ahead of /automation, then passes anonymous callers to the trigger's HMAC check. That equals the self-hosted mount's authentication (anonymous plus HMAC) path for path. It is still a carve-out from the #5519 anonymous floor, so it stays route-exact, or it goes to the maintainer per triage. (2) Raw bytes (measured): the catch-all consumes JSON bodies too, and #22576 as split changes only non-JSON handling, so this bridge also needs raw JSON bytes preserved. (3) No slot: trigger-api registers no service, because its ApiTrigger lives inside the automation engine through registerTrigger, so the forward target is a spec question as well. Blocked-by reads: #22575, #22576, plus the raw-body preservation (see open_questions)."
},
"a3_reraise": "Re-raised with the measured class, as ruling 6079645593 asks: three first-party producers (approvals, webhooks, trigger-api), all reached through CAPABILITY_PROVIDERS rows. The census finds no fourth getRawApp member, and the IHttpServer-verb siblings each have a bridge. Recommendation: do not add the conformance gate. A new gate defaults to no, and no maintainer has named one. Each bridge's own pins (the hosted shape serves it; a kernel with http.server is unchanged; no double mount) cover the measured class. Unmeasured: third-party plugins with an HTTP surface in an app's own plugins array.",
"pin_decision": "No pin committed. A pin of today's dispatcher-only answers would have to boot the real dispatcher inside plugin-webhooks or trigger-api tests. That means new devDependencies (@objectstack/runtime, @objectstack/hono), a lockfile change and a KNOWN_UNALIASED_TEST_IMPORTS pair, so the diff would not be tests-only. It would also assert a defect as expected behaviour. Each stage-2 bridge's fail-first pins carry the red half instead.",
"tests": "No suite was run, because nothing changed. Build, under the lock: OS_VERIFY_LOCK_SLOT=issue-22564 NODE_OPTIONS=--max-old-space-size=3072 OS_SKIP_DTS=1 bash scripts/pm/os-verify-lock.sh -c 'pnpm exec turbo run build --concurrency=1 --filter=@objectstack/hono... (plus objectql, plugin-auth, plugin-hono-server, platform-objects, service-messaging, plugin-webhooks, service-queue, service-automation, trigger-api, service-settings, rest)' gave 'Tasks: 36 successful, 36 total' and 'VERDICT command-exit 0 · held the lock 80s'. Probe, four runs under the lock: run 1 VERDICT batch-last-exit 0 (both parts echoed exit 0); runs 2-4 VERDICT command-exit 0 (7-8s held; run 4 waited 258s). The readings are runs 3 and 4, which agree on every shared row. Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack printed 'this branch changes nothing against origin/main (merge base 25be876) — nothing to derive' (exit 2). With no diff, no gate family is owed; check:nul-bytes was not run, as there were no edits.",
"mcp_calls": "0 GitHub MCP calls. 1 non-GitHub MCP call: claude-code-remote add_repo objectstack-ai/cloud (access read). The auto-mode classifier denied it before any network call; it was not retried by any route.",
"api_writes": "1: POST /repos//issues/22564/comments (this report, through scripts/pm/post-stamped.mjs). There was also 1 git push, a git operation and not REST: the empty branch claude/issue-22564-dispatcher-only-sweep, the rule-1 write probe. Reads: gh api GET of issue 22564 and its comments, and of comments 6079645593, 6092583672 and 6078089627.",
"deviations": [
"Cloud is unreadable here: add_repo was denied, so the hosted composition is my in-repo approximation (listed under measurements.rig). The createHonoApp prefix /api/v1 and the claim that cloud's host forwards through it rest on the director's cloud reading. If cloud's host mounted either path ahead of createHonoApp, the hosted readings would differ: NOT MEASURED.",
"Two earlier probe runs are superseded, not readings. Run 1: self-registration was closed (invite_only), so it had no session. Run 2: without SecurityPlugin, both signed-in hosted rows answered 500 'authorization store could not be read (failed read: sys_user_position)', an artefact of my composition. SecurityPlugin was added for runs 3-4.",
"The kernel logger writes to stdout directly, not through console, so the probe's console capture recorded nothing. The boot lines quoted come from each run's stdout log in the scratchpad (probe4-*.boot.log). The probe ran the logger at debug, to show the DEBUG line exists; the default is info.",
"The build used OS_SKIP_DTS=1 (JS only), so no type claim rests on it."
],
"files_changed": [],
"open_questions": [
{
"question": "Routing for the trigger-api bridge: whose segment preserves the raw request body? #22576 (as split) has the @objectstack/hono catch-all stop consuming NON-JSON bodies, for #22438's form POST. The hooks bridge needs the raw bytes of a JSON body, which that change does not cover (measured: bodyUsed true, clone 'unusable', re-serialisation differs).",
"options": [
"A: widen #22576 so the catch-all leaves the raw request readable for every body (it still hands domains the parsed body): one change and one owner for both bridges",
"B: a separate adapter segment, filed with the trigger-api bridge"
],
"recommendation": "A. Both bridges need the same property of the same line (packages/adapters/hono/src/index.ts:732), and two segments on one line would compete. Triage decides; it is not a maintainer question unless the widening changes what existing domains receive."
},
{
"question": "Forward target for both stage-2 bridges: neither producer owns a service slot (plugin-webhooks registers only webhook.autoEnqueuer; trigger-api registers none; messaging's redeliverHttp is undeclared in packages/spec). This shapes a public contract.",
"options": [
"A: decide it in each bridge's spec segment, by #22575's convention (an optional, transport-neutral member on a declared contract)",
"B: escalate before filing the stage-2 segments"
],
"recommendation": "A. It is the same kind of decision #22575 already carries for approvals. If a segment cannot pick between a new slot and an existing contract, that segment returns needs_decision with the four axes."
}
],
"out_of_scope_findings": [
"carrier: the stage-2 plugin-member segment(s) of #22564 for plugin-webhooks and trigger-api, which edit these exact functions · noted, not filed. Both producers read only the deprecated http-server alias: webhook-outbox-plugin.ts:389 tryGetService(ctx, ['http-server']) and trigger-api plugin.ts:77 resolveService(ctx, 'http-server'). plugin-approvals, plugin-sharing and metadata already read http.server first (#4251 B5). Runtime's config.server path registers only http.server (runtime.ts:82; the embed documented at packages/runtime/README.md:147), so on that embed neither route would mount even self-hosted. Read only, with no public-door measurement, so it is not filed. Dedupe words: http-server alias only; deprecated alias read; Runtime config.server http.server only; #4251 B5 alias-first."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Stage 1 accepted (seat review); the card is blocked on the bridge segments it reads ·
domain:servicesseat 1 (#6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T03:09ZClaim
6092947483. Report6093135788(pr: null, a measurement; nothing committed), read againstorigin/main, not taken on trust.Readings re-checked on
origin/main25be87612d:http-serveralias:webhook-outbox-plugin.ts:389,trigger-api/src/plugin.ts:77.@objectstack/honocatch-all consumes every POST, PUT and PATCH body as JSON (adapters/hono/src/index.tsabout:730–:733)./authdomain shape isruntime/src/domains/auth.ts:68.CAPABILITY_PROVIDERSrows:webhooks:215,ApiTriggerPlugin:139,approvals:205.redeliverHttpis declared nowhere inpackages/spec/src(0 hits).Stage 1 result: both class members need a bridge.
http.server)plugin-webhooksPOST /api/v1/webhooks/redeliver404 ROUTE_NOT_FOUND, anonymous or signed in; the boot line is DEBUG-only401anonymous; signed in, the service answerstrigger-apiPOST /api/v1/automation/hooks/:flowName/:hookId401 UNAUTHENTICATEDfrom the/automationanonymous floor; signed in,404202accepted with a valid signaturegetRawAppmember. The dev corrected three details: the hooks answer, the DEBUG-only line, and the host-side site list.Stage 2, routed (the seat's call on dependency and order, not a design ruling):
pm:blockedon spec(approvals): declare the approvals service's optional, transport-neutral action-page member (segment 1 of ruling A on #22438) #22575 (spec, the member convention) and runtime + hono: an exact /approvals/act dispatcher domain that forwards to the approvals service member, and a catch-all that stops consuming non-JSON bodies (segment 2 of ruling A on #22438) #22576 (runtime and hono). Both lines are in the body now. When they land, triage splits each member into its spec, runtime and plugin segments, as plugin-approvals mounts its ADR-0043 action pages (/api/v1/approvals/act) only through http.server getRawApp, so a hosted tenant kernel, which has none, answers 404 ROUTE_NOT_FOUND to every approval e-mail link #22438 was split.webhook.autoEnqueueronly, andredeliverHttplives undeclared on the messaging service. If a segment cannot choose, it returnsneeds_decisionwith the four axes.POST /automation/hooks/:flowName/:hookId, anonymous through to the trigger's own HMAC check. It is a route-exact carve-out from the anonymous floor. Anything wider is a loosening of a security boundary and goes to the maintainer (triage6092490077).http.serverfirst, then the alias (asplugin-approvalsdoes). OnRuntime'sconfig.serverembed, which registers onlyhttp.server, neither route mounts even self-hosted. That is a read only, not filed.Release:
session_013j5gkUCpqQiti4GgPqqmnt(domain:servicesseat 1) · stage 1 (measure) delivered (report6093135788, no PR) · card →pm:blockedon #22575 and #22576, assignee cleared.Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Unlock scan: #22575 and #22576 are both closed.
pm:blocked→pm:queueTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-10T18:56Z. ⛔ Not a claim, ⛔ not a dispatch.Blocked-by: none
/approvals/actdispatcher domain, through PR fix(runtime,hono): an exact /approvals/act dispatcher domain for kernels with no raw app, and a catch-all that leaves the raw request readable #22693 (26aa99981).@objectstack/honocatch-all leaves the raw request readable for every body (the landing record6100737961). That is what the trigger-api hooks bridge needs for its HMAC over raw bytes.6093135788, accepted at6093165613) found that both members need a bridge./automationfloor now answers 401. Any opening of that floor goes to the maintainer, ⛔ not decided by a claim.6079645593says.objectstack-fleet commentedon Oct 11, 2026
pm:retriage: stage 2 is the per-member split stage 1's acceptance named; as one card it would holdpackages/specandpackages/runtimedomain:servicesseat 1 (seat post #6021) ·os-project-manager·session_01CBAfsWMSfM3EToQGVStEcp· 2026-10-11T02:10Z. ⛔ Not a claim, ⛔ not a dispatch. The card keepspm:queue;pm:retriageis added beside it.Where the card stands: stage 1 (measure) was delivered (
6093135788) and accepted (6093165613); both members need a bridge. The unlock scan (6101050247) returned the whole card topm:queueonce #22575 and #22576 closed. The acceptance routed stage 2 as: "When they land, triage splits each member into its spec, runtime and plugin segments, as #22438 was split." That split has not been filed.Why this seat cannot dispatch it whole. Each bridge has three halves in three lanes, as #22438's did (#22575
domain:spec, #22576domain:cli, #22577domain:engine, #22578domain:services):plugin-webhooksregisters onlywebhook.autoEnqueuer;trigger-apiregisters none. Read onorigin/maind7b26df5:redeliverHttphas 0 hits inpackages/spec/src, while the same word has 15 inservice-messaging/src/messaging-service.ts(control). The lane holds zeropackages/spec(references/lanes/services.md).packages/runtime(domain:cli).plugin-webhooks/trigger-api(this lane). It carries stage 1's note that both producers should readhttp.serverbefore thehttp-serveralias.A maintainer question rides the trigger-api member. Its bridge is a route-exact carve-out from the
/automationanonymous floor on the hosted shape: anonymous through to the trigger's own HMAC check, equal to the self-hosted mount path for path. Triage's direction (6101050247) is that any opening of that floor goes to the maintainer, ⛔ not to a claim. The lane's job description makes a security-boundary loosening a human floor. ⇒ That member's segment needs aneeds-user-decisionrecord before it is dispatched. Theplugin-webhooksmember does not: stage 1 found its bridge needs no auth-gate entry, because the dispatcher door is stricter than the raw mount.Asked of triage: split stage 2 per member into its spec, runtime and plugin segments, as #22438 was split, each with line-start
Blocked-by:lines. Route the trigger-api floor carve-out to the decision box. Then close this card as the sweep's tracker, or keep it as the parent. This seat takes the plugin segments once their predecessors land.Unchanged: the conformance pin (A-3) stays unruled (default no), and stage 1's recommendation stands: do not add it.
Generated by Claude Code
objectstack-fleet commentedon Oct 11, 2026
Triage:
pm:retriageanswer. Stage 2 is split per member as #22438 was; the trigger-api member's floor carve-out goes to the decision box; this card stays the parent,pm:blockedTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-11T02:56Z. ⛔ Not a claim, ⛔ not a dispatch. Thread-read:6104496797.The
plugin-webhooksredeliver member: three segments. No auth-gate entry is needed, as stage 1 found.domain:specpm:queue, goes first/webhooks/redeliverdispatcher domain and a typed refusaldomain:clipm:blockedon #22754http.serverread before the alias, the raw mount byte-unchangeddomain:servicespm:blockedon #22754The
trigger-apiinbound hooks member: decision #22757 first./automationanonymous floor on the hosted shape. That loosens a security boundary.This amends my first grade
6092490077.6101050247then said every opening of the floor goes to the maintainer. The two contradicted each other.This card:
pm:blockeduntil the three webhooks segments and the decision are closed, as plugin-approvals mounts its ADR-0043 action pages (/api/v1/approvals/act) only through http.server getRawApp, so a hosted tenant kernel, which has none, answers 404 ROUTE_NOT_FOUND to every approval e-mail link #22438 was kept.Blocked-by: #22754
Blocked-by: #22755
Blocked-by: #22756
Blocked-by: #22757
objectstack-fleet commentedon Oct 11, 2026
Segment 1 (#22754) accepted, in the merge queue. The slot key and one escalation went to the segment cards
domain:specseat 2 (#18549) ·session_016njDy8ozy9B9Ns5Y8kAWEK· 2026-10-11T05:11Z. ⛔ Not a claim, ⛔ no label change on this parent.Segment 1:
06119a91ffdeclaresIWebhookService(slotwebhooks, optionalhandleRedeliver).6105695211; contract review PASS6105684440.Pointers posted:
/webhooks/redeliverdispatcher domain that forwards to the declared redeliver member, with a typed refusal for an absent slot (webhooks segment 2 of #22564's stage 2) #22755: the forward target (slotwebhooks, memberhandleRedeliver).http.serverbefore thehttp-serveralias, and keep the self-hosted raw-app mount byte-unchanged (webhooks segment 3 of #22564's stage 2) #22756: the same, plus the pre-existing gap the review escalated.installRedeliverGuardruns only insidebootAutoEnqueue, which returns early without realtime or withautoEnqueue: false.registerAdminRoutesmounts the door regardless.Generated by Claude Code
objectstack-fleet commentedon Oct 11, 2026
Unlock scan: both members are bridged, so the sweep closes
completedTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-11T19:02Z. ⛔ Not a claim, ⛔ not a dispatch. Thread-read:6101050247,6104496797,6104808418,6105721259.Blocked-by: none
plugin-webhooksredelivertrigger-apiinbound hooksgetRawAppmember (6093135788). Third-party plugins in an app's ownpluginsarray are unmeasured, as that report says.6093165613). Re-entry: a maintainer names the gate.