Skip to content

fix(runtime): ctx.user.name 交付真实 display name —— 三条 dispatch 路径统一 user 形状 (#5372) - #5518

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-5372-action-ctx-user-name
Aug 5, 2026
Merged

fix(runtime): ctx.user.name 交付真实 display name —— 三条 dispatch 路径统一 user 形状 (#5372)#5518
baozhoutao merged 2 commits into
mainfrom
claude/issue-5372-action-ctx-user-name

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #5372

前提核验(issue 正文基于 17.0.0-rc.2 dist,行号对 src 无效)

三条路径已在 origin/main 的 src 上逐条定位,premise 成立且未漂移:

路径 src 落点(修改前) 实际交付
REST /actions packages/runtime/src/domains/actions.ts:279name: ec.userId 裸 user id(硬编码)
MCP run_action packages/runtime/src/action-execution.ts:878ec.userName ?? ec.userDisplayName ?? ec.userId 裸 user id
AI routes packages/runtime/src/domains/ai.ts:178displayName: ec.userDisplayName ?? ec.userName ?? ec.userId 裸 user id,且键名不同

全仓 grep 确认:ec.userName / ec.userDisplayName 只有读点没有赋值点,ExecutionContextSchema 也没有声明这两个键 —— 即 #4984 的"死枝 ?? 链"家族,唯一可达分支就是最后一个 id。buildActionSandboxContext(sandbox/body-runner.ts:326)确为 pass-through,名字不是在 sandbox 丢的。

顺带核出同类的第二个静默错值:AI route 的 email: ec.userEmail —— 声明的字段是 ec.email,所以该路径的 user.email 一直是 undefined。它与本单要统一的是同一个对象、同一处构造,故一并修正(未另开 issue)。

改了什么

新增唯一生产者 packages/runtime/src/security/actor-user.ts,三条路径 + AI route 的第二个生产者(dispatcher-plugin.tsresolveRequestUser)全部经它构造。

  • namesys_user.name(平台自己的 profile 显示名列,对象上 required: true),每请求解析一次:memo 以请求的 ExecutionContext 对象身份为 key(WeakMap),同一请求内 N 次 action dispatch 只读一次,且跨请求不缓存(改名下一请求即生效,无需失效钩子)。
  • 解析安静失败:查不到行 / 无引擎 / 读抛错 / name 为空白 → 回退 id。名字缺失不该让 action 失败。于是 name === id 精确地只表示"该用户没有可解析的 display name" —— 这正是下游 workaround(name !== id 即信任)得以自退的条件。
  • 读取是 system-elevated 的(与 core 的 resolveAuthzContext 自己那次 sys_user 读一致):判断"调用者是谁"不能依赖调用者对身份表的读权限。

统一形状的键集与依据(PM 授权由实测消费方决定)

先 grep 了下游读键,结论把形状锚到已有的 spec 契约而不是新造:

[ADR-0068 D1] EvalUser 是仓里声明的唯一 user-context 契约,谓词面(formula/stdlib.ts buildScope)正是以 current_user / user / ctx.user 三个别名挂同一个对象,且其上 name 的声明就是 "Display name"。也就是说 REST 的硬编码不只是与兄弟 dispatcher 不一致,它是在一个 spec 已经定义过的键上交付了别的东西。一个 action 的 visible 谓词和它的 body 并排写、都拼作 ctx.user,两者形状不同正是 ADR-0068 当初要消灭的缺陷。

所以身份内核改由 spec 自己的 createEvalUser 构造 —— id / name / email / positions / isPlatformAdmin / organizationId;在此之上保留两个 dispatch 面本来就已发布的传输键:

对所有既有读者是纯增量,未删任何键。匿名 / 自调用仍是 system principal(#2701),AI route 匿名仍返回 undefined(handler 以 user 缺席判断无调用者)—— 这两处语义不同,故不强行合一。

resolveRequestUser?? user.email 中间档被去掉:它的 display name 直接取 session 的 user.name(better-auth 对 sys_user.name 的投影,无额外读),这样 name === id 在每个生产者上含义一致;邮箱仍在自己的 email 键上。

性能实测

真实 ObjectQL + better-sqlite3(200 行 sys_user,主键读),500 次取均值:

BENCH cold=0.2239ms/dispatch memoized=0.00055ms/dispatch

即每请求首次 0.22ms,同请求内后续 dispatch 约 0.0006ms。未新增连接或缓存层。

测试

新增 packages/runtime/src/action-ctx-user-shape.test.ts(15 例),按三族组织:(REST 为主,MCP / AI 各一条同值断言,并有一条真 QuickJS sandbox body return ctx.user.name 的端到端)、反向(无 name / 空白 name / 无行 → name === id,双向闭合)、失败模式与形状(读抛错仍 200、system-elevated 读、每请求一次、三路径键集与值逐一相等)。另在 ai-request-user-capability-channel.test.ts 给第二个生产者加了形状与 fallback 断言,并把该文件里 userEmail: 这个谁也不赋值的拼写改回声明的 email:

反向验证(方向先判后跑):此处是标准 RED,不是 #5018 的 inverted —— 被删的枝就是错值的生产者,不存在 canonical-first 的 ?? 链。预判"把 REST 硬编码还原 → REST 值断言与形状断言转红";实跑 7 failed / 8 passed,红的正是 REST 值、sandbox 端到端、形状两条、system-elevated 读、每请求一次、匿名 principal 的 systemPermissions。值得如实记一笔:留绿的 4 条全是"无 name 时回退 id"方向 —— 单靠那一侧抓不到这个回归,双向断言里的另一侧才是探针。

命令与结果:

  • pnpm --filter @objectstack/runtime testTest Files 94 passed (94) / Tests 1362 passed (1362)
  • pnpm --filter @objectstack/runtime typechecktsc --noEmit,无输出
  • pnpm --filter @objectstack/runtime build → CJS/ESM/DTS build success
  • 相邻闸门:check:authz-resolver ✓、check:role-word ✓、check:adr-anchors ✓、check:route-envelope ✓、check:org-identifier ✓、check:slot-lookup ✓、check-nul-bytes OK

文件面

只动 packages/runtime/src/** 与该包测试 + 一个 changeset。未触 packages/rest(#5487/#5462 领地),未触 hotcrm 仓,未动 content/docs/releases/

🤖 Generated with Claude Code

https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh


Generated by Claude Code

…patch paths (#5372)

The REST /actions dispatcher hardcoded `name: ec.userId` — a declared key
delivering a plausible WRONG value, undetectable by any consumer-side
fallback. The MCP and AI-route dispatchers read `ec.userName` /
`ec.userDisplayName`, neither declared on ExecutionContextSchema nor ever
assigned, so their `??` chains also landed on the id; the AI route additionally
read `ec.userEmail` (declared field: `ec.email`), leaving `user.email`
permanently undefined.

One shared producer (security/actor-user.ts) now builds the envelope for all
three paths plus the AI routes' second producer. `name` comes from
`sys_user.name`, resolved once per request (memo keyed on the ExecutionContext;
~0.22ms per cold read against real SQLite) and falling back to the id quietly —
so `name === id` means exactly "no display name", which is what lets an
app-side workaround self-retire.

[ADR-0068 D1] The identity core is built through the spec's own
`createEvalUser`, the same factory the predicate surface mounts under
`ctx.user`, so a body and the predicate beside it see one shape. The transport
keys (`userId`, `displayName`, `roles`, `permissions`, `systemPermissions`) sit
on top; nothing was removed.

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 3:03pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l 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.

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