Files
AutoZero/tools/guard.go
T
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

228 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}