package tools import ( "context" "os" "path/filepath" "strings" "testing" ) func TestCheckPath(t *testing.T) { root := t.TempDir() SetAllowedRoots(root) defer SetAllowedRoots() // 根内:直接命中 if err := checkPath(filepath.Join(root, "a.txt")); err != nil { t.Fatalf("inside root rejected: %v", err) } // 根外:拒绝 if err := checkPath("/etc/passwd"); err == nil { t.Fatal("outside root accepted") } // 根内不存在的新文件:父目录解析路径 if err := checkPath(filepath.Join(root, "sub", "new.txt")); err != nil { t.Fatalf("new file under root rejected: %v", err) } // 符号链接越界:指向根外的链接应被解析后拒绝 outside := t.TempDir() if err := os.Symlink(outside, filepath.Join(root, "evil")); err != nil { t.Skip("symlink not permitted:", err) } err := checkPath(filepath.Join(root, "evil", "secret.txt")) if err == nil || !strings.Contains(err.Error(), "outside allowed roots") { t.Fatalf("symlink escape not detected: %v", err) } } func TestBuiltinToolsRespectFence(t *testing.T) { root := t.TempDir() SetAllowedRoots(root) defer SetAllowedRoots() ctx := context.Background() reg := NewRegistry() for _, tl := range Builtin() { if err := reg.Register(tl); err != nil { t.Fatal(err) } } // 根外写入被拒 write, _ := reg.Get("write") if _, err := write.Run(ctx, `{"path":"/etc/autogo-evil.txt","content":"x"}`); err == nil { t.Fatal("write outside fence accepted") } // 根内写入放行 inside := filepath.Join(root, "ok.txt") res, err := write.Run(ctx, `{"path":"`+inside+`","content":"hi"}`) if err != nil { t.Fatalf("write inside fence rejected: %v", err) } if !strings.Contains(res.Text, "wrote") { t.Fatalf("unexpected: %q", res.Text) } // read / ls / grep 同样受约束 read, _ := reg.Get("read") if _, err := read.Run(ctx, `{"path":"/etc/passwd"}`); err == nil { t.Fatal("read outside fence accepted") } ls, _ := reg.Get("ls") if _, err := ls.Run(ctx, `{"path":"/etc"}`); err == nil { t.Fatal("ls outside fence accepted") } grep, _ := reg.Get("grep") if _, err := grep.Run(ctx, `{"pattern":"x","path":"/etc"}`); err == nil { t.Fatal("grep outside fence accepted") } } // TestCheckPathMemoryDirReachable:记忆目录必须与工作区/落盘目录一起进允许根, // 否则模型无法 read 记忆文件自查与清理(read 走 checkPath)。 func TestCheckPathMemoryDirReachable(t *testing.T) { root := t.TempDir() mem := filepath.Join(root, "memory") SetAllowedRoots(root, mem) defer SetAllowedRoots() if err := checkPath(filepath.Join(mem, "global.md")); err != nil { t.Fatalf("memory file inside allowed root rejected: %v", err) } // 同目录下的项目记忆文件同样可达。 if err := checkPath(filepath.Join(mem, "AutoGo-1a2b3c4d.md")); err != nil { t.Fatalf("project memory file rejected: %v", err) } // 但记忆目录之外的路径仍然拒绝——加根不等于放宽其它地方。 if err := checkPath("/etc/passwd"); err == nil { t.Fatal("unrelated outside path accepted") } } func TestAddRemoveAllowedRoot(t *testing.T) { base := t.TempDir() SetAllowedRoots(base) defer SetAllowedRoots() // 追加:目录必须存在 if err := AddAllowedRoot(filepath.Join(base, "nope"), ModeRW); err == nil { t.Fatal("nonexistent dir accepted") } extra := t.TempDir() if err := AddAllowedRoot(extra, ModeRW); err != nil { t.Fatalf("AddAllowedRoot: %v", err) } if err := checkPath(filepath.Join(extra, "a.txt")); err != nil { t.Fatalf("imported root rejected: %v", err) } // 重复导入:更新模式而不是重复追加 if err := AddAllowedRoot(extra, ModeRO); err != nil { t.Fatalf("re-add: %v", err) } if n := len(AllowedRootDetails()); n != 2 { t.Fatalf("re-add duplicated root: %d roots", n) } // ro 模式:读放行、写拒绝 if err := checkPathMode(filepath.Join(extra, "a.txt"), false); err != nil { t.Fatalf("ro read rejected: %v", err) } if err := checkPathMode(filepath.Join(extra, "a.txt"), true); err == nil { t.Fatal("ro write accepted") } // 移除:仅可移除根可摘 if !RemoveAllowedRoot(extra) { t.Fatal("RemoveAllowedRoot on imported root failed") } if err := checkPath(filepath.Join(extra, "a.txt")); err == nil { t.Fatal("removed root still allowed") } if RemoveAllowedRoot(base) { t.Fatal("builtin root was removable") } } // 围栏实例化(v2 的账:围栏曾是进程级全局单例,A 工作区导入的目录 B 也能读)。 // 每个会话一把围栏,互不可见;默认围栏与会话围栏互不影响。 func TestFenceInstancesAreIsolated(t *testing.T) { wsA, wsB := t.TempDir(), t.TempDir() extraA := t.TempDir() fa := NewFence() if err := fa.AddRoot(wsA, ModeRW, false); err != nil { t.Fatal(err) } if err := fa.AddRoot(extraA, ModeRW, true); err != nil { t.Fatal(err) } fb := NewFence() if err := fb.AddRoot(wsB, ModeRW, false); err != nil { t.Fatal(err) } // A 的会话能看到 A 的工作区与 A 导入的目录。 if err := fa.Check(filepath.Join(extraA, "a.txt")); err != nil { t.Fatalf("fence A rejected its own extra root: %v", err) } // B 的会话看不到 A 的任何东西——这正是隔离的目的。 if err := fb.Check(filepath.Join(wsA, "x.txt")); err == nil { t.Fatal("fence B can read fence A's workspace") } if err := fb.Check(filepath.Join(extraA, "a.txt")); err == nil { t.Fatal("fence B can read fence A's imported dir") } // 默认围栏不受会话围栏影响。 SetAllowedRoots(t.TempDir()) defer SetAllowedRoots() if err := Default().Check(filepath.Join(wsA, "x.txt")); err == nil { t.Fatal("default fence polluted by session fence") } } // BuiltinIn 绑定会话围栏:工具实际执行走传入的那把,不是默认围栏。 func TestBuiltinInUsesSessionFence(t *testing.T) { root := t.TempDir() f := NewFence() if err := f.AddRoot(root, ModeRW, false); err != nil { t.Fatal(err) } reg := NewRegistry() for _, tl := range BuiltinIn("", f, nil) { if err := reg.Register(tl); err != nil { t.Fatal(err) } } write, _ := reg.Get("write") if _, err := write.Run(context.Background(), `{"path":"/etc/autogo-evil2.txt","content":"x"}`); err == nil { t.Fatal("write outside session fence accepted") } // 默认围栏此时是空的:若工具误用默认围栏,根内写入也会被拒——用这个反差验证绑定。 res, err := write.Run(context.Background(), `{"path":"`+filepath.Join(root, "ok.txt")+`","content":"x"}`) if err != nil { t.Fatalf("write inside session fence rejected: %v", err) } if !strings.Contains(res.Text, "wrote") { t.Fatalf("unexpected: %q", res.Text) } } // 固有根不可降级:SetRoots 设的根,后续 AddRoot 不能把它变成可移除。 func TestFenceInherentRootNotDemoted(t *testing.T) { root := t.TempDir() f := NewFence() f.SetRoots(root) if err := f.AddRoot(root, ModeRO, true); err != nil { t.Fatal(err) } for _, d := range f.RootDetails() { if d.Path == evalPath(mustAbs(t, root)) && d.Removable { t.Fatal("inherent root was demoted to removable") } } } func mustAbs(t *testing.T, p string) string { t.Helper() abs, err := filepath.Abs(p) if err != nil { t.Fatal(err) } return abs }