Skip to content

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

@yinlianghui

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 name key declared and delivered wrong — it is assigned the raw user id:

  • @objectstack/runtime dist (17.0.0-rc.2) REST action path, dist/index.js:5397-5399: { id: ec.userId, name: ec.userId, … }
  • MCP dispatch path disagrees in the same file (dist/index.js:1776): ec.userName ?? ec.userDisplayName ?? ec.userId — but nothing in the installed packages ever assigns ec.userName or ec.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.
  • The AI-route builder disagrees with both: dist/index.js:5264 exposes displayName, not name.

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.name reads 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.name is the platform's own profile display-name column (plugin-auth SYS_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.name consistently on all three paths (REST, MCP, AI-route). Downstream app-side workarounds (hotcrm now resolves via a sys_user read inside the action body, with a comment naming this issue's condition) can then be deleted — the hotcrm workaround deliberately trusts ctx.user.name the moment it differs from the id, so it self-retires when this lands.

Acceptance

  • An action body dispatched over REST reads ctx.user.name as the acting user's display name (not the id);
  • MCP and AI-route paths agree on the same user shape;
  • a pin test covers the REST path (the one that was hardcoded).

Activity

  1. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    分诊(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

  2. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    Contributor

    车道排程注记(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

  3. self-assigned this
    on Aug 5, 2026
  4. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    Contributor

    认领: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

  5. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    Contributor

    实施完成,draft PR:#5518(分支 claude/issue-5372-action-ctx-user-name,会话 session_016FNvXhtSdnEGEfLEsMmvxh)。

    premise 核验结论:成立,未漂移。 issue 正文的 dist 行号已逐条落到 origin/main 的 src:REST domains/actions.ts:279(name: ec.userId 硬编码)、MCP action-execution.ts:878、AI route domains/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.ts buildScope)正是以 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

  6. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    Contributor

    验收(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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions