用户 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(下一步);线上的库要等下一次启动才跑迁移。
88 lines
3.4 KiB
Go
88 lines
3.4 KiB
Go
package repo
|
||
|
||
import (
|
||
"reflect"
|
||
"testing"
|
||
)
|
||
|
||
/*
|
||
SuggestPaths 的候选过滤(2026-09-15 用户报的「莫名其妙的 pi@/home 会话」)。
|
||
|
||
背景是一整条**自增强环**:建议列表是从历史数据学的(mails.to_workspace + 平台心跳 +
|
||
注册自报),而历史里混进了「进程恰好所在的目录」——`/root`、`/home`、`/home/program`、
|
||
`/root/.pi/mail-sessions/<uuid>`。用户点了第一条建议 → 发信给 `zcode@/home` →
|
||
那条会话的 workspace 成了 `/home` → 之后这条线索里所有参与方都显示 `xxx@/home`,
|
||
pi 桥还会**真把 worker 起在 /home**(实测日志 `新建 pi 会话 …(cwd=/home)`),
|
||
而沙箱 rw 只有 /home/program/agentmail ⇒ 那个 worker 连文件都写不了。
|
||
|
||
判据两侧都写:
|
||
- 坏值必须被剔掉(否则环继续跑);
|
||
- 好值必须保留,包括**还没创建的目录**(注册时自报的 /tmp/remotebot-ws 那种)
|
||
—— 如果实现顺手加上存在性检查,这一条会红。
|
||
*/
|
||
func TestFilterWorkspaces(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
in []string
|
||
want []string
|
||
}{
|
||
{
|
||
name: "剔掉进程家目录、家目录本身、父目录、隐藏段(实测到的污染形状)",
|
||
in: []string{
|
||
"/root", // 进程家目录
|
||
"/home", // 一切的家目录
|
||
"/home/program", // 只是容器
|
||
"/root/.pi/mail-sessions/8f056b73", // pi 的会话存储(隐藏段)
|
||
"/home/program/agentmail", // ← 唯一像工作目录的
|
||
},
|
||
want: []string{"/home/program/agentmail"},
|
||
},
|
||
{
|
||
name: "两个互不为祖先的项目目录都要保留",
|
||
in: []string{"/home/program/agentmail", "/home/program/llmsproxy"},
|
||
want: []string{"/home/program/agentmail", "/home/program/llmsproxy"},
|
||
},
|
||
{
|
||
name: "还没创建的目录不因为'不存在'被剔(存在性检查是过度过滤)",
|
||
in: []string{"/tmp/remotebot-ws", "/home/program/agentmail"},
|
||
want: []string{"/tmp/remotebot-ws", "/home/program/agentmail"},
|
||
},
|
||
{
|
||
name: "相对路径不是工作目录地址",
|
||
in: []string{"rel/path", "/home/program/agentmail"},
|
||
want: []string{"/home/program/agentmail"},
|
||
},
|
||
{
|
||
name: "尾斜杠 / 中间 . 段都做 Clean(不是当成新候选)",
|
||
in: []string{"/home/program/agentmail/"},
|
||
want: []string{"/home/program/agentmail"},
|
||
},
|
||
{
|
||
// 取舍:rule 4 让**更具体**的目录胜出。父子都是工作目录时父目录会被剔掉 ——
|
||
// 这是为治本(`/home`、`/home/program` 那种容器)付出的代价:宁可少给
|
||
// 一条候选(用户仍可手写),也不能把容器当工作目录(那会让 worker 起在
|
||
// 一个连文件都写不了的目录里)。这条判据把代价钉住,避免以后有人
|
||
// "顺手放宽"却没人知道为什么。
|
||
name: "父子都是候选时只留更具体的那个(已知取舍)",
|
||
in: []string{"/home/program/agentmail/./", "/home/program/agentmail/x"},
|
||
want: []string{"/home/program/agentmail/x"},
|
||
},
|
||
{
|
||
name: "空输入原样返回(不放大 nil/空切片差异)",
|
||
in: nil,
|
||
want: nil,
|
||
},
|
||
}
|
||
for _, c := range cases {
|
||
t.Run(c.name, func(t *testing.T) {
|
||
got := filterWorkspaces(c.in)
|
||
if len(got) == 0 && len(c.want) == 0 {
|
||
return
|
||
}
|
||
if !reflect.DeepEqual(got, c.want) {
|
||
t.Errorf("filterWorkspaces(%v)\n got %v\nwant %v", c.in, got, c.want)
|
||
}
|
||
})
|
||
}
|
||
}
|