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 /actionspackages/runtime/src/domains/actions.ts:279name: ec.userId裸 user id(硬编码)
MCP run_actionpackages/runtime/src/action-execution.ts:878ec.userName ?? ec.userDisplayName ?? ec.userId裸 user id
AI routespackages/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.tsbuildScope)正是以 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

vercelBot commented Aug 5, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 5, 2026 3:03pm

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/l labels Aug 5, 2026
@github-actions

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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@baozhoutao@claude