revert(过度矫正): 撤回"目录合不合法"的两道门槛 —— 错在投递,不在值
用户否掉了我上一步的方向:「?拒收那个目录干啥?明明是你的网关投递逻辑造成的错误」。 他是对的。/home 是**合法目录**,平台不该替用户裁定"哪个目录算工作目录": 网关地址受理处的 400、建议接口的过滤、桥侧的同类闸(revert 141003d)全部撤回。 判据随之删除 —— 判据不该给错误的实现护航(那会让错的东西看起来有保障)。 保留的是**真修复**(ad1f3f1,已在线): - 会话解析按 (path, 别名):错投的根因是解析只看别名、path 被当提示; - 唯一索引 (path, 别名);无 path 时"唯一才认,否则报歧义"; - 毒数据清理(/home 污染值 0|0|0)。 核心回归判据 TestFindNamedSessionForRequiresPath 保留。 仍未修(用户指的那一处):SSE 投递只按 name 广播 —— 同名不同 path 的客户端谁接到谁处理。 下一 commit:投递也带会话身份,只投给声明了该会话的客户端。
This commit is contained in:
@ -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> 续谈。
|
||||
|
||||
@ -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, ".") {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user