package tools import ( "fmt" "os" "path/filepath" "strings" "sync" ) // 路径围栏:read/write/edit/ls/grep 只允许访问允许根内的路径。 // 这是硬边界而非信任门控——`--yes` 也不能越过。 // // 围栏是实例(Fence),不是进程级全局状态:每个会话按其工作区现场组装 // 自己的围栏(工作区目录 + 该工作区的额外授权 + spill/memory),A 工作区 // 导入的目录 B 工作区的会话看不到。包级的 Default() 围栏只服务默认工作区 // (进程 cwd)场景,历史 API(SetAllowedRoots 等)是它的薄封装。 // // 每个根带访问模式:rw 可读写;ro 只读——read/ls/grep 放行,write/edit 拒绝。 // ro 的典型场景:导入别人的项目只做分析,防止模型改坏。 // // bash 边界(有意为之,非遗漏):bash 不做逐路径校验,只在 cmd.Dir 绑定 // 工作区。ZCode 对持久 shell 有 bash-cwd-policy(命令结束后把逃逸的 cwd // 重置回工作区),AutoGo 的 bash 是无状态的 `sh -c`,每次调用都在工作区 // 重新开始,cwd 不存在跨调用逃逸,该策略在此不适用。bash 仍可读写围栏外 // 文件——ro 挡的是文件类工具的误写,不是恶意进程,完整沙箱见 TODO P2。 // RootMode 是允许根的访问模式。 type RootMode string const ( ModeRW RootMode = "rw" ModeRO RootMode = "ro" ) // allowedRoot 是围栏里的一个允许根。removable=false 是固有根 // (cwd/工作区目录/spill/memory),RemoveRoot 不允许摘掉它们。 type allowedRoot struct { path string // 已解析(符号链接收敛)的绝对路径 mode RootMode removable bool } // Fence 是一组允许根的实例。零值可用(空围栏 = 不限制,供测试)。 type Fence struct { mu sync.RWMutex roots []allowedRoot } // NewFence 创建空围栏。 func NewFence() *Fence { return &Fence{} } var defaultFence = NewFence() // Default 返回包级默认围栏(默认工作区 / 进程 cwd 场景用)。 func Default() *Fence { return defaultFence } func normalizeRoot(r string) (string, bool) { if r == "" { return "", false } abs, err := filepath.Abs(expandHome(r)) if err != nil { return "", false } // macOS 的 /var -> /private/var 等系统符号链接:根与路径必须 // 在同一(解析后的)坐标系里比较。 return evalPath(abs), true } // SetRoots 整体替换允许根(全部 rw、不可移除;传空 = 不限制,供测试使用)。 func (f *Fence) SetRoots(roots ...string) { f.mu.Lock() defer f.mu.Unlock() f.roots = nil for _, r := range roots { if p, ok := normalizeRoot(r); ok { f.roots = append(f.roots, allowedRoot{path: p, mode: ModeRW}) } } } // AddRoot 追加一个允许根。要求 path 是已存在的目录;已存在时只更新模式。 // removable=false 标记固有根(工作区目录),不可被 RemoveRoot 摘除。 func (f *Fence) AddRoot(path string, mode RootMode, removable bool) error { if mode != ModeRO { mode = ModeRW } p, ok := normalizeRoot(path) if !ok { return fmt.Errorf("invalid path %q", path) } if st, err := os.Stat(p); err != nil || !st.IsDir() { return fmt.Errorf("%s is not a directory", path) } f.mu.Lock() defer f.mu.Unlock() for i := range f.roots { if f.roots[i].path == p { f.roots[i].mode = mode // 固有根不会被追加降级为可移除(SetRoots 设的 cwd/工作区目录等)。 if f.roots[i].removable { f.roots[i].removable = removable } return nil } } f.roots = append(f.roots, allowedRoot{path: p, mode: mode, removable: removable}) return nil } // RemoveRoot 移除一个可移除的允许根(固有根不可移除)。返回是否确实移除了。 func (f *Fence) RemoveRoot(path string) bool { p, ok := normalizeRoot(path) if !ok { return false } f.mu.Lock() defer f.mu.Unlock() for i := range f.roots { if f.roots[i].path == p { if !f.roots[i].removable { return false } f.roots = append(f.roots[:i], f.roots[i+1:]...) return true } } return false } // RootDetail 是允许根的展示信息(提示词注入、/dir list 用)。 type RootDetail struct { Path string Mode RootMode Removable bool } // RootDetails 返回全部允许根(含模式)。 func (f *Fence) RootDetails() []RootDetail { f.mu.RLock() defer f.mu.RUnlock() out := make([]RootDetail, 0, len(f.roots)) for _, r := range f.roots { out = append(out, RootDetail{Path: r.path, Mode: r.mode, Removable: r.removable}) } return out } // Roots 返回当前允许根路径列表(用于错误提示与展示)。 func (f *Fence) Roots() []string { f.mu.RLock() defer f.mu.RUnlock() out := make([]string, 0, len(f.roots)) for _, r := range f.roots { out = append(out, r.path) } return out } // Check 校验路径在允许根内(只读语义:ro 根放行)。 // 做尽力而为的符号链接解析(沿存在的祖先链向上解析,deepest-first),防止用软链越界。 func (f *Fence) Check(p string) error { return f.check(p, false) } // CheckWrite 按读写语义校验:ro 根拒绝。 func (f *Fence) CheckWrite(p string) error { return f.check(p, true) } func (f *Fence) check(p string, write bool) error { abs, err := filepath.Abs(expandHome(p)) if err != nil { return err } abs = evalPath(abs) f.mu.RLock() defer f.mu.RUnlock() if len(f.roots) == 0 { return nil } for _, r := range f.roots { if abs == r.path || strings.HasPrefix(abs, r.path+string(filepath.Separator)) { if write && r.mode == ModeRO { return fmt.Errorf("path %q is inside read-only directory %q (imported as read-only)", p, r.path) } return nil } } return fmt.Errorf("path %q is outside allowed roots %v (import one with /dir add)", p, f.roots) } // ---- 包级默认围栏的历史 API(默认工作区场景兼容保留)---- // SetAllowedRoots 整体替换默认围栏的允许根。 func SetAllowedRoots(roots ...string) { defaultFence.SetRoots(roots...) } // AddAllowedRoot 向默认围栏追加可移除根(目录导入 /dir 命令)。 func AddAllowedRoot(path string, mode RootMode) error { return defaultFence.AddRoot(path, mode, true) } // RemoveAllowedRoot 从默认围栏移除一个可移除根。 func RemoveAllowedRoot(path string) bool { return defaultFence.RemoveRoot(path) } // AllowedRootDetails 返回默认围栏的全部允许根。 func AllowedRootDetails() []RootDetail { return defaultFence.RootDetails() } // AllowedRoots 返回默认围栏的允许根路径列表。 func AllowedRoots() []string { return defaultFence.Roots() } func checkPath(p string) error { return defaultFence.Check(p) } func checkPathMode(p string, write bool) error { return defaultFence.check(p, write) } // evalPath 尽力而为地把路径按真实文件系统解析(处理 /var -> /private/var // 这类系统符号链接):从最深处向上找到第一个存在的祖先,解析后拼回尾部。 func evalPath(abs string) string { var tail []string cur := abs for { if resolved, err := filepath.EvalSymlinks(cur); err == nil { return filepath.Join(append([]string{resolved}, tail...)...) } parent := filepath.Dir(cur) if parent == cur { return abs // 到根都没有存在的祖先:放弃解析 } tail = append([]string{filepath.Base(cur)}, tail...) cur = parent } }