@epoch-agent/plugin-file
v0.9.0
Published
epoch-agent 文件系统操作插件
Readme
@epoch-agent/plugin-file
文件工具插件。8 个工具。
- ✅ 做:读 / 写 / 精确编辑 / V4A 多文件补丁 / 搜索 / 列目录
- ❌ 不做:git 操作、跨机同步、任何引擎逻辑
- 依赖:
diff(算 diff)。protocol 和 infra 是 peer
工具
只读五个:file_read file_multi_read file_search file_list file_diff_preview;
破坏性三个:file_write file_patch file_multi_patch。
每个工具干什么、分页与 output_mode 那些边界在
docs/TOOLS.md —— 那份是工具清单的唯一真源,
这里只写这个包自己的实现取舍。
file_search 用 ripgrep,但不要求用户自己装:rg 从哪来由 infra 的
resolveRipgrep() 三级决定 —— 系统 PATH 上有就用系统的,
没有就用随包带的 vendor 二进制,两个都没有才退回「工具在、一调才报错 + 给安装提示」
的老行为(启动诊断一条 warn,其余七个工具不受影响)。
本插件不再导出 probeRipgrep() / RIPGREP_INSTALL_HINT,它们搬去 infra 了:
file_list 和将来的 @ 文件补全也要用同一份解析,而 plugin 之间不许互相依赖。
这里只管参数怎么拼和输出怎么收。
只读的四个 + file_diff_preview 标了 supportsParallel: true,引擎可以并发调。
处理的几类脏输入
- 二进制:扩展名黑名单拦截(
.png/.exe/.zip等),不把乱码喂给模型 - BOM:剥掉 Windows 编辑器留的 UTF-8 BOM,否则第一行的 patch 匹配永远不中
- 行尾:CRLF ↔ LF 归一,patch 匹配不受平台影响
- 路径:
~和~user展开 - 工作区边界:
resolveSafePath防路径穿越——resolve 之后用relative判, 不是startsWith。判定本身是 infra 的isInWorkspace()(全仓唯一一份,权限层和 plugin-terminal 共用),这里只管~展开和抛PathEscapeError。ctx.extraRoots(epoch --add-dir)一并透传:落在额外根里的绝对路径 不算越界,但相对路径仍然只按ctx.workDir解析——否则src/a.ts会在每个 根里各找一遍,同一个相对路径有多个答案,而模型无从知道命中的是哪一个 - 读类工具多认一格:本会话的 artifact 目录(方案 47)。
file_read/file_search/file_list/file_multi_read走的是resolveReadablePath, 它在上面那套之外再认artifactsDir(ctx.sessionId)。理由是大输出落盘之后 取回提示会告诉模型「用file_search在这个路径里搜」,而那个路径在工作区外—— 不放开这一格,那句提示就是一句必定失败的指令。 口子收窄到三条:只放开读(file_write/file_patch仍走resolveSafePath, 模型没有理由改写「命令当时输出了什么」这份证据)、只放开本会话那一个子目录 (不是~/.epoch/artifacts,更不是~/.epoch——后者等于把.env里的 API key 和sessions.db一起交出去)、sessionId由引擎给且artifactsDir()内部再洗一次。 这三条各有一条否定用例钉着,见__tests__/artifact-readable.test.ts
四个读类工具自己收进 ctx.outputBudget(2026-09-08)
预算从 ctx.outputBudget 现拿 —— 那是引擎将要用的同一个数,所以自己截完不会
再被引擎切一次,工具说的那句「还有多少、怎么接着读」也就是真的。
MAX_OUTPUT_CHARS(50_000)退化成兜底,只在宿主自己拼
ToolContext 时用(那条路上也没有引擎在截)。
改之前是两处各量一遍、而且两个数不一样:这四个按 50_000 截并明说截了多少, 引擎再按自己那个数切一刀 —— 模型看到的既不是全文,也不是我们说的那句话。
file_read 那一格更具体:它的分页单位是行(offset / limit),预算的单位是
字符。两个单位不换算的后果是那条工具描述里的假话「一次读 500 行」——
500 行长行早就越过预算被引擎从中间切掉,而它报的 nextOffset 是按行算的,
模型照它翻页正好跳过被切掉的那一段,两侧都不报错。现在按行收进预算
(fitLines),报的是真正读到的那一行。
file_diff_preview 的 operation 是故意填错的
它的真实语义是只读(只算 diff 不落盘),annotations 照实写。但 operation 仍填
file_write:老白名单把它归在 FILE_WRITE_TOOLS 里,只改一边会让 recordApproval
那条路径与这里分叉。往严的方向偏,代价只是多一次确认。
开发
pnpm --filter @epoch-agent/plugin-file testpath-boundary.test.ts 记的是具体的穿越手法,改 resolveSafePath 之前先读它。
