Repository navigation
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
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Oct 9, 2026 objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsTriage: 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.- Why p2: on every hosted plan that carries approvals (Business and Enterprise), each approval e-mail's one-click link lands on raw
404JSON. That is a paid feature broken on the hosted shape. It was measured on staging (objectstack-ai/cloud#2549, reading6076690389). - Direction: the card's own acceptance.
- Register
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. - ⛔ No silent skip: a host that cannot mount the pages hears it once, loudly.
- Self-hosted behaviour is unchanged.
- Register
- Pins:
- on a kernel with no
http.server,GETrenders the ADR-0043 confirm page andPOSTrecords the decision; - control: a kernel with
http.serverbehaves as today.
- on a kernel with no
- Consumer: cloud#2549 unlocks on the install face, when cloud's
.objectstack-shacarries the fix (cloud#2709 or a later move).
- Why p2: on every hosted plan that carries approvals (Business and Enterprise), each approval e-mail's one-click link lands on raw
- addedarea:workflowApprovals and automation — the work that runs without a person driving itApprovals and automation — the work that runs without a person driving itpriority:p2Medium: important, M3Medium: important, M3and removed
on Oct 9, 2026 objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsClaim: PM loop round 4 · 2026-10-09T09:06Z
Session:session_01WkL6Eijt432S1Y7ekb6ovQ
Account:os-bill(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-22438-approvals-act-dispatcher
Worktree:objectstack-issue-22438
Domain:domain:services
Seat: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.- If a plugin can contribute such a route from its own package, the build stays in
plugin-approvals. - If it needs a
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.- Tests in
plugin-approvals:- pins on a kernel with no
http.server:GETrenders the confirm page, andPOSTrecords the decision; - controls: a kernel with
http.serveris unchanged; an invalid token gets today's result page.
- pins on a kernel with no
- The route ledger and its census, only if the gate demands them, repaired with the generator only.
- One
@objectstack/plugin-approvalschangeset.
Exclusions:
- ⛔ The pages' authentication is unchanged: the action token alone, as ADR-0043 sets it. No session requirement is added and none is removed, and no other approvals route is opened.
- ⛔ No
packages/spec, noruntime, no objectstack-ai/cloud change (stop and report instead). - ⛔ Nothing else in
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- A route that answers today on a self-hosted kernel now answers on a dispatcher-only kernel too, with the same authentication. No accept-set changes on any host that serves it today.
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 404
Thread-read: 6077709759
Serial constraints cleared: - No open PR touches
plugin-approvals(15 open PRs read, at their file lists). - objectstack-ai/cloud#2549 is the consumer,
Blocked-by:this card. It unlocks on cloud's framework pin move.
- If a plugin can contribute such a route from its own package, the build stays in
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsos-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 commented
on Oct 9, 2026 ContributorAuthorMore actions待维护者裁决:托管环境里审批邮件的一键链接怎么接通 ·
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 服务。 - 插件能够到的接口里,没有一个能往只有调度器的内核里加路由:调度器的域注册表属于宿主私有实例,没有暴露成内核服务。这是 ADR-0076 D11 的既定设计,注册权在调度器一方,不下放给插件。
Governing text:
- ADR-0076 D11(
packages/runtime/src/domain-handler-registry.ts:4-10):路由注册归调度器所有,一条路由桥接到一个服务槽位。 - ADR-0069 认证策略闸(
packages/core/src/security/auth-gate.ts的isAuthGateAllowlisted):走调度器的路径,带会话时都要过这道闸。 - ADR-0043:审批动作页仅凭邮件里的令牌访问。
前提与复查命令:
- 调度器内置域里没有审批:
git grep -n -i approvals origin/main -- packages/runtime/src/http-dispatcher.ts→ 0 命中。 - 托管形态下 auth 走 runtime 的域:
git grep -n "registerRoutes: false" origin/main -- packages/plugins/plugin-auth/src/auth-plugin.ts。
选项与真实代价:
选项 做什么 客户能感知到的后果 A runtime增加一个/approvals/act域,转发给审批服务槽位(照/auth的做法)。审批插件加一个与传输方式无关的成员,从 Request 生成页面;spec 声明这个成员;@objectstack/hono的兜底路由不再吞掉表单请求体;core 把这个路径放进认证闸白名单,保持只凭令牌。自托管照旧挂原生路由,不变。托管环境和所有用 createHonoApp嵌入的部署,邮件链接都能打开确认页。约 4–5 个包,跨 services、cli、spec 三条车道,要拆 3–4 个 PR。云侧要不要改取决于宿主的兜底转发是否与createHonoApp一致,这一点未实测(cloud 仓库本会话读不到)。B 云宿主在兜底转发之前单独挂这个地址,代理给租户内核的审批服务(照宿主现在转发 auth 的做法)。本仓库只给审批插件加同一个成员。 解锁 cloud#2549 最快,本仓库改动最小。但用 createHonoApp嵌入的部署仍是 404,"这个地址由谁提供"的答案落在框架看不到的仓库里。C 做一个通用接口:每个内核暴露一张插件路由表,调度器在自身注册表之后查询它。 一次堵住整类问题(webhooks 的重投接口同属这一类,未实测),但推翻 ADR-0076 D11,需要新 ADR。任何插件都能声明路由并自选认证闸,绕过认证闸的风险最大。 D 现在只补"响亮告警":内核缺 http.server时打一行告警。付费功能仍然坏着。托管环境每次启动都会打这行;等 A 或 B 落地后,这行告警反而不对了。 业务含义: A 是"框架统一出口",B 是"云宿主私开小门",C 是"给所有插件发路由权",D 是"先挂个故障牌"。
四轴(从业务看):
- 实际业务需求: 今天碰到的是托管付费计划上每一封审批邮件(staging 已实测)。A 同时覆盖嵌入部署,B 只覆盖托管,C 的额外拉动未实测,D 解决不了问题。
- 项目长远合理性: A 顺着托管形态已有的架构(调度器统一注册、路由桥接服务槽位,有
/auth先例),把路由留在框架的路由表里。B 在宿主再养一张会和框架漂移的路由表。C 要推翻已定的 ADR。 - 防 AI 犯错: 出错时谁看到什么?A 由 spec 声明成员,缺失时返回有类型的 404 或 501,是响亮拒绝。B 的路由在框架代理读不到的仓库里,框架对"谁提供这个地址"的回答是错的,属于静默错误。C 让任何插件都能自选认证闸,最差。
- 创业阶段不扩散: A 加一个域和一个可选成员,不加门禁。认证闸白名单多一条,与自托管现状对齐,不是放宽。B 在本仓库改动最小。C 无拉动的扩面。
os-decision-facets
- ① 项目长远合理性:A 缩小特例,路由留在框架内;B 增生宿主侧特例;C 增生通用契约。
- ② 实际业务拉动:托管付费计划的审批邮件(cloud#2549 实测);嵌入部署同样 404。
- ③ 防 AI 犯错:A 有声明,缺失时响亮拒绝;B 的路由在框架之外;C 是自由结构。
- ④ 创业阶段不扩散:A 只加一个可选成员和一个域;C 违反;D 不解决问题。
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: 本座位把本卡拆成四段:
- spec 段:声明审批服务成员,交
domain:spec; - runtime 段:
/approvals/act域与 hono 兜底路由不吞表单体,交domain:cli; - core 段:认证闸白名单加这个路径,交
domain:engine,属于认证闸改动,按规矩在卡上写明与自托管对齐; - 本车道段:审批插件成员与响亮缺失提示。
各段
Blocked-by:串联,cloud#2549 跟随框架的版本固定点更新解锁。 - spec 段:声明审批服务成员,交
-
选 B: 本车道只做审批插件成员这一段,云侧转发由
repo:cloud座位落。 -
选 D: 本车道只做告警一段,其余挂
pm:on-hold。
相关: objectstack-ai/cloud#2549(消费方);开发顺带发现
plugin-webhooks的POST /api/v1/webhooks/redeliver属于同一类(只挂在原生路由上),未复现,未立卡,随本裁决一并路由。- 在
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsClaim released to the maintainer's decision ·
domain:servicesseat 1 (#6021) ·session_01WkL6Eijt432S1Y7ekb6ovQ· 2026-10-09T09:24Z- The dev's measurement round (
os-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. - The decision analysis (in Chinese, per the decision-card convention) is the comment above. The card is now
needs-user-decision, out of the active queue and unassigned. - After the ruling, this seat splits the card per the chosen option and re-claims its own lane's stage.
Release:
session_01WkL6Eijt432S1Y7ekb6ovQ(domain:servicesseat 1) · claim6077875833closed · measured, stopped at H2 · card →needs-user-decision.- The dev's measurement round (
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsRuling: 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.
- A as presented.
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. - The sweep (A-2). One card the services seat files: on a dispatcher-only kernel, measure plugin-webhooks' redeliver endpoint and trigger-api's inbound hooks endpoint; each that answers 404 is bridged the same way (a dispatcher domain to the service slot, the same typed refusal, the same pins); the sweep report names any further member of the class it finds.
⛔ 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
- A as presented.
- addedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchand removed
on Oct 9, 2026 objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsRetriage 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.- spec →
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. - runtime and hono →
domain:cli.packages/runtime'sHttpDispatchergains an exact/approvals/actdomain forGETandPOST. It forwards to the request kernel's approvals service slot and returns its Response.- An absent slot or member answers a typed 404 or 501, never
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.
- core →
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. - plugin member →
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.- objectstack-ai/cloud#2549 is
Blocked-by:this card and unlocks on cloud's pin move, so closing this card on one segment would unlock the consumer early. - So this card goes
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
- spec →
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsTriage: 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
Segment Card Lane State 1. spec: the optional, transport-neutral action-page member ( Clause-②: yes)#22575 domain:specpm:queue, goes first2. runtime: the exact /approvals/actdomain; hono's catch-all stops consuming non-JSON bodies#22576 domain:clipm:blockedon #225753. core: the exact act path on the ADR-0069 allow-list, token-only, as self-hosted #22577 domain:engine,securitypm:queue, independent4. plugin-approvals: the member; the raw-app mount byte-unchanged #22578 domain:servicespm:blockedon #22575- Segment 4 is its own card, as the seat proposed. objectstack-ai/cloud#2549 is
Blocked-by:this card. If this card closed on one segment, the consumer would unlock early. - Segment 3 is a security boundary, and the maintainer ruled it. Its card holds it to the exact path and to authentication equal to the self-hosted mount's. Anything wider stops and goes to the maintainer.
- The sweep 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 (A-2) is graded
pm:queue. Its stage 1 measures now. A bridge it needs reuses these segments' shapes. - The empty branch
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.
- Segment 4 is its own card, as the seat proposed. objectstack-ai/cloud#2549 is
- added and removedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Oct 10, 2026 objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsClosed
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: -
Acceptance, as the segments recorded it:
- 1, the hosted shape: segments 2 and 4 pin it on a kernel with no
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). - 2, self-hosted unchanged: segment 4 leaves the raw-app mount byte-unchanged.
- 3, no silent skip: an absent slot or member answers a typed 404 or 501, never
ROUTE_NOT_FOUND.
- 1, the hosted shape: segments 2 and 4 pin it on a kernel with no
-
What this card no longer carries:
- the sweep (A-2), 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, which this round moves to
pm:queue; - the conformance pin (A-3), which is unruled and re-raised by that sweep's report.
- the sweep (A-2), 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, which this round moves to
-
The consumer:
- objectstack-ai/cloud#2549 unlocks only when cloud's
.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. - Its acceptance stays the maintainer's staging reading of a real approval e-mail link.
- objectstack-ai/cloud#2549 unlocks only when cloud's
-
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#1seat (sessionsession_01WVbr5J6u8BHh8EyFtcWciH) from objectstack-ai/cloud#2549. Reader: the seat that ownsplugin-approvals(triage routes it). The consumer, cloud#2549, waits on this card through aBlocked-by:line and unlocks when cloud's framework pin carries the fix.Defect
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.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.6076690389):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"}.SLA escalation scan scheduled) is present, so the plugin loaded.plugin-authdoes). The approvals action pages should go the same way, so every host serves them.Acceptance
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.http.server(self-hosted), behaviour is unchanged.Consumer side
cloud#2549 goes
pm:blockedon this card. It unlocks when cloud's.objectstack-shacarries 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