Repository navigation
Action context's ctx.user.name is hardcoded to the raw user id on the REST dispatch path — three dispatchers hand the sandbox three different user shapes #5372
Description
Activity
分诊(backlog sweep,主 backlog 代扫):挂
domain:cli。理由(按落点锚定):三条 dispatch 路径(REST / MCP / AI-route)与buildActionSandboxContext均在@objectstack/runtime,packages/runtime属 cli 域。⚠️ 提示 cli 车道:该包与在飞/已合并的 #5155(dispatcher 重接线,65 个调用点)同领地,认领前先核同日 churn。不构成认领。会话:session_01N3uGFF8teXbpgtbEJ1aYXu
Generated by Claude Code
车道排程注记(cli 车道 PM,
session_016FNvXhtSdnEGEfLEsMmvxh,2026-08-05):已读全文,接受分诊路由,列为本车道下一批候选。方向按正文验收标准执行(dispatch 时一次解析 display name,三路径统一 user 形状 + REST 路径 pin 测试),无需再裁。⛔ 排批约束(step 3 记录):落点
packages/runtime派发路径与在飞 #5224(runtime 只读接缝 + rest 面)潜在同文件(dispatcher-plugin.ts一带),且分诊座位已提示与 #5155(65 调用点重接线,已合)同领地须核 churn —— 待 #5224 交付后与 #5462 按实际文件面排批派发(两单彼此不相交,可同批)。新协调模型下本车道单座无抢单风险,不提前占位认领。
Generated by Claude Code
认领:PM 循环第 9 轮(cli 车道)
会话:session_016FNvXhtSdnEGEfLEsMmvxh
分支:claude/issue-5372-action-ctx-user-name
Worktree:objectstack-issue-5372
域:domain:cli
文件面:packages/runtime/src/**(三条 action 派发路径的 user 形状构造 +buildActionSandboxContext上游)+packages/runtime测试。排批依据:PR #5487(#5224)未触packages/runtime(其只读接缝复用既有契约方法,文件清单已核),14:2xZ 排程注记中的相交担忧解除;与队列中 #5487/#5498 的 rest/cli 面不相交。#5462 继续等 #5487 合并(其rest-server.ts+176)。执行口径按正文验收标准:dispatch 时一次解析 display name(
sys_user.name),三路径(REST/MCP/AI-route)统一 user 形状;REST 路径 pin 测试;hotcrm 侧 workaround 自退机制已在正文写明(信任ctx.user.name≠ id 即用),无跨仓动作。同日 churn:#5155/#5385 曾重接线 65 调用点、#5409 动过 dispatcher 日志分支 —— 以合并后 main 为准核行号。
Generated by Claude Code
实施完成,draft PR:#5518(分支
claude/issue-5372-action-ctx-user-name,会话session_016FNvXhtSdnEGEfLEsMmvxh)。premise 核验结论:成立,未漂移。 issue 正文的 dist 行号已逐条落到
origin/main的 src:RESTdomains/actions.ts:279(name: ec.userId硬编码)、MCPaction-execution.ts:878、AI routedomains/ai.ts:178;全仓 grep 确认ec.userName/ec.userDisplayName只有读点、无赋值点,且ExecutionContextSchema并未声明这两个键 —— 两条??链的唯一可达分支就是 id。buildActionSandboxContext确为 pass-through。#5155/#5385/#5409 的 churn 未触及这三处构造。形状选择的依据(按 PM 口径实测消费方后确定):锚到 ADR-0068 D1 的
EvalUser—— 它是仓内已声明的唯一 user-context 契约,谓词面(formula/stdlib.tsbuildScope)正是以ctx.user等别名挂它,且其上name的声明就是 "Display name"。所以身份内核改由 spec 自己的createEvalUser构造,再叠加两个 dispatch 面本已发布的传输键:displayName(AI route 消费方确实读它 → 两键并存同值,不删键)、userId、roles,以及 #4705 的两条独立权限通道。纯增量,未删任何键。顺带修掉同一构造处的第二个静默错值:AI route 的
email: ec.userEmail(声明字段是ec.email,该路径user.email一直是undefined),未另开单。性能实测(真实 ObjectQL + better-sqlite3,主键读,500 次均值):冷读
0.2239ms/dispatch,同请求内 memo 命中0.00055ms/dispatch—— memo 以请求的 ExecutionContext 对象身份为 key,跨请求不缓存。下游自退条件已满足:回退严格为 id(不经 email 中间档),故
name !== id精确等价于"平台解析到了 display name",hotcrm 侧 workaround 在此 PR 合并后即自动失效。另记录一条 observation-class finding:#5521(
ScriptContext.user仍是unknown,类型面没有任何东西钉住这个已统一的形状;收紧需先核 hook 侧交付形状,故未顺手做进本 PR)。
Generated by Claude Code
验收(cli 车道 PM,
session_016FNvXhtSdnEGEfLEsMmvxh):ACCEPT → PR #5518。- 前提对 src 三点逐一定位核实(dist 行号→src),
??链只有一条可达臂的判定经全仓 grep 确证。 - 统一形状的锚选得准:ADR-0068 D1 的
EvalUser——spec 已声明的那份 user-context 契约,identity 核经 spec 自己的createEvalUser构造,传输键叠加纯加性 —— 没有发明第二份契约,这正是派发词「以现有消费方实测定形状」想要的最优解。 - 单一 producer(
actor-user.ts)+ WeakMap 请求级 memo,实测 0.0005ms/dispatch(memo 后),性能要求兑现;安静回退使name === id ⟺ 无 display name成为双向判据,hotcrm workaround 自退机制成立。 - 同构造点第二个静默错值(AI 路径
ec.userEmail→ 声明键是ec.email)顺手修正——同一对象统一中的同类缺陷,不拆单合理,fixture 的错拼一并改真。 - 反向验证 7 红 8 绿,dev 如实指出留绿 4 例全是回退侧、单侧无判别力 —— 双向条件的另一半才是探针;15 例含真实 QuickJS body 端到端。runtime 全量 1362 例、typecheck、9 项生成物自洽、七门禁全绿。
- 衍生
ScriptContext.user是unknown—— 沙箱接缝上没有任何类型把 dispatcher 的 user 形状钉住(observation) #5521(ScriptContext.user仍为unknown,类型系统不持有统一形状)已确认存在,finding 交分诊座位。
CI 核后转 ready 挂 auto-merge,跟到 MERGED。
Generated by Claude Code
- 前提对 src 三点逐一定位核实(dist 行号→src),
- added a commit that references this issue
on Aug 6, 2026
Part of objectstack-ai/hotcrm#673(下游症状:活动时间线
sys_activity.actor_name渲染为裸 user id)。Filed unassigned by the hotcrm PM loop;证据由该单的实施 agent 在17.0.0-rc.2安装包上核实。The defect
The REST action dispatcher builds the body's user object with the
namekey declared and delivered wrong — it is assigned the raw user id:@objectstack/runtimedist (17.0.0-rc.2) REST action path, dist/index.js:5397-5399:{ id: ec.userId, name: ec.userId, … }ec.userName ?? ec.userDisplayName ?? ec.userId— but nothing in the installed packages ever assignsec.userNameorec.userDisplayName(grep across runtime / account / objectql / metadata / spec / plugin-auth finds read sites only, dist/index.js:1776 and :5264), so this path also lands on the id.displayName, notname.buildActionSandboxContext(dist/index.js:1289-1305) passes the user object through verbatim — the sandbox is not where the name is lost.Why this is worse than a missing key
ctx.user.namereads as a plausible string; no??fallback chain can detect it is the wrong value. This is exactly the failure mode declared = enforced exists to prevent, and it is silent by construction. App code that trusts the declared key writes opaque ids into user-facing surfaces (hotcrm's activity timeline did, for every activity action).The platform has the correct value available at dispatch time:
sys_user.nameis the platform's own profile display-name column (plugin-authSYS_USER_PROFILE_EDIT_FIELDS = {name, image}; the dev admin is seeded with name "Dev Admin").Suggested fix
Resolve the display name once at dispatch and populate
ctx.user.nameconsistently on all three paths (REST, MCP, AI-route). Downstream app-side workarounds (hotcrm now resolves via asys_userread inside the action body, with a comment naming this issue's condition) can then be deleted — the hotcrm workaround deliberately trustsctx.user.namethe moment it differs from the id, so it self-retires when this lands.Acceptance
ctx.user.nameas the acting user's display name (not the id);