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。
228 lines
7.2 KiB
Go
228 lines
7.2 KiB
Go
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
|
||
}
|
||
}
|