feat(tools): 真实工具声明文件目标(让文件级互斥真正生效)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

新增 plugins/tools/io/file_targets.go:fileTool 与 cropTool 实现 tool.FileTargeter
(编译期钉住 var _ tool.FileTargeter,声明集中一处可审计)。
file:read/list/search ⇒ 读;其余(含 delete / 未知 / 缺失 action)⇒ 写(保守侧,宁可多挡一次)。
路径取沙箱解析前的声明值(绝对、clean、已确认在 roots 内,纯词法不触盘);沙箱会拒的路径不声明。
search 的缺省 base 与执行路径共用 f.searchBase(避免声明目录 ≠ 实际走查目录)。
image-crop:仅本地路径声明为读(URL/data URI 不落盘);分类与 load 共用一份。
体检清单:file ✅ / image-crop ✅ / document-read ❌(sidecar 走「临时文件 + rename」原子替换,
无撕读风险;且布局属未导出的第二份真相)/ basic、http、knowledge 等不碰盘;
vision·ocr(internal 越界)、terminal(cmd 越界 + 目标不可知)、MCP 外部工具 未做 —— 如实登记为遗留。
测试:单元(action→Op 全表 / 路径归一 / 不声明场景)+ 机制层 6 条(真插件 + 真 Dispatcher + 真占用表,
用闸门工具把并发变确定性事件,不靠 sleep)。
变异 5 条(一切按写 / 未知按读 / 用原始相对串 / crop 不声明 / file 声明被抹掉)。
注:本切片声明正确但生效依赖 ef3dcb6(网关给每条在途路挂互斥身份)。
This commit is contained in:
AskaEth committed 2026-10-05 09:55:29 +08:00
1 parent ef3dcb699b
commit 3589f4b14f
7 files changed
+842 -15

No files matched your search

+29 -3
View File
@@ -192,13 +192,39 @@ func encodePNG(img image.Image) ([]byte, error) {
return buf.Bytes(), nil
}
// cropSourceKind 是 image 参数的来源分类:只有本地路径会碰文件系统,URL 与 data URI
// 都不落盘。分类只此一份 —— 执行分派(load)与目标声明(file_targets.go 的
// cropTool.FileTargets)共用它,两处就不会各自推演前缀规则而漂移。
type cropSourceKind int
const (
// cropSourceFile 是本地路径:读取要过 file 工具的沙箱。
cropSourceFile cropSourceKind = iota
// cropSourceURL 是 http(s) 源:走共享的 SSRF 防护客户端。
cropSourceURL
// cropSourceData 是 base64/data: URI:内容就在参数里,不碰磁盘。
cropSourceData
)
// cropSourceKindOf 分类一份 image 参数。
func cropSourceKindOf(src string) cropSourceKind {
switch {
case strings.HasPrefix(src, "data:"):
return cropSourceData
case strings.HasPrefix(src, "http://"), strings.HasPrefix(src, "https://"):
return cropSourceURL
default:
return cropSourceFile
}
}
// load reads the source image: a local path through the file sandbox, an http(s)
// URL through the shared SSRF-guarded client, or a base64/data: URI payload.
func (c *cropTool) load(ctx context.Context, src string) ([]byte, error) {
switch {
case strings.HasPrefix(src, "data:"):
switch cropSourceKindOf(src) {
case cropSourceData:
return decodeCropDataURI(src)
case strings.HasPrefix(src, "http://"), strings.HasPrefix(src, "https://"):
case cropSourceURL:
return c.fetch(ctx, src)
default:
return c.readFile(src)