fix(session): 会话身份 = (path, 别名) —— 修用户报的错投,并堵住 /home 这类毒 path

用户 2026-09-15 的订正:「agent 平台的 session 是和 path 绑定的,path+session 才能
指定到准确的 agent,而授权也是对 session 授权,而不是整个 agent」;随后报了一个
严重错投:「工作区在 TrueAgent 的 pi 客户端,发送邮件给 dsh 被投递给了工作区在 home
的客户端」。

根因是四处叠加(都有实测证据):
1. 解析只按别名:`WHERE session_alias = $1`,path 被当可选提示 ⇒ 两条不相干的
   线索能落进同一会话;
2. 别名全局唯一 ⇒ path 在解析时冗余,进一步被忽略;
3. 桥按来信的 to_workspace 起 worker(实测日志 `新建 pi 会话 …(cwd=/home)`);
4. 投递(SSE)只按 name 广播。

本提交改 1/2 + 堵源头:
- 解析按 (path, 别名):给了 path 必须两者同时命中,对不上就是 ErrSessionNotFound
  (**绝不**退回按别名找);没给 path 则要求该别名唯一,多条时 ErrSessionAmbiguous
  (不许掷骰子);
- 唯一索引从 `(session_alias)` 改成 `(COALESCE(workspace,''), session_alias)`
  (两方言 + 显式 DROP 旧索引;EnsureSessionAlias 靠唯一索引判撞名,自动变成按 path);
- 新 IsPlausibleWorkspace(与目录建议**同源**一条规则):地址受理处拒收不像工作目录的
  path(`/home`、`/root`、`~/.pi/mail-sessions/…`、顶层挂载点),并给出可执行文案;
- 建议接口的候选过滤改用同一条规则(只留具体目录;祖先容器让位给更具体的候选)。

判据:新增/改写 4 组,全部做变异验证 —— 三条核心变异(解析退回按别名找 / 歧义时
掷骰子 / 去掉「深度≥2」)全部变红。过程中判据还抓到自己一个漏洞:
IsPlausibleWorkspace("/home") 在旧规则下是 true(/home 不是 root 的家目录)。

★ 语义变化(会让旧测试红):不同 path 下的同名别名现在是两条独立会话,不再"撞名"。
TestAdoptHandlesAliasCollision 已按新模型重写为两侧(同 path 才加后缀)。

未做:桥侧仍按来信 to_workspace 起 cwd(下一步);线上的库要等下一次启动才跑迁移。
This commit is contained in:
2026-09-15 12:20:28 +08:00
parent ee3ada2740
commit ad1f3f14c3
7 changed files with 460 additions and 37 deletions

View File

@ -86,6 +86,22 @@ func resolveTarget(r *http.Request, addr models.Address, replyTo, fromAgent, sub
return mail.SessionID, &replyID, false, nil
}
// 地址里的 path 必须**像一个工作目录**(用户 2026-09-15 报的严重 bug)。
//
// 实情:客户端把收件人的 path 填成了 `/home`,于是链条一路跑通:
// 会话 workspace=/home → 桥按来信的 to_workspace 起 worker(实测 cwd=/home)
// → 那个 worker 发的信继续带 /home → 建议接口把 /home 学成第一条候选 → 自我强化。
// 后果不只是地址难看:两条不相干的线索会落进同一条会话、同一个 pi 会话文件。
//
// 拒收而不是"悄悄改成默认目录":path 是会话身份的一半,猜一个等于把信
// 放进另一条会话里 —— 那正是要修的毛病。文案给出可执行的下一步。
if addr.Path != "" && !repo.IsPlausibleWorkspace(addr.Path) {
return uuid.Nil, nil, false, errBadRequest(fmt.Sprintf(
"地址里的 path 不像工作目录:%q。地址形如 name@/绝对/路径.<会话别名>;"+
"新建会话可写 name@<工作目录>.new,人类回信可省略 path(name@.<别名>)。",
addr.Path))
}
switch addr.Mode() {
case models.SessionNew:
// 新建会话:若调用方给了别名,当场命名,之后即可用 name@path.<alias> 续谈。
@ -149,6 +165,13 @@ func resolveTarget(r *http.Request, addr models.Address, replyTo, fromAgent, sub
repo.TouchSession(r.Context(), id)
return id, nil, false, nil
}
// 同名别名在多个工作目录下都存在、而地址里又没写 path:不能猜一条,
// 要让发信方补 path(见 repo.ErrSessionAmbiguous 的注释)。
if errors.Is(err, repo.ErrSessionAmbiguous) {
return uuid.Nil, nil, false, errBadRequest(fmt.Sprintf(
"别名 %q 在多个工作目录下都存在,无法确定是哪一条:请在地址里写明 path(name@/路径.%s)。",
addr.Session, addr.Session))
}
if !errors.Is(err, repo.ErrSessionNotFound) {
return uuid.Nil, nil, false, err
}