Appearance
3.3 文件系统能力族
本章概览:DSH 的文件系统能力不是"一个读写工具",而是一条四层栈——提供方契约、本地/沙箱实现、经
fs/*事件门参与的策略插件、模型侧工具。读完可回答:为什么edit要求先read,为什么用字符串替换而非整文件重写,glob/grep为什么不走ctx.fs。
概述 / 定位
fs 能力族位于 packages/fs/ 下,全部是产品包:
| 包 | 角色 | ctx 键 |
|---|---|---|
dsh-fs | Service Definition:存储原语 + fs/* 事件词汇 | ctx.fs |
dsh-fs-local | Provider:宿主文件系统实现 | (注册 ctx.fs) |
dsh-fs-sandbox | Provider:沙箱围栏实现(扩展 fs-local) | (注册 ctx.fs) |
dsh-fs-observation-policy | 策略插件:观察状态 + 写前读 + 版本守卫 | (无服务,仅 fs/* 监听) |
dsh-tool-fs | Consumer:模型侧 read/write/edit/read_image 与执行器 | (注册 ctx.tools) |
dsh-tool-fs-search | Consumer:模型侧 glob/grep 发现工具 | (注册 ctx.tools) |
dsh-tool-str-replace-editor | Consumer:独立 str_replace_editor 工具 | (注册 ctx.tools) |
出处:
packages/fs/README.md(deepseek-harness-src/packages/fs/README.md)
它在 agent-loop 主干之外、是可选项:换后端(本地→沙箱→远程 E2B)不触碰策略与工具 schema。搜索有意不扩展提供方契约——glob/grep 是 ripgrep 进程工作流,经 ctx.subprocess 跑打包的 @vscode/ripgrep 二进制,而不是 ctx.fs 的方法。
核心机制
四层栈与三角色
dsh-fs 把"读文件"拆成四个可独立演化的层:tool/executor(dsh-tool-fs)→ policy(dsh-fs-observation-policy)→ provider contract(dsh-fs)→ provider(dsh-fs-local / dsh-fs-sandbox / E2B)。关键在 policy 层:它不是工具注入的服务,而是一个只通过 fs/* 事件门参与的插件——删掉它,工具优雅回落到裸 provider(无条件写/编辑),不会在服务注入边界上断裂。
出处:
packages/fs/fs/README.md(deepseek-harness-src/packages/fs/fs/README.md)
ctx.fs 契约要点
FileSystem 实现十二个原语:resolve / processPath / fileUrl / contains / stat / lstat / readText / streamText / readBytes / listDir / writeText / editText。要点:
- 目标身份:
resolve(path)产出不透明的FsTarget(targetKey是品牌化不透明 id,消费方不得解析;只有displayPath可展示)。processPath/fileUrl提供"执行世界"坐标,contains测规范包含关系。 - 原子突变:
writeText/editText都是原子写(临时文件 + fsync + 发布),版本守卫可选——省略即无条件,提供即守卫。守卫是FsWriteIntent:createIfAbsent(缺失才创建,存在报FS_NOT_OBSERVED)或replaceIfVersion(仅在观察版本上替换,否则FS_STALE_VERSION)。 - 结构化错误码:
FsErrorCode闭集(FS_NOT_FOUND、FS_STALE_VERSION、FS_NOT_OBSERVED、FS_SANDBOX_DENIED、FS_AMBIGUOUS_EDIT、FS_EDIT_NOT_FOUND等),工具注册表在isError结果上暴露{ name, code }。
fs-sandbox:写围栏
SandboxedFileSystem 继承本地实现全部机制,只对 writeText / editText 加逐调用模式围栏:read-only 一律拒绝(结构化 FS_SANDBOX_DENIED,携带生效模式);workspace-write 只允许目标落在 workspace 根与平台临时区之下;danger-full-access 不围栏。读永远放行。它与 bash 共享同一个 ctx.sandboxPolicy 解析出的 per-call policy(mode + 会话 cwd 根),可写根集合来自单一来源函数 writableRoots——fs 围栏与 bash runner 不可能漂移。拒绝是进程内精确的结构化错误,无需像 bash 那样从 stderr 推断。
出处:
packages/fs/fs-sandbox/README.md(deepseek-harness-src/packages/fs/fs-sandbox/README.md)
观察策略:fs/* 事件门
dsh-fs-observation-policy 注册三个 fs/* 事件监听(事件词汇归 dsh-fs 所有,发方是 dsh-tool-fs,监听方是策略插件——发方不依赖策略包):
| 事件 | 类型 | 策略插件的处理 |
|---|---|---|
fs/write-intent | 单槽决策 waterfall | 未见过/确认缺席 → createIfAbsent;观察到 present → replaceIfVersion(带观察版本) |
fs/edit-intent | 单槽决策 waterfall | 未见过 → FS_NOT_OBSERVED;确认缺席 → FS_NOT_FOUND;present → 以观察版本作 CAS 基准 |
fs/observed | 即发即忘记录 | 同步、仅副作用的 WeakMap.set,记录 present(version) 或 absent |
两个 intent 槽是单槽、注册顺序首胜:策略插件完全决策、不调用 next()。观察状态是"先验记录"(unseen / absent / present@version),策略插件本身不做任何文件系统 I/O——它把状态转换成 provider 的守卫。移除插件后工具回落到裸 provider;这正是事件门相对强制方法服务的全部意义。
出处:
packages/fs/fs-observation-policy/README.md(deepseek-harness-src/packages/fs/fs-observation-policy/README.md)
模型侧工具集
| 工具 | 参数 | 行为 |
|---|---|---|
read | file_path, offset?, limit? | 带行号的 UTF-8 内容 + 分页 footer;offset 从 1 起,limit 默认并封顶 2000 |
read_image | file_path | 仅在有持久 attachment 服务且路由模型声明 image 输入时注册/执行 |
write | file_path, content | 创建或整体替换;策略下覆盖已有文件需先 read 且版本未变 |
edit | file_path, old_string, new_string, replace_all? | 字面替换;默认要求唯一匹配 |
glob | pattern, path? | rg --files 发现文件,--no-config 防 RIPGREP_CONFIG_PATH 注入 |
grep | pattern, path?, include? | rg --json 行式匹配,按文件分组 |
str_replace_editor | view/create/str_replace/insert | 绝对路径编辑器命令集 |
工具侧还注册了固定提示词段:读用 read 而非 cat("Results include line numbers. Use offset and limit to continue reading large files.")、写用 write("Existing files are overwritten, so read an existing file first...")、编辑用 edit("Use the edit tool for targeted changes to existing UTF-8 text files...")。
出处:
packages/fs/tool-fs/README.md与packages/fs/tool-fs-search/README.md(均在deepseek-harness-src/packages/fs/下)
为什么字符串替换而非整文件重写
edit / str_replace_editor 坚持"字面字符串替换"而不是"模型重发整个文件":
- token 与噪声:整文件重写要模型重新产生全部内容,代价高且容易夹带与目标无关的改动;字面替换只传一小段
old_string,且old_string必须唯一匹配(多匹配默认报FS_AMBIGUOUS_EDIT,除非replace_all)。 - 编辑是提供方原语,不是 read+write 组合:版本守卫校验、字面匹配、原子重写必须留在一个临界区内,才能正确归因错误(陈旧先报
FS_STALE_VERSION,而不是对新内容报匹配失败)并保证"一胜一败"的并发语义;远程后端还可实现为原生 compare-and-edit。 - 展示即输入:
str_replace_editor的view用 1 起行号并保留内容 tab,使展示文本可直接当作字面替换输入;str_replace拒绝 0/多匹配且没有replace_all参数,从契约上逼模型给出精确的唯一片段。
出处:
packages/fs/tool-str-replace-editor/README.md(deepseek-harness-src/packages/fs/tool-str-replace-editor/README.md)与docs/subsystems/filesystem.md(deepseek-harness-src/docs/subsystems/filesystem.md)
"写前观察"模式
默认部署的完整工作流是:先 read(记录版本)→ 再 write/edit(带版本守卫)。策略把守卫翻译成可操作的错误与恢复指令:
- 未先读就
edit:FS_NOT_OBSERVED,精确文案edit requires reading "<path>" first; - 版本已变:
FS_STALE_VERSION,工具层追加恢复指令— re-read the file, then retry; - 观察到缺席:只允许
createIfAbsent写,edit报FS_NOT_FOUND。
观察状态是进程内 WeakMap、不跨会话持久化:恢复的会话必须先重读。授权语义是"版本新鲜度"而非"视图完整性"——任何窗口的 read 只要文件未变就授权后续整文件覆盖,这是刻意比"完整视图"更弱的规则。
无超时
read/write/edit 没有 timeoutMs,契约也不挂截止时间:文件 IO 是尽力可中止的本地 syscall,截止时间杀不掉正在进行的 fsync/rename;取消经工具执行信号在 syscall 边界尽力传播。这与进程托底的 bash/web(有超时)形成对照。
出处:
packages/fs/README.md(deepseek-harness-src/packages/fs/README.md)
关键设计决策
- 策略走事件门而非方法服务:
fs/write-intent/fs/edit-intent单槽决策 +fs/observed即发即忘,发方与策略解耦,策略可优雅装卸。 - 守卫可选,裸 provider 是完整缝:
ctx.fs自身是无约束的完整存储缝;观察策略是"策略插件加上的策略",不是提供方行为——沙箱/远程后端不会继承模型面向的观察策略。 - 编辑留在缝上:版本 + 字面匹配 + 原子重写在一个临界区,错误归因与并发语义才正确。
- 文本优先契约:UTF-8 解码、二进制拒绝(
FS_NOT_TEXT)在缝上完成,策略层永不触碰原始字节;readBytes是唯一裸字节原语。 - 搜索独立于 fs 缝:不强迫每个文件系统后端实现搜索 API;ripgrep 进程工作流自带平台二进制、固定 argv(
--no-config)与结构化错误码。 - 无超时:不给契约无法执行的截止时间承诺。
扩展点
- 新后端:实现
FileSystem十二原语注册ctx.fs(如 E2B 远程实现);沙箱/远程后端自动继承策略与工具层。 - 新策略:注册
fs/write-intent/fs/edit-intent决策监听(单槽,注册顺序决定胜者);注意这不是可组合授权链,分层权限/审计拦截应走tools/execute。 - 新工具:在
dsh-tool-fs或独立包注册 schema,并声明 UI 呈现意图(read-card / diff-card)。 - 观察持久化:观察状态跨会话持久化是留待未来的扩展点;当前恢复的会话必须重读。
参考资料
- 族总览:
packages/fs/README.md(deepseek-harness-src/packages/fs/README.md) - 子系统页:
docs/subsystems/filesystem.md(deepseek-harness-src/docs/subsystems/filesystem.md) - Definition:
packages/fs/fs/README.md;Provider:packages/fs/fs-local/README.md、packages/fs/fs-sandbox/README.md;策略:packages/fs/fs-observation-policy/README.md(均在deepseek-harness-src/packages/fs/下) - Consumer:
packages/fs/tool-fs/README.md、packages/fs/tool-fs-search/README.md、packages/fs/tool-str-replace-editor/README.md(均在deepseek-harness-src/packages/fs/下) - 设计笔记:跨族 fs 沙箱(
deepseek-harness-src/.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md)