Files
Sakurasan d5dac5a6fc feat: AutoZero 通用 Agent 框架(从 AutoGo 五阶段拆分完成)
llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、
builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、
编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。

产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt)
留在 AutoGo,宿主接入方式见 MIGRATION.md。
2026-10-26 00:54:46 +08:00

234 lines
7.1 KiB
Go

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
}