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。
This commit is contained in:
commit
d5dac5a6fc
140 files changed
+26497
No files matched your search
@@ -0,0 +1,233 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user