用户 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(下一步);线上的库要等下一次启动才跑迁移。
114 lines
4.8 KiB
Go
114 lines
4.8 KiB
Go
package repo
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"testing"
|
||
)
|
||
|
||
/*
|
||
会话身份 = **path + 别名**(用户 2026-09-15 订正)。
|
||
|
||
原话:「agent 平台的 session 是和 path 绑定的,path+session 才能指定到准确的 agent,
|
||
而授权也是对 session 授权,而不是整个 agent」。
|
||
|
||
这条判据直接对着当天那个**严重错投**:工作区在 TrueAgent 的 pi 给 dsh 发信,信却进了
|
||
workspace=/home 的会话/客户端。根因是解析只按别名(`WHERE session_alias = $1`),
|
||
path 被当成了可选提示 —— 于是两条不相干的线索能落进同一条会话。
|
||
|
||
判据两侧都写(只验一侧会挡住"退回按别名找"这种假修复):
|
||
- 给了 **正确** path → 命中;
|
||
- 给了 **另一个** path(而别名确实存在于别处)→ 必须 ErrSessionNotFound,
|
||
**绝不能**命中共名的那一条(这一条是本次 bug 的回归判据);
|
||
- 不给 path 且别名唯一 → 命中(兼容 `name@.别名` 的旧地址);
|
||
- 不给 path 且同名两条 → ErrSessionAmbiguous(不许掷骰子)。
|
||
*/
|
||
func TestFindNamedSessionForRequiresPath(t *testing.T) {
|
||
setupTestDB(t)
|
||
ctx := context.Background()
|
||
seedAgent(t, "dsh", 20)
|
||
|
||
alias := "鸿蒙客户端与-WebUI-界面对齐"
|
||
a := alias
|
||
idA, err := CreateSession(ctx, &a, "pi", "在 repo 里干活", "/home/program/agentmail")
|
||
if err != nil {
|
||
t.Fatalf("建会话 A: %v", err)
|
||
}
|
||
seedMailForSession(t, idA, "pi", "dsh", "/home/program/agentmail")
|
||
|
||
b := alias
|
||
idB, err := CreateSession(ctx, &b, "pi", "在 /home 里那一条(污染值)", "/home")
|
||
if err != nil {
|
||
t.Fatalf("建会话 B(同名不同 path): %v", err)
|
||
}
|
||
seedMailForSession(t, idB, "pi", "dsh", "/home")
|
||
|
||
// ① 正确 path → 命中 A
|
||
got, err := FindNamedSessionFor(ctx, "dsh", "/home/program/agentmail", alias)
|
||
if err != nil || got != idA {
|
||
t.Fatalf("正确 path 应命中 A:got=%v err=%v", got, err)
|
||
}
|
||
|
||
// ② ★ 错 path(/home)但别名相同 → 必须命中 B,**不是** A;
|
||
// 而如果那个 path 根本没有会话 → NotFound,绝不退回命中 A。
|
||
got, err = FindNamedSessionFor(ctx, "dsh", "/tmp/别的地方", alias)
|
||
if !errors.Is(err, ErrSessionNotFound) {
|
||
t.Fatalf("错 path 必须 ErrSessionNotFound(不许退回按别名找),实际 got=%v err=%v", got, err)
|
||
}
|
||
got, err = FindNamedSessionFor(ctx, "dsh", "/home", alias)
|
||
if err != nil || got != idB {
|
||
t.Fatalf("path=/home 应命中 B(同名但不同身份):got=%v err=%v", got, err)
|
||
}
|
||
|
||
// ③ 不给 path:别名不唯一 → 歧义,不许猜
|
||
if _, err := FindNamedSessionFor(ctx, "dsh", "", alias); !errors.Is(err, ErrSessionAmbiguous) {
|
||
t.Fatalf("同名两条 + 无 path 必须 ErrSessionAmbiguous,实际 %v", err)
|
||
}
|
||
|
||
// ④ 不给 path 且唯一 → 命中(兼容旧地址 `name@.别名`)
|
||
uniq := "只有一条的别名"
|
||
u := uniq
|
||
idU, err := CreateSession(ctx, &u, "pi", "独一份", "/home/program/agentmail")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
seedMailForSession(t, idU, "pi", "dsh", "/home/program/agentmail")
|
||
if got, err := FindNamedSessionFor(ctx, "dsh", "", uniq); err != nil || got != idU {
|
||
t.Fatalf("唯一别名 + 无 path 应命中:got=%v err=%v", got, err)
|
||
}
|
||
|
||
// ⑤ 没参与过的名字不该命中(地址解析的另一半判据)
|
||
if _, err := FindNamedSessionFor(ctx, "zcode", "/home/program/agentmail", alias); !errors.Is(err, ErrSessionNotFound) {
|
||
t.Fatalf("没参与过这条线索的名字不该命中,实际 %v", err)
|
||
}
|
||
}
|
||
|
||
// IsPlausibleWorkspace:地址受理与"目录建议"共用的一条规则。
|
||
//
|
||
// 它存在的意义是**堵住源头**:一个 `/home` 这样的值进了地址,会话的 workspace 就错了,
|
||
// 桥会把 worker 起在那里(实测 cwd=/home),那个 worker 发的信继续带 /home,
|
||
// 建议接口再把它学成第一条候选 —— 自我强化。所以这里两侧都钉:
|
||
// 实测污染过的形状必须是 false,真实工作目录必须是 true。
|
||
func TestIsPlausibleWorkspace(t *testing.T) {
|
||
cases := []struct {
|
||
in string
|
||
want bool
|
||
why string
|
||
}{
|
||
{"/home", false, "一切的家目录(实测污染值)"},
|
||
{"/root", false, "进程家目录(实测污染值)"},
|
||
{"/root/.pi/mail-sessions/8f056b73", false, "会话存储目录(实测污染值)"},
|
||
{"/home/program/agentmail/../llmsproxy", true, "含 .. 但归一化后是具体目录"},
|
||
{"rel/path", false, "相对路径不是地址里的 path"},
|
||
{"", false, "空"},
|
||
{"/home/program/agentmail", true, "真实工作目录"},
|
||
{"/home/newqqagent", true, "homeagent 的真实工作目录"},
|
||
{"/tmp/remotebot-ws", true, "尚未创建的注册目录也要放行(不做存在性检查)"},
|
||
}
|
||
for _, c := range cases {
|
||
if got := IsPlausibleWorkspace(c.in); got != c.want {
|
||
t.Errorf("IsPlausibleWorkspace(%q) = %v, want %v(%s)", c.in, got, c.want, c.why)
|
||
}
|
||
}
|
||
}
|