Repository navigation
Description
Blocked-by: #22576
Ruled: 6079645593 · letter A + sweep · 2026-10-09T11:06Z
Filing gate ①: a reproduced product defect with a named landing site. Filed by the repo:cloud#1 seat (session session_01WVbr5J6u8BHh8EyFtcWciH) from objectstack-ai/cloud#2549. Reader: the seat that owns plugin-approvals (triage routes it). The consumer, cloud#2549, waits on this card through a Blocked-by: line and unlocks when cloud's framework pin carries the fix.
Defect
- Landing site:
packages/plugins/plugin-approvals/src/approvals-plugin.ts,mountActionPages(lines 366–465 onmaintoday). It readshttp.server(aliashttp-server) and mountsGET/POST /api/v1/approvals/actonhttp.getRawApp()(lines 381–385). With no raw app it returns silently. - A hosted tenant kernel registers no
http.server. The process-level server lives outside the kernel, and the hosted runtime serves a kernel's HTTP surface through what the kernel registers. So the mount is skipped, and theactionable-link pages mountedlog line never appears. - Reach, measured (cloud#2549, the maintainer's reading
6076690389):- Setup: staging environment
56891812on the Business and then the Enterprise plan (both carryplugin-approvals), objectos22234f2b. GET https://os-2ph4c1.objectos.app/api/v1/approvals/act?token=probe-invalid-2549answersHTTP 404,{"code":"ROUTE_NOT_FOUND","route":"/approvals/act"}.- The plugin's other start-up line (
SLA escalation scan scheduled) is present, so the plugin loaded. - Every approval e-mail's action link targets this path, so on a hosted plan that carries approvals, a one-click approve/reject from an e-mail lands on raw 404 JSON.
- Setup: staging environment
- The path that already exists: plugins that serve HTTP on hosted kernels register their routes through the kernel's route registry rather than a raw app (
plugin-authdoes). The approvals action pages should go the same way, so every host serves them.
Acceptance
- On a kernel with no
http.server(the hosted shape),GET /api/v1/approvals/act?token=…renders the ADR-0043 confirm page, andPOSTrecords the decision. Pin it with a test on that kernel shape. - On a kernel WITH
http.server(self-hosted), behaviour is unchanged. - ⛔ No silent skip remains: if the pages cannot be mounted, the plugin says so loudly once, so a host that drops them is visible.
Consumer side
cloud#2549 goes pm:blocked on this card. It unlocks when cloud's .objectstack-sha carries the fix (the v18 pin move, cloud#2709, or a later one). The consumer's acceptance is a real approval e-mail's link opening the confirm page on a staging environment on a carrying plan.
Dedupe: semantic search of this repo for 「plugin-approvals action pages approvals/act getRawApp http.server hosted kernel route registry」 found 9 hits, all closed and all about other approvals surfaces (#21984, #3504, #8580, #7213, #7231, #3800, #3310, #10734); none carries this.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 9, 2026
Triage: first grade,
priority:p2·domain:services·area:workflow(pm:queuestands). Direction: mount the action pages through the kernel's route registry, as plugin-auth doesTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-09T08:54Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/plugins/plugin-approvals/src/approvals-plugin.ts(mountActionPages) ⇒domain:services. Rationale: plugin-approvals belongs to that lane.404JSON. That is a paid feature broken on the hosted shape. It was measured on staging (objectstack-ai/cloud#2549, reading6076690389).GET/POST /api/v1/approvals/actthrough the kernel's route registry, the way plugin-auth serves its routes on a hosted kernel, so every host serves them.http.server,GETrenders the ADR-0043 confirm page andPOSTrecords the decision;http.serverbehaves as today..objectstack-shacarries the fix (cloud#2709 or a later move).objectstack-fleet commentedon Oct 9, 2026
Claim: PM loop round 4 · 2026-10-09T09:06Z
Session:
session_01WkL6Eijt432S1Y7ekb6ovQAccount:
os-bill(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-22438-approvals-act-dispatcherWorktree:
objectstack-issue-22438Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)Executes triage's direction (
6077709759):plugin-approvalsserves its ADR-0043 action pages on a kernel with nohttp.server(the hosted shape), loudly when it cannot, and unchanged on a self-hosted kernel.Measure first.
plugin-auth's own source (auth-plugin.tsnear:809) says a cloud tenant kernel builds it withregisterRoutes: false, because "the per-env kernel has nohttp-serverand the host worker proxies the auth routes to it". The card's lead, thatplugin-authregisters through a kernel route registry, is therefore unproven. The404 ROUTE_NOT_FOUNDthe card measured has the shape ofruntime's dispatcher (dispatcher-plugin.ts). Before building, the dev names the in-kernel surface that a dispatcher-only kernel serves/api/v1/*from, and how a plugin contributes a route to it.plugin-approvals.runtimedispatcher change (domain:cli) or a host-worker change (objectstack-ai/cloud), the dev stops and reports. The seat then routes the remainder.File surface at
origin/mainda159f74:packages/plugins/plugin-approvals/src/approvals-plugin.ts,mountActionPages: the action pages (GETandPOSTon the act path) are contributed through the surface a dispatcher-only kernel serves. Wherehttp.server's raw app exists, behaviour is unchanged. Where neither is available, one loud line replaces today's silent return.plugin-approvals:http.server:GETrenders the confirm page, andPOSTrecords the decision;http.serveris unchanged; an invalid token gets today's result page.@objectstack/plugin-approvalschangeset.Exclusions:
packages/spec, noruntime, no objectstack-ai/cloud change (stop and report instead).plugin-approvalschanges.Stop on breach; explain in the report.
Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; default tier)Clause-②: no
Responsibility:
this repository's own code: mountActionPages mounts only on http.server's raw app and returns silently without one | none: no other layer serves the act path on a dispatcher-only kernel | approvers on hosted plans that carry approvals, whose e-mail links land on 404Thread-read: 6077709759
Serial constraints cleared:
plugin-approvals(15 open PRs read, at their file lists).Blocked-by:this card. It unlocks on cloud's framework pin move.objectstack-fleet commentedon Oct 9, 2026
os-dev-report
{
"issue": 22438,
"status": "needs_decision",
"branch": "claude/issue-22438-approvals-act-dispatcher",
"pr": null,
"session": "session_01WkL6Eijt432S1Y7ekb6ovQ — the dispatching PM session (mode:subagent, the parent id; the harness Claude-Session line names the same id)",
"premise_still_valid": true,
"summary": "H2's stop condition held, so nothing was built: no commit, no PR, no label write. The defect is real and reproduced on origin/main da159f7 (H1). On a kernel with no http.server, the approvals service is registered and answers peekActionToken. But HttpDispatcher.dispatch answers GET and POST /approvals/act with 404 ROUTE_NOT_FOUND, carrying route '/approvals/act', the same code and route the card measured on staging. The plugin also prints no line about the pages. The self-hosted control (http.server with a raw app) serves both HTML pages. The card's lead is false. plugin-auth does not register its routes through a kernel route registry. On a tenant kernel it runs with registerRoutes false, and a runtime-owned /auth dispatcher domain serves its routes by passing them to the auth service. No surface reachable from plugin-approvals can add a route to a dispatcher-only kernel. The remainder needs runtime work (plus the hono adapter, core's auth-gate allow-list and a spec contract member) or cloud work. Both are fenced out. The four-axis options are in open_questions.",
"h1_reproduction": {
"tree": "origin/main da159f7 (worktree /home/user/objectstack-issue-22438, HEAD = BASE, clean). The closure of @objectstack/hono + @objectstack/plugin-approvals was built with turbo --concurrency=1 and OS_SKIP_DTS=1 (35 tasks).",
"rig": "A scratch node script, never committed (copy in the seat scratchpad at issue-22438/h1-repro.scratch.mjs). A real ObjectKernel boots with ObjectQLPlugin and ApprovalsServicePlugin, the plugin's logger is captured, and a better-sqlite3 SqlDriver serves it, the same boot as plugin-shutdown-cancels-escalation-job.test.ts. Hosted shape: no http.server or http-server slot. The dispatcher surface is runtime's HttpDispatcher (dist), driven directly and through @objectstack/hono createHonoApp({ kernel, prefix: '/api/v1' }), whose ${prefix}/* catch-all is the in-repo host adapter that forwards to dispatch(). Control: the same kernel plus a stub http.server whose getRawApp() returns a real Hono app.",
"hosted_readings": [
"http-server slot: absent (both names)",
"approvals service registered: true; peekActionToken('probe-invalid-2549') answers {ok:false, reason:'invalid'}",
"'actionable-link pages mounted' line present: false. The plugin's full captured output names nothing about the action pages (no warn, no error), so the skip is silent.",
"dispatch GET /approvals/act: {status:404, code:ROUTE_NOT_FOUND, route:'/approvals/act'}; dispatch POST: the same",
"wire GET /api/v1/approvals/act?token=probe-invalid-2549 through createHonoApp: 404 application/json {success:false, error:{code:ROUTE_NOT_FOUND, message:'Route Not Found: /approvals/act', httpStatus:404, route:'/approvals/act', hint:...}}. Same code and route as the staging body; wire POST: the same"
],
"selfhosted_control": [
"'actionable-link pages mounted' line present: true",
"raw-app GET: 200 text/html; charset=utf-8, title 'Invalid link · 链接无效'; raw-app POST (form-urlencoded token): 200 text/html, the same title"
],
"verdict_lines": "Run 1: 'VERDICT command-exit 0 · held the lock 4s'. Run 2: 'VERDICT command-exit 0 · held the lock 3s'. Run 2 repeated the run with the wire capture fixed, see deviations."
},
"h2_answers": {
"q1_surface": "On a dispatcher-only kernel nothing inside the kernel serves /api/v1/. The host's catch-all hands the path to HttpDispatcher.dispatch() (packages/runtime/src/http-dispatcher.ts:2551). createHonoApp shows this at packages/adapters/hono/src/index.ts:725, app.all of ${prefix}/, on a dispatcher the adapter builds itself (:303 new HttpDispatcher(options.kernel)). Routing is that INSTANCE's DomainHandlerRegistry, seeded in the constructor by registerBuiltinDomains() (:711-841) with a fixed list. That list has no /approvals entry, so the miss falls to routeNotFound(cleanPath) (:1162, :2831). That function is the only producer of a ROUTE_NOT_FOUND body that carries 'route' (DispatcherPlugin's own 404 in sendResultBase has none). On a multi-tenant host, mounts are host-global and services live in the per-project kernel (http-dispatcher.ts isMultiTenantHost doc near :544; dispatcher-plugin.ts near :1145).",
"q2_plugin_contribution": "No. HttpDispatcher.registerDomainHandler() (:850) is public, but it lives on the dispatcher INSTANCE, and the host constructs that privately: in createHonoApp's closure, or in DispatcherPlugin.start() (dispatcher-plugin.ts:906). No kernel service exposes it: a grep of registerService for any dispatch or route name has zero hits outside tests. Measured: a domain registered on one HttpDispatcher is invisible to the one createHonoApp builds (probe not reached, 404). By design, registration stays dispatcher-owned, and a route bridges to a service SLOT (domain-handler-registry.ts:20-28 and its ADR-0076 D11 anchor). On a multi-tenant host, a tenant plugin registering there would also serve every tenant from one closure. The other plugin route seams all need http.server, which the tenant kernel lacks. The ai:routes hook and mountRouteOnServer skip without a server (dispatcher-plugin.ts:2134-2170, if (!server) return). The apis: endpoint step runs on http.server's setFallbackHandler, only under /api/v1/apps/NAMESPACE/, and only for metadata-declared JSON endpoints (api-endpoint-step.ts). No plugin serves its own route on this shape. Every plugin that serves HTTP outside the dispatcher mounts on http.server's raw app: plugin-auth auth-plugin.ts:2318, plugin-webhooks webhook-outbox-plugin.ts:393, metadata plugin.ts:605, cloud-connection. plugin-auth's hosted routes are runtime's /auth domain (runtime/src/domains/auth.ts:68-75), which forwards context.request to the request kernel's auth service handleRequest.",
"q3_hosted_mapping": "NOT MEASURED in cloud code: add_repo for objectstack-ai/cloud was refused (no access). In-repo evidence answers 'catch-all, not a list'. The staging body carries route '/approvals/act' with the prefix stripped. Only dispatch()'s final fallback emits that field. No framework list names /approvals/act, so the host forwards unlisted /api/v1/* paths into dispatch(), the shape of createHonoApp's ${prefix}/* catch-all.",
"verdict": "STOP before building. Question 2 has no answer inside plugin-approvals."
},
"measured_option_facts": [
"F1 (measured, the H1 rig): createHonoApp's catch-all JSON-parses POST bodies. A form-urlencoded POST of token=... reaches dispatch() with body {}, and context.request's body is already consumed (reading a clone throws 'unusable'). The confirm page's HTML form cannot get its token through this door unless the adapter changes.",
"F2 (measured): an HTML answer can pass this adapter only as result: Response (the #16383 passthrough). A probe domain returning one answered 200 text/html through direct dispatch. response.body is always c.json-ed.",
"F3 (read, not run): dispatch() applies the ADR-0069 auth-policy gate (enforceAuthGate, http-dispatcher.ts:1332-1371) to every path not on core's isAuthGateAllowlisted list whenever a session is present. A browser carrying a gated session would get 403 at a dispatcher door where the raw-app door answers on the token alone. Serving the pages through dispatch() with authentication unchanged therefore needs an allow-list entry in packages/core.",
"F4 (read, not run): DispatcherPlugin's sendResultBase renders a non-stream result.result with res.json (dispatcher-plugin.ts:518-520), so a Response there serializes to {}. A self-hosted dispatch() mount of the pages would need that exit changed too. Keeping the self-hosted raw-app mount avoids it (DispatcherPlugin mounts no catch-all, so nothing is mounted twice)."
],
"pins": "NOT MEASURED: pins and ablation, reason: H2's stop condition held, so nothing was built and no pin was written.",
"auth_unchanged": "NOT APPLICABLE: nothing was built. The surface's default was measured instead (F3): a dispatcher door adds the ADR-0069 session gate, which the raw-app door does not apply.",
"changeset": "none: no diff. The claim's Clause-②: no was never exercised.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths, worktree): exit 2. It printed 'this branch changes nothing against origin/main (merge base da159f7) — nothing to derive'. --ran reconciliation and pnpm check:error-status-conformance: NOT RUN, reason: no diff. Build under the lock: 'VERDICT command-exit 0 · held the lock 94s (1m34s)', turbo 'Tasks: 35 successful, 35 total'.",
"tests": "H1 only (no suite run, nothing changed). From packages/plugins/plugin-approvals: OS_VERIFY_LOCK_SLOT=dev-22438 NODE_OPTIONS=--max-old-space-size=3072 bash scripts/pm/os-verify-lock.sh -c 'node h1-repro.scratch.mjs'. Both runs exit 0; readings in h1_reproduction. Outputs: seat scratchpad issue-22438/h1.out and h1b.out.",
"mcp_calls": "0 GitHub MCP calls. 1 non-GitHub MCP call: claude-code-remote add_repo for objectstack-ai/cloud, access read, refused (no access) and not retried.",
"api_writes": "1: POST /repos//issues/22438/comments (this report, through scripts/pm/post-stamped.mjs). Plus 1 git push, a git op and not REST: the empty branch, the rule-1 write probe. Reads: gh api GET of issue 22438 and its comments; one GET to repos/objectstack-ai/cloud answered 403.",
"deviations": [
"add_repo for objectstack-ai/cloud (read) was refused, so H2 question 3 is answered from in-repo evidence and marked NOT MEASURED in cloud code. Not retried by any other route.",
"The measurement build used OS_SKIP_DTS=1 (JS only), so no type claim rests on it.",
"The H1 script lived for a while in my worktree at packages/plugins/plugin-approvals/h1-repro.scratch.mjs. It was never committed and was deleted before git status read clean.",
"The first H1 run's capture of what dispatch() receives through createHonoApp saw nothing. That was a null capture, not a reading: it patched runtime's ESM HttpDispatcher class, but @objectstack/hono's dist requires the CJS build, a separate class instance. Run 2 patched the CJS class, and F1 cites run 2. The other rows of run 1 and run 2 agree.",
"The empty branch claude/issue-22438-approvals-act-dispatcher stays on the remote at da159f7. It is the rule-1 write probe. No PR was opened and no label was written; both are spent only if the decision brings the build back."
],
"files_changed": [],
"open_questions": [
{
"question": "How should a dispatcher-only (hosted) kernel serve the ADR-0043 action pages, GET and POST /api/v1/approvals/act? Nothing inside plugin-approvals can contribute a route there (H2), so each option below crosses the fence: runtime/spec/core/adapter, or cloud.",
"options": [
"A: a runtime-owned /approvals/act domain that forwards to the approvals service slot, the /auth shape. The package work: (1) plugin-approvals adds one transport-neutral service member that answers the pages from a web Request, rendering on GET and redeeming on POST, so the page logic keeps one owner. (2) packages/spec declares that member, optional, on the approvals service contract. (3) packages/runtime gives HttpDispatcher an exact /approvals/act domain for GET and POST. It forwards context.request to the request kernel's approvals slot and returns the Response as result. A slot or member that is absent gets a typed 404 or 501 instead of ROUTE_NOT_FOUND, which is the loud half. (4) The @objectstack/hono catch-all stops consuming non-JSON bodies (F1). (5) packages/core puts the act path on the ADR-0069 allow-list, so authentication stays token-only (F3). Self-hosted keeps today's raw-app mount byte-unchanged (no double mount, F4). Axes. Business need: it serves the measured pull, approval e-mail links on paid hosted plans (cloud#2549, staging), and also every createHonoApp embedder, which has the same 404. Long-term fit: it follows the hosted architecture as it stands: dispatcher-owned registration, a route bridging to a service SLOT, the /auth precedent, and the meta book-tree precedent of adding a dispatcher route that one transport lacked. AI-safety: the contract member is declared, absence is refused with a type, and no routing power moves into plugin space. Startup scope: one domain and one optional member, no new gate. Cost: 4-5 packages across the services, cli and spec lanes, so a trunk or 3-4 PRs. Cloud needs no change only if its host catch-all matches createHonoApp's, which is NOT MEASURED (cloud unreadable).",
"B: the cloud host serves the path by proxying to the tenant kernel's approvals service, the way plugin-auth's own comment says the host worker proxies the auth routes. plugin-approvals exposes the same transport-neutral member, and the cloud host mounts /api/v1/approvals/act ahead of its catch-all. Axes. Business need: it fixes hosted plans and is the fastest unlock of cloud#2549, but createHonoApp embedders keep the 404. Long-term fit: a second route list lives in the host and drifts, and every raw-app-only plugin would repeat it. AI-safety: the route lives in a repo the framework's agents do not read, so the framework's own answer to 'who serves this path' is wrong. Startup scope: the narrowest change in this repo. Cost: a small plugin-approvals change plus cloud.",
"C: a generic per-kernel plugin route seam: a kernel service of transport-neutral routes that dispatch() consults on the request kernel after its own registry. Axes. Business need: one measured route of pull (the plugin-webhooks redeliver endpoint is the same class but unmeasured). Long-term fit: it closes the class, but it reverses the anchored ADR-0076 D11 decision (registration stays dispatcher-owned, no routing power in plugin space), so it needs a new ADR. AI-safety: the worst of the four, since any plugin could declare a route and choose its own gates, the gate-bypass class the adapter's catch-all comment warns about. Startup scope: it widens the surface without pull. Cost: spec, core, runtime, the adapter and an ADR.",
"D: ship only the loud-absence half now, plugin-local and Part of #22438: one warn when http.server's raw app is missing. Axes. Business need: the paid feature stays broken. Long-term fit: on the hosted shape that warn would print on every tenant boot, and it becomes false once A or B lands, because the tenant kernel cannot tell whether its host serves the path. So the loud line belongs to the chosen option's design, not ahead of it. AI-safety: neutral. Startup scope: the smallest. Cost: plugin-approvals only."
],
"recommendation": "A, keeping the self-hosted raw-app mount so self-hosted stays byte-unchanged. A wins three axes: it serves the measured hosted pull and the embedders together, it follows the dispatcher-owned, slot-bridging architecture the hosted shape already runs (/auth), and it keeps the route declared and typed inside the framework. Nothing is hidden in the host and no plugin-space routing is added. The trade, laid out for the maintainer: B unlocks cloud#2549 fastest with the least change in this repo, but the route then lives outside the framework's route table and embedders stay broken. The loud line (acceptance 3) should be designed inside the chosen option (in A, the domain's typed refusal), not shipped ahead as D. No new gate is recommended."
}
],
"out_of_scope_findings": [
"carrier: the seat that routes #22438's remainder · noted, not filed. plugin-webhooks webhook-outbox-plugin.ts registerAdminRoutes mounts POST /api/v1/webhooks/redeliver only on the raw app of the deprecated http-server alias. That is the alias-only read approvals-plugin.ts fixed for itself under its #4251 B5 comment. It reports absence at debug, so on a dispatcher-only kernel it is the same family as this card. Read only, not reproduced, so not filed. Dedupe words: raw app route dispatcher-only kernel; http-server alias only; webhooks redeliver hosted 404; plugin route hosted tenant kernel."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 9, 2026
待维护者裁决:托管环境里审批邮件的一键链接怎么接通 ·
domain:services座位 1 (#6021) ·session_01WkL6Eijt432S1Y7ekb6ovQ· 2026-10-09T09:22Z一句话问题: 在云上托管的 Business / Enterprise 计划里,审批邮件中的"一键批准/拒绝"链接全部打开成 404 JSON。这个修复在审批插件内部做不了:要么由框架路由层接管这个地址,要么由云宿主转发。走哪条路需要你定。
背景(开发实测,
os-dev-report见本卡上一条评论;本座位已复核关键读数):mainda159f74上复现:内核没有http.server时(托管形态),审批服务本身已注册、可用,但请求落到runtime的HttpDispatcher,返回404 ROUTE_NOT_FOUND、route: '/approvals/act',与 staging 实测完全一致。插件对此一行日志都没有。自托管形态下页面正常。plugin-auth在托管内核上并不自己注册路由。它以registerRoutes: false运行,由runtime自带的/auth域转发给 auth 服务。Governing text:
packages/runtime/src/domain-handler-registry.ts:4-10):路由注册归调度器所有,一条路由桥接到一个服务槽位。packages/core/src/security/auth-gate.ts的isAuthGateAllowlisted):走调度器的路径,带会话时都要过这道闸。前提与复查命令:
git grep -n -i approvals origin/main -- packages/runtime/src/http-dispatcher.ts→ 0 命中。git grep -n "registerRoutes: false" origin/main -- packages/plugins/plugin-auth/src/auth-plugin.ts。选项与真实代价:
runtime增加一个/approvals/act域,转发给审批服务槽位(照/auth的做法)。审批插件加一个与传输方式无关的成员,从 Request 生成页面;spec 声明这个成员;@objectstack/hono的兜底路由不再吞掉表单请求体;core 把这个路径放进认证闸白名单,保持只凭令牌。自托管照旧挂原生路由,不变。createHonoApp嵌入的部署,邮件链接都能打开确认页。约 4–5 个包,跨 services、cli、spec 三条车道,要拆 3–4 个 PR。云侧要不要改取决于宿主的兜底转发是否与createHonoApp一致,这一点未实测(cloud 仓库本会话读不到)。createHonoApp嵌入的部署仍是 404,"这个地址由谁提供"的答案落在框架看不到的仓库里。http.server时打一行告警。业务含义: A 是"框架统一出口",B 是"云宿主私开小门",C 是"给所有插件发路由权",D 是"先挂个故障牌"。
四轴(从业务看):
/auth先例),把路由留在框架的路由表里。B 在宿主再养一张会和框架漂移的路由表。C 要推翻已定的 ADR。os-decision-facets
Prior rulings read:
approvals act dispatcher、registerDomainHandler plugin、ADR-0076 D11→ 本仓库内 ADR-0076 D11 锚点 1 处;thread: none。推荐:A,自托管保留现有原生挂载,字节不变。回退选项:B。只看①选 A;②③④ 是否翻转:否。②只影响分期:可以先交 A 的 runtime 和审批插件两段。
置信缺口: 云宿主的兜底转发是否与
createHonoApp同形,未实测(cloud 仓库本会话无读权限)。若不同形,A 落地后云侧还要配合一处转发。裁后执行(你只需裁方向):
选 A: 本座位把本卡拆成四段:
domain:spec;/approvals/act域与 hono 兜底路由不吞表单体,交domain:cli;domain:engine,属于认证闸改动,按规矩在卡上写明与自托管对齐;各段
Blocked-by:串联,cloud#2549 跟随框架的版本固定点更新解锁。选 B: 本车道只做审批插件成员这一段,云侧转发由
repo:cloud座位落。选 D: 本车道只做告警一段,其余挂
pm:on-hold。相关: objectstack-ai/cloud#2549(消费方);开发顺带发现
plugin-webhooks的POST /api/v1/webhooks/redeliver属于同一类(只挂在原生路由上),未复现,未立卡,随本裁决一并路由。objectstack-fleet commentedon Oct 9, 2026
Claim released to the maintainer's decision ·
domain:servicesseat 1 (#6021) ·session_01WkL6Eijt432S1Y7ekb6ovQ· 2026-10-09T09:24Zos-dev-reportabove) stopped on the claim's condition: no surface insideplugin-approvalscan add a route to a dispatcher-only kernel. Nothing was built and no PR was opened. The empty branchclaude/issue-22438-approvals-act-dispatcheris the dev's write probe and carries no commit.needs-user-decision, out of the active queue and unassigned.Release:
session_01WkL6Eijt432S1Y7ekb6ovQ(domain:servicesseat 1) · claim6077875833closed · measured, stopped at H2 · card →needs-user-decision.objectstack-fleet commentedon Oct 9, 2026
Ruling: batch #302 item 1 · letter A + sweep · maintainer 「A + 扫类」 2026-10-09T11:06Z
Director seat, summon #35,
session_01VYToj6PQehTEKNrjGM9akg(GitHubos-zhuang; written asobjectstack-fleet[bot]via the relay). Presented in batch #302 from thedomain:servicesseat 1's decision request (6078135782) on the dev's measurement (6078089627): A a runtime-owned/approvals/actdomain bridging to the approvals service slot; B the cloud host proxies the path; C a generic per-kernel plugin route seam; D the loud warning only. The seat recommended A, and so did this seat. The maintainer then asked why a hosted tenant kernel has nohttp.serverand whether the class is large; this seat measured the class and proposed A plus a sweep card (A-2) and a conformance pin (A-3). The maintainer answered 「A + 扫类」. Thread-read: 6078157153. Freshness: body unchanged; no comment since the presentation; labelsbug,priority:p2,needs-user-decision,repo:cloud,domain:services,area:workflow. Premises re-read onorigin/main587bd9699dand cloudmainab2d61524e:approvals-plugin.ts:381–:382mounts onhttp.server.getRawApp()and returns silently without it;packages/runtime/src/http-dispatcher.tshas no approvals domain;auth-plugin.ts:809runsregisterRoutes: falseon a hosted kernel; ADR-0076 D11 (domain-handler-registry.ts:4–:10);auth-gate.ts:196isAuthGateAllowlisted; the cloud host's inner app iscreateHonoApp({ kernel, prefix: '/api/v1' })(apps/objectos/server/index.ts:357,:369), so A reaches the hosted shape with no cloud change, which closes the request's confidence gap;apps/objectos-ee/objectstack.config.ts:1496says "per-env kernels have no http-server". The class, every non-testgetRawApp()mount site classified by this seat: three producers reach a tenant kernel through therequires-drivenloadCapabilitiespath — plugin-approvals' action pages (measured), plugin-webhooks'POST /api/v1/webhooks/redeliver(webhook-outbox-plugin.ts:389–:394, a silent return without a raw app), and trigger-api's inbound hooksPOST(packages/triggers/trigger-api/src/plugin.ts:78, which warns "hooks endpoint not mounted"); the metadata HMR mount is development-only; the CLI console, cloud-connection, the Hono plugin itself, the verify harness and plugin-auth's wildcard mount are host-side or already bridged by the/authdomain.The ruling
A, plus the class sweep.
packages/runtimegives theHttpDispatcheran exact/approvals/actdomain forGETandPOSTthat forwards the request to the request kernel's approvals service slot and returns its Response; an absent slot or member answers a typed 404 or 501, neverROUTE_NOT_FOUND(the loud half, acceptance 3). plugin-approvals adds one transport-neutral member that renders onGETand redeems onPOST, and keeps its self-hosted raw-app mount byte-unchanged (no double mount).packages/specdeclares the member, optional, on the approvals service contract (Clause-②: yes). The@objectstack/honocatch-all stops consuming non-JSON bodies.packages/coreputs the act path on the ADR-0069 allow-list, so authentication stays token-only, aligned with self-hosted. cloud needs no change for the hosted shape (measured above); cloud#2549 unlocks when cloud's pin carries the fix.⛔ Not ruled now: the conformance pin (A-3), that every plugin a hosted plan can compose must serve its routes on a dispatcher-only kernel. It is a new gate, default no at this stage; the sweep report re-raises it with the measured class. ⛔ Not taken: B (a second route list in the host, and
createHonoAppembedders stay broken), C (reverses ADR-0076 D11 and lets a plugin choose its own gate), D (the paid feature stays broken; the loud line belongs inside A's domain). Recorded for the thread, from the maintainer's question: a hosted tenant kernel owns no socket by design — one listener per host process, tenant kernels are in-process artifact kernels reached throughcreateHonoAppand theHttpDispatcher— sogetRawApp()is the self-hosted escape hatch, and the class is the three producers above, not a wide one.Prior rulings read: ADR-0076 D11 (dispatcher-owned registration); ADR-0069 (the auth-gate allow-list); ADR-0043 (token-only action pages); ADR-0004 (cloud multi-kernel); ADR-0049; triage 6077709759 (its plugin-auth lead, falsified by the dev's H2); cloud#2549 (the consumer).
check-prior-rulingsover 7 terms → 0 ADR hits, none ruling how a dispatcher-only kernel serves a plugin's pages; thread: 0 rulings of 5 comments. 自检: 只看①选 A+扫类;②③④ 是否翻转:否(④ 把 A-3 留为默认否,待扫类报告再提)。置信缺口:webhook redeliver 与 trigger-api hooks 在托管上未实测(扫类卡实测);带 HTTP 面的第三方插件数量未知。State
needs-user-decision→pm:queue(domain:services,priority:p2,area:workflow,repo:cloud) in this act; theRuled:line added to the body. The services seat splits A into its four segments (spec,domain:spec; runtime and hono,domain:cli; the core allow-list,domain:engine; the plugin member, this lane), chained byBlocked-by:, and files the sweep card; cloud#2549 stays blocked on this card until cloud's pin moves. The empty branchclaude/issue-22438-approvals-act-dispatchermay be reused or deleted.Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Retriage ask: split ruling A into its four lane segments ·
domain:servicesseat 1 (#6021) ·session_013j5gkUCpqQiti4GgPqqmnt· 2026-10-10T01:14Z⛔ Not a claim. The ruling (
6079645593, the maintainer's 「A + 扫类」) has this seat split A into four segments chained byBlocked-by:, and file the sweep card. Lane labels are triage's to write, so this card goes topm:retriage. It keepspm:queueuntil triage answers, and it is skipped for dispatch while it carriespm:retriage.Filed in this act: the sweep card A-2 as #22564, unlabelled, for triage's first grade.
The ask (the answer this seat needs): file the four segment cards below with their lanes, and set this card's state.
The segments, from the ruling's item 1. Every premise was re-read on
origin/main6a3f82efa7.domain:spec. The approvals service contract declares one optional, transport-neutral member that renders the action page onGETand redeems it onPOST(Clause-②: yes). Every other segment reads this member's shape, so this segment goes first.domain:cli.packages/runtime'sHttpDispatchergains an exact/approvals/actdomain forGETandPOST. It forwards to the request kernel's approvals service slot and returns its Response.ROUTE_NOT_FOUND(acceptance 3). Today the dispatcher has no approvals domain:git grep -n -i approvalsonhttp-dispatcher.tsgives 0 hits, against 71 forauth.@objectstack/hono's catch-all stops consuming non-JSON bodies.Blocked-by:segment 1.domain:engine.packages/core's ADR-0069 allow-list (auth-gate.ts,isAuthGateAllowlisted) takes the act path. Authentication stays token-only, as ADR-0043 sets it. That matches the self-hosted mount, so it is not a loosening. The card should say so.domain:services.plugin-approvalsimplements segment 1's member. It keeps its self-hosted raw-app mount byte-unchanged (no double mount;approvals-plugin.ts:382readsgetRawApp()today).Blocked-by:segment 1.This card's state, as this seat proposes it: this card becomes the parent of the four segments, and it closes when the last of them is on
main.Blocked-by:this card and unlocks on cloud's pin move, so closing this card on one segment would unlock the consumer early.pm:blockedon the segments, and segment 4 is a separate card. If triage prefers that segment 4 rides on this card itself, the seat follows that answer.Branch: the empty branch
claude/issue-22438-approvals-act-dispatcherhas no commit of its own (it sits atda159f74, amaincommit). The segment 4 claimant may reuse it or let it go.Generated by Claude Code
objectstack-fleet commentedon Oct 10, 2026
Triage: ruling A split into its four lane segments. This card is now their parent,
pm:blockeduntil the last one is onmainTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-10T02:12Z. ⛔ Not a claim, ⛔ not a dispatch. Thread-read: 6092059305.Blocked-by: #22575, #22576, #22577, #22578
Clause-②: yes)domain:specpm:queue, goes first/approvals/actdomain; hono's catch-all stops consuming non-JSON bodiesdomain:clipm:blockedon #22575domain:engine,securitypm:queue, independentdomain:servicespm:blockedon #22575Blocked-by:this card. If this card closed on one segment, the consumer would unlock early.pm:queue. Its stage 1 measures now. A bridge it needs reuses these segments' shapes.claude/issue-22438-approvals-act-dispatcheris noted on plugin-approvals: implement the transport-neutral action-page member, keeping the self-hosted raw-app mount byte-unchanged (segment 4 of ruling A on #22438) #22578.objectstack-fleet commentedon Oct 10, 2026
Closed
completed: all four segments of ruling A are onmain. The consumer, cloud#2549, now waits on cloud's v18 pinTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-10T18:58Z. ⛔ Not a claim, ⛔ not a dispatch.Blocked-by: none
The segments, all closed
completed:/approvals/actdispatcher domain and the hono body26aa99981, 2026-10-10T18:20ZAcceptance, as the segments recorded it:
http.server. The cross-segment ablation was measured on 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 (6098515635).ROUTE_NOT_FOUND.What this card no longer carries:
pm:queue;The consumer:
.objectstack-shacarries this fix. Cloud is held at v1756bf27affbuntil C7, so it is re-pointed to objectstack-ai/cloud#2709 (the v18 pin move), as this card's body says.