Skip to content

fix(runtime): carry the capability channel onto AI-route req.user (#4705) - #4712

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-4705-ai-route-system-permissions
Aug 2, 2026
Merged

fix(runtime): carry the capability channel onto AI-route req.user (#4705)#4712
os-zhuang merged 1 commit into
mainfrom
claude/issue-4705-ai-route-system-permissions

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #4705

问题

packages/runtime/src/domains/ai.tshandleAIRequest() 从 ExecutionContext 构造 req.user 时,只拷了权限集名通道,没拷 capability 通道。这两类东西在平台上是两个字段,形状像但语义完全不同:

ExecutionContext 字段 承载内容 来源
permissions 权限集名admin_full_access / organization_admin / member_default)+ 合成的 ai_seat core/src/security/resolve-authz-context.tsgrants.permissions.push(ps.name)
systemPermissions capabilitymanage_metadatastudio.accesssetup.access …),即用户所有已解析权限集 systemPermissions[] 的并集 同文件的 grants.systemPermissions.push(p) 循环

结果是 /ai/* 成了全仓唯一无法做 capability gate 的路由域:平台其他每一处 capability 判断读的都是 systemPermissionsdomains/meta.tsmanage_metadata 门禁、action-execution.tsrest/src/rest-server.ts),而 AI 路由的 handler 只拿得到集名,所以任何写在 req.user.permissions 上的 capability 判断恒为 false——那不是「收紧」,是把路由对所有人(含平台管理员)关死。这正是 objectstack-ai/cloud#1015 想给 POST /api/v1/ai/tools/:toolName/execute 加门禁时撞上的阻塞项(该路由在默认配置下允许任何登录用户执行任意已注册工具,含 create_object / apply_blueprint / create_seed)。

改动

只做透传,不含任何策略。 本仓没有任何路由因此开始判 capability,route.permissions 那套(声明了从不执行)一行未动。

  1. domains/ai.tsreq.user 增加 systemPermissions,与 roles / permissions 完全同款的 fail-closed 缺省:非数组(或 ExecutionContext 上根本没有该字段——它在 ExecutionContextSchema 上是 optional)一律变成 [],绝不是 undefined

    systemPermissions: Array.isArray(ec.systemPermissions) ? ec.systemPermissions : [],

    两条通道并排拷贝、绝不合并。把任一条压进另一条会在「看起来修好了」的同时污染 permissions 的每一个既有消费者。

  2. dispatcher-plugin.tsresolveRequestUser(另一条挂载路径,服务于逐路由的具体挂载)——它没有 ExecutionContext 可取,继续保持 fail-closed,没有为它编造任何 capability 来源。这里只是把 systemPermissions: [] 显式写出来而不是省略,让 AI 路由 req.user 的两个生产者在形状上一致:消费者看到的永远是「不持有任何 capability」,而不是一边 [] 一边 undefined——否则消费者迟早要自己写个 ?? [] 去抹平两者,那正是 Prime Directive Add comprehensive test suite for Zod schema validation #12 要避免的宽容 fallback。

关于「这个字段在真实请求里到底有没有值」

issue 里最该被证伪的一点,先说结论:

  • /api/v1/ai/* 的真实请求走的是 dispatcher-pluginregisterAIRoutes 挂的 method-wildcard,经 dispatcher.dispatch()domains/ai.ts,而 dispatch() 在此之前已经通过 timedResolveExecutionContextresolveExecutionContext 填好了 context.executionContext;后者在 security/resolve-execution-context.tsauthz.permissionsauthz.systemPermissions 两个字段并列挂上。
  • 这些 wildcard 在同一个 start()先于 mountAiRoute 的具体路由注册,所以答请求的是带 ExecutionContext 的那条路径。这一点已被测试钉住(见下),避免将来有人调换注册顺序、把 /ai/* 悄悄交还给没有 capability 的那个生产者。
  • capability 确实到得了平台管理员手上:plugin-securityadmin_full_accesssystemPermissions 里声明了 manage_metadata / studio.access / setup.access,经 resolve-authz-context.ts 汇聚进 grants.systemPermissions。所以 cloud#1015 的门禁按平台既有契约判时,管理员会通过而不是被 403。

测试

新增 packages/runtime/src/domains/ai-request-user-capability-channel.test.ts,6 个用例:

  • ExecutionContext 上持有的 capability 到达 req.user.systemPermissions
  • permissions逐字是集名 + ai_seatroles 仍是 positions——并断言两条通道互不含有对方的元素(capability 读不到 permissions 上,集名 / ai_seat 也读不到 systemPermissions 上);
  • 不持有任何 capability 的调用方拿到 [],不是 undefined
  • ec.systemPermissions 是畸形值(如字符串)时同样 fail-closed 成 []
  • resolveRequestUser 这条路径同样给出 systemPermissions: []
  • /ai/* wildcard 先于具体 AI 路由挂载(上面那条可达性论证的回归钉子)。

没有本 PR 的改动时,这个测试文件 6 个用例里会挂 5 个(唯一通过的是挂载顺序那条,它本就不依赖本改动)。实测输出:

AssertionError: expected undefined to deeply equal []
 ❯ src/domains/ai-request-user-capability-channel.test.ts:155:40
Test Files  1 failed (1)
     Tests  5 failed | 1 passed (6)

加上改动后的完整门禁(均在本地实跑):

pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2
  Test Files  80 passed (80)
       Tests  1092 passed (1092)

npx turbo run typecheck --filter=@objectstack/runtime... --concurrency=2
  Tasks:    44 successful, 44 total

npx eslint packages/runtime/src/domains/ai.ts packages/runtime/src/dispatcher-plugin.ts \
           packages/runtime/src/domains/ai-request-user-capability-channel.test.ts --no-inline-config
  (无输出,exit 0)

pnpm check:type-check-coverage
  OK — 60/77 workspace packages type-checked …

@objectstack/runtime 本身在 #4311 的 DEBT 账本里没有 typecheck 脚本,所以编译层证据取自它的 build:tsup 的 DTS 步骤对 src 跑完整 tscDTS ⚡️ Build success

影响面

  • 用户可见(AI 路由 handler 的 req.user 多了一个字段),已附 changeset .changeset/ai-route-user-system-permissions.md
  • 未改 content/docs/releases/**
  • 解除 objectstack-ai/cloud#1015 的阻塞:那边的门禁可以照平台既有 systemPermissions 契约写,不需要在消费侧做任何宽容处理。

🤖 Generated with Claude Code

https://claude.ai/code/session_019TYoxKa8yFLiDDh7tkBqtu


Generated by Claude Code

)

`domains/ai.ts` built `req.user` from the request's ExecutionContext but
copied only the permission-SET-NAME channel (`ec.permissions`, which also
carries the synthesized `ai_seat`), never the CAPABILITY channel
(`ec.systemPermissions` — `manage_metadata`, `studio.access`, …).

That made `/ai/*` the one route domain where a capability gate could not be
written: every other surface tests `systemPermissions` (domains/meta.ts's
`manage_metadata` gate, action-execution.ts, rest-server.ts), so the same
test written against an AI route's `req.user.permissions` is permanently
false — it would close the route on platform admins, not tighten it. It is
the direct blocker for the capability gate on
`POST /api/v1/ai/tools/:toolName/execute` (objectstack-ai/cloud#1015).

Transport only: the field is copied through with the fail-closed default the
neighbouring fields use (non-array or absent -> `[]`, never `undefined`), the
two channels stay side by side and unmerged, and no route in this repo gates
on it. `route.permissions` is untouched.

`dispatcher-plugin`'s `resolveRequestUser` — the other producer of an
AI-route `req.user`, backing the concrete per-route mounts — has no
ExecutionContext to read and stays capability-less on purpose; it now says so
in the same shape (`systemPermissions: []`) so a consumer never has to tell
`undefined` from `[]`.

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

vercel Bot commented Aug 2, 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 2, 2026 8:16pm

Request Review

@github-actions

github-actions Bot commented Aug 2, 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 size/m documentation Improvements or additions to documentation tests tooling labels Aug 2, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 2, 2026 20:44
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 2, 2026
Merged via the queue into main with commit 7e7a605 Aug 2, 2026
21 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-4705-ai-route-system-permissions branch August 2, 2026 20:49
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AI 路由的 req.user 丢失 capability 通道 —— ec.systemPermissions 从不透传,AI 域无法做任何 capability gate

2 participants