diff --git a/server/internal/handler/mail.go b/server/internal/handler/mail.go index 4757390..e08d177 100644 --- a/server/internal/handler/mail.go +++ b/server/internal/handler/mail.go @@ -86,22 +86,6 @@ 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. 续谈。 diff --git a/server/internal/repo/repo.go b/server/internal/repo/repo.go index c824f87..f0afd10 100644 --- a/server/internal/repo/repo.go +++ b/server/internal/repo/repo.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "os" "path/filepath" "strings" "time" @@ -1469,55 +1468,21 @@ func filterWorkspaces(in []string) []string { return in } out := make([]string, 0, len(in)) + seen := map[string]bool{} for _, p := range in { - if !IsPlausibleWorkspace(p) { + // 只归一化去重。**不做"这目录像不像工作目录"的判断**: + // 用户 2026-09-15 否掉了那个方向(「拒收那个目录干啥?明明是你的网关投递 + // 逻辑造成的错误」)—— /home 是合法目录,错在投递,不在值。 + c := filepath.Clean(p) + if seen[c] { continue } - if isAncestorOfOther(filepath.Clean(p), in) { - continue - } - out = append(out, filepath.Clean(p)) + seen[c] = true + out = append(out, c) } return out } -// IsPlausibleWorkspace 报告地址里的 path 位**是否可能是一个工作目录**。 -// -// 规则与 filterWorkspaces **同源**(一处定义、两处使用):建议接口用它筛候选, -// 地址受理用它拒收 —— 否则"建议里没有的值"仍能被人手写进来,污染从另一条路回来。 -// -// 1. 绝对路径; -// 2. 任何一段以 `.` 开头 → 不是(缓存/会话存储,如 /root/.pi/mail-sessions/x); -// 3. 进程用户的家目录 → 不是(那不是"干活的地方")。 -// -// 刻意**不检查是否存在**:注册时自报的目录可能还没建(/tmp/remotebot-ws 那种)。 -// "是不是另一个候选的祖先"也不在这里判:那要看候选集,属于**目录建议**的取舍 -// (/home/program 单独出现时是合法路径,只是不该被优先推荐)。 -func IsPlausibleWorkspace(p string) bool { - p = strings.TrimSpace(p) - if p == "" || !filepath.IsAbs(p) { - return false - } - clean := filepath.Clean(p) - // 2. **深度至少 2**:工作目录是树里的一个**具体**位置,不是顶层挂载点。 - // `/home`、`/root`、`/tmp`、`/opt` 本身都不是"人/agent 干活的地方", - // 它们是容器的容器。实测的污染值 `/home` 正是靠这条被挡住 - // —— 它在旧规则下是"合法绝对路径",因为 /home 并不是 root 用户的家目录。 - // (判据第一次跑就抓到了这个漏洞:IsPlausibleWorkspace("/home") = true ✗) - if strings.Count(strings.Trim(clean, "/"), "/") < 1 { - return false - } - if hasHiddenSegment(clean) { - return false - } - if home, err := os.UserHomeDir(); err == nil && home != "" { - if clean == filepath.Clean(home) { - return false - } - } - return true -} - func hasHiddenSegment(p string) bool { for _, seg := range strings.Split(strings.Trim(p, "/"), "/") { if len(seg) > 1 && strings.HasPrefix(seg, ".") { diff --git a/server/internal/repo/session_identity_test.go b/server/internal/repo/session_identity_test.go index b93cfea..c069d94 100644 --- a/server/internal/repo/session_identity_test.go +++ b/server/internal/repo/session_identity_test.go @@ -82,32 +82,3 @@ func TestFindNamedSessionForRequiresPath(t *testing.T) { 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) - } - } -}