fix(runtime): carry the capability channel onto AI-route req.user (#4705) - #4712
Merged
Merged
Conversation
) `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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 2, 2026 20:44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4705
问题
packages/runtime/src/domains/ai.ts的handleAIRequest()从 ExecutionContext 构造req.user时,只拷了权限集名通道,没拷 capability 通道。这两类东西在平台上是两个字段,形状像但语义完全不同:permissionsadmin_full_access/organization_admin/member_default)+ 合成的ai_seatcore/src/security/resolve-authz-context.ts的grants.permissions.push(ps.name)systemPermissionsmanage_metadata、studio.access、setup.access…),即用户所有已解析权限集systemPermissions[]的并集grants.systemPermissions.push(p)循环结果是
/ai/*成了全仓唯一无法做 capability gate 的路由域:平台其他每一处 capability 判断读的都是systemPermissions(domains/meta.ts的manage_metadata门禁、action-execution.ts、rest/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那套(声明了从不执行)一行未动。domains/ai.ts—req.user增加systemPermissions,与roles/permissions完全同款的 fail-closed 缺省:非数组(或 ExecutionContext 上根本没有该字段——它在ExecutionContextSchema上是 optional)一律变成[],绝不是undefined。两条通道并排拷贝、绝不合并。把任一条压进另一条会在「看起来修好了」的同时污染
permissions的每一个既有消费者。dispatcher-plugin.ts的resolveRequestUser(另一条挂载路径,服务于逐路由的具体挂载)——它没有 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-plugin里registerAIRoutes挂的 method-wildcard,经dispatcher.dispatch()→domains/ai.ts,而dispatch()在此之前已经通过timedResolveExecutionContext→resolveExecutionContext填好了context.executionContext;后者在security/resolve-execution-context.ts把authz.permissions与authz.systemPermissions两个字段并列挂上。start()里先于mountAiRoute的具体路由注册,所以答请求的是带 ExecutionContext 的那条路径。这一点已被测试钉住(见下),避免将来有人调换注册顺序、把/ai/*悄悄交还给没有 capability 的那个生产者。plugin-security的admin_full_access在systemPermissions里声明了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 个用例:req.user.systemPermissions;permissions仍逐字是集名 +ai_seat,roles仍是 positions——并断言两条通道互不含有对方的元素(capability 读不到permissions上,集名 /ai_seat也读不到systemPermissions上);[],不是undefined;ec.systemPermissions是畸形值(如字符串)时同样 fail-closed 成[];resolveRequestUser这条路径同样给出systemPermissions: [];/ai/*wildcard 先于具体 AI 路由挂载(上面那条可达性论证的回归钉子)。没有本 PR 的改动时,这个测试文件 6 个用例里会挂 5 个(唯一通过的是挂载顺序那条,它本就不依赖本改动)。实测输出:
加上改动后的完整门禁(均在本地实跑):
@objectstack/runtime本身在#4311的 DEBT 账本里没有typecheck脚本,所以编译层证据取自它的build:tsup 的 DTS 步骤对 src 跑完整tsc,DTS ⚡️ Build success。影响面
req.user多了一个字段),已附 changeset.changeset/ai-route-user-system-permissions.md。content/docs/releases/**。systemPermissions契约写,不需要在消费侧做任何宽容处理。🤖 Generated with Claude Code
https://claude.ai/code/session_019TYoxKa8yFLiDDh7tkBqtu
Generated by Claude Code