Skip to content

fix(runtime): deny anonymous callers on /actions and /automation (#5519) - #5569

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-5519-actions-anonymous-gate
Aug 5, 2026
Merged

fix(runtime): deny anonymous callers on /actions and /automation (#5519)#5569
baozhoutao merged 1 commit into
mainfrom
claude/issue-5519-actions-anonymous-gate

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #5519

前提复核(先证后改,rule 6)

分诊席 15:54Z 核过一次;我在动手时刻的 origin/main(9894a72) 上重新复现,结论:前提成立,且比原单记录的更广

复现方式是真机等价的 harness:bootStack(showcaseStack, { automation: true, extraPlugins: [ConnectorRest/OpenApi/Mcp] }) —— 真 kernel、真 plugin-auth/plugin-security、真 Hono server、真端口、真 socket。全程不带任何凭证:

请求(匿名) 修复前 证据
POST /actions/showcase_task/showcase_mark_done/:id 200 {"ok":true,"id":"1XIijpV1PUjdN7ZB"} script body 的 ctx.api.object('showcase_task').update({done:true,progress:100}) 真的执行了(isSystem:true 提权)
POST /automation/showcase_reassign_wizard/trigger 200 {"status":"paused","runId":"run_7de908b7-..."} 匿名启动了真实 flow run
GET /automation 200 返回全量 flow 清单
DELETE /automation/showcase_inquiry_janitor 200 {"deleted":true} 匿名注销了一条已注册 flow —— 原单未提到的破坏性写
对照:GET /data/showcase_task 401 同一进程,数据面基线成立

最后一行是本单判 p0 的核心:同一个进程里两套注册路径,只有 rest 那套设了门

门的落点

两处,均只在 packages/runtime/**,复用 @objectstack/coreshouldDenyAnonymous(本体语义不改):

  1. packages/runtime/src/domains/actions.tshandleActionsRequest第一条语句,在 405 之前。匿名调用者只得到 auth 基线,不泄露路由形状,与 /data 一致。
  2. packages/runtime/src/domains/automation.tshandleAutomationRequest 顶部,整域而非逐路由,且在 isServiceServeable 探测之前(否则 501 与 401 的差异会泄露该部署是否挂了 automation)。

信封与 domains/ai.ts:127domains/meta.ts:59domains/security.ts:92 完全一致:401 + UNAUTHENTICATED + Authentication is required to access this endpoint.

requiredPermissions(ADR-0066 D4)、ai.exposed、ADR-0104 参数契约语义一动不动,只是排在匿名门之后。

内部路径测绘(边界 1:内部/系统派发不许破)

测绘方法是枚举 handleActionsRequest / handleAutomationRequest全部调用者,不是读注释:

grep handleActionsRequest|handleAutomationRequest  ->  只有 http-dispatcher.ts 的两个 thin delegate
grep dispatch('POST', '/actions|/automation')      ->  只有 dispatcher-plugin.ts 的 HTTP 路由注册

结论:这两个 handler 是纯 HTTP seam,没有任何内部派发路径经过它们,逐条钉住:

内部路径 实际入口 是否受影响
MCP run_action action-execution.invokeBusinessAction(domains/mcp.ts 直接接线) 否。它的边界是 ai.exposed + requiredPermissions
声明式 endpoint 执行器(#5040 E5) buildAutomationContext + IAutomationService.execute,挂在 transport 的 fallback seam,dispatch() 刻意看不到(#5090) 否。它有自己的 authRequired 门(endpoint-policy.ts ②),authRequired: false 的公开 endpoint 仍然公开
flow 内部 / 记录变更 / 定时触发 引擎内部,根本不说 HTTP
系统上下文 isSystem: true 就在这个 seam 上 照常通过(且 isSystem 永远不可由 wire 设置)

上述每一条都有对应用例(见下),不是口头断言。

反向验证(方向先判后跑)

预判:摘掉门后,匿名用例转红,收到的状态码应为修复前的答案(已派发的 200、非 POST 的 405、gated action 的 403、无 automation 服务的 501);已认证/系统/requiredPermissions/内部路径用例两种情况下都绿。

实跑:

  • handler 级:12 failed | 13 passed,红的收到 200 / 405 / 403 / 501 —— 与预判一致。
  • socket 级:7 failed | 2 passed,7 条全部收到 200 —— 每一条已挂载的路由在修复前都真的为匿名调用者派发了。

一处偏差,如实申报:第一版 object-less(global)用例复用了 crm_contact 上的声明,摘门后收到 404(ADR-0110 D3 的「无声明」出口)而非 200 —— 也就是说它的 executeAction 未被调用 断言是因为本来就不会派发而通过的,即 #5046 记录的「verdict 1 -> 0,断言因空而绿」。按 fixture 三分法这属于 replace wholesale:换成真正 object-less 的独立声明,并补一条已认证对照用例证明该 URL 确实会派发。改后摘门收到 200,断言才真正有牙。

测试

新增两个文件,分层是刻意的:

既有用例的三分法处置(52 例受影响,全部逐条判,无批量重拼):

  • Re-spell(48):主题是「哪条路径打到哪个 service 方法」的路由用例,只是顺带匿名({ request: {} } 是能编译的最小上下文)。给它们一个 session,让每个文件继续测自己名字上的东西。
  • Replace wholesale(4):主题就是匿名路径的用例。#2701 的「匿名 -> system principal」在 HTTP 上已不可达,若只改期望值,断言会读一个从未发生的调用undefined —— 空绿,比红更糟。改为:system principal 形状用自调用 isSystem 上下文钉住(这是唯一仍能无 userId 到达 body 的形态),匿名那半则明写成 401 + executeAction 未被调用。ADR-0066 D4 那条 403 -> 401 是本次唯一可见的顺序变化,注释写明了原因(旧的 403 描述了错误的问题,且暗示 session 存在)。

消费半径扫描(不按被改的包扫):枚举 /actions/automation 的全部 caller 与 fixture ——

packages/qa/dogfood   85 files / 506 passed   (真 showcase/CRM 启动,含 action-params-contract、flow-runas、flow-durable-suspend 等全部 flow 触发用例;无一条匿名驱动这两个面)
packages/cli          82 files / 807 passed
packages/adapters/hono                        (mock dispatcher,不受影响)

实跑记录

pnpm --filter @objectstack/runtime test    ->  Test Files 96 passed (96) | Tests 1397 passed (1397)
pnpm --filter @objectstack/runtime typecheck -> tsc --noEmit,无输出(绿)
pnpm --filter @objectstack/dogfood test    ->  Test Files 85 passed | 1 skipped (86) | Tests 506 passed | 3 skipped (509)
pnpm --filter @objectstack/cli test        ->  Test Files 82 passed (82) | Tests 807 passed (807)
node scripts/check-nul-bytes.mjs           ->  OK (scanned 5496 tracked text files; no raw ASCII control bytes)

改动前基线已单独确认:把门改成 if (false) 后,受影响的 6 个既有文件 371 passed (371) —— 52 例失败全部归因于本次改动,main 上无既有红。

行为变化(升级须知,已写进 changeset)

之前拿到 200(或 gated action 的 403、405、501)的未认证调用,现在一律 401。若某部署依赖匿名调用 action / flow,受支持的替代是 authRequired: false 的声明式 endpoint、public-form grant 或 share-link token —— 而不是匿名 /actions POST。

自限

给 review 的一个取舍(不阻塞本 PR)

packages/qa/dogfood/test/authz-conformance.matrix.tsshowcase-anonymous-deny-surfaces.dogfood.test.ts 列为 #2567「匿名姿态跨面统一」这条主张的 proof 工件,而本单恰恰是这两个面破了该主张。真机证据我已取得(见上表),但按派发的文件面自限没有落成常驻用例。本 PR 用 runtime 内的真 socket 集成用例满足了「boot the real composition」的要求;是否再把 /actions + /automation 补进那个 proof 工件,建议单独立单处理。


🤖 Generated with Claude Code

https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh


Generated by Claude Code

The dispatcher's `/actions/*` and `/automation/*` routes carried no anonymity
check, while `@objectstack/rest`'s `/data` and the dispatcher's own `/meta`,
`/ai` and `/security` have denied anonymous callers 401 UNAUTHENTICATED since
#3963 made that a platform promise (the `api.requireAuth` opt-out is a
tombstone). Two registration paths, one gated.

`/actions` was the expensive half: a script action's body runs with
`isSystem: true` forced on (buildActionExecutionContext), so an unauthenticated
POST bought an RLS/FLS-bypassing SYSTEM write. The only gate ahead of it was
ADR-0066 D4's requiredPermissions, which allows every action declaring none.

Measured on a real showcase boot before the fix, with no credential:
  POST /actions/showcase_task/showcase_mark_done/:id -> 200 {ok:true}
  POST /automation/showcase_reassign_wizard/trigger  -> 200 {runId: run_...}
  GET  /automation                                   -> 200 (full inventory)
  DELETE /automation/showcase_inquiry_janitor        -> 200 {deleted:true}
...while /data on the same process answered 401.

Both domains now call the shared `shouldDenyAnonymous` decision before anything
dispatches, in the same envelope every other seam returns. Finer authorization
is unchanged below the floor. Internal dispatch paths never enter these HTTP
handlers (MCP run_action, the declarative endpoint executor, engine-internal
triggers), and `isSystem` contexts pass untouched.

Tests: a handler-level file pinning the decision and a real-socket integration
file pinning that the MOUNTED routes reach the gated handler. Collateral in
existing suites was triaged per case: route-behaviour tests that were only
incidentally anonymous gained a session; the four whose SUBJECT was the
anonymous path were replaced rather than re-spelled, because their assertions
would otherwise have read off a call that never happens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 5, 2026 6:46pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime.

21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/kernel/cluster.mdx (via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx (via @objectstack/runtime)
  • content/docs/releases/v17.mdx (via @objectstack/runtime)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added the size/l label Aug 5, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review August 5, 2026 18:50
@baozhoutao
baozhoutao enabled auto-merge August 5, 2026 18:50
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit 5aaa6fc Aug 5, 2026
24 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-5519-actions-anonymous-gate branch August 5, 2026 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants