revert(过度矫正): 撤回"目录合不合法"的两道门槛 —— 错在投递,不在值

用户否掉了我上一步的方向:「?拒收那个目录干啥?明明是你的网关投递逻辑造成的错误」。

他是对的。/home 是**合法目录**,平台不该替用户裁定"哪个目录算工作目录":
网关地址受理处的 400、建议接口的过滤、桥侧的同类闸(revert 141003d)全部撤回。
判据随之删除 —— 判据不该给错误的实现护航(那会让错的东西看起来有保障)。

保留的是**真修复**(ad1f3f1,已在线):
- 会话解析按 (path, 别名):错投的根因是解析只看别名、path 被当提示;
- 唯一索引 (path, 别名);无 path 时"唯一才认,否则报歧义";
- 毒数据清理(/home 污染值 0|0|0)。
核心回归判据 TestFindNamedSessionForRequiresPath 保留。

仍未修(用户指的那一处):SSE 投递只按 name 广播 —— 同名不同 path 的客户端谁接到谁处理。
下一 commit:投递也带会话身份,只投给声明了该会话的客户端。
This commit is contained in:
2026-09-15 12:54:25 +08:00
parent 82c51e7a8e
commit 7e1adfd731
3 changed files with 8 additions and 88 deletions

View File

@ -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.<alias> 续谈。

View File

@ -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, ".") {

View File

@ -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)
}
}
}