Volume 2 · Chapter 26
文件系统:事件 gate 型 seam 的样板
15 页给过判定口诀:provider 与 policy 靠事件名连接 = 事件 gate 型。fs 就是这种类型的样板——fs/write-intent、fs/edit-intent、fs/observed 三个事件让 policy 注入检查,而不是靠 ctx.get() 直接调。
示例本次示例:模型写文件的意图闸门
# 模型:write({path:'src/auth.ts', content:'...'})
# → tool-fs 执行(inject=['tools','fs','systemPrompt'])
# → 写之前发 fs/write-intent(fs/src/index.ts:58,waterfall):
# payload = { target: {path:'src/auth.ts'}, actor: exec.agent }
# fs-observation-policy 的监听器(fs-observation-policy/src/index.ts:116-119):
# 占据决策槽、不调 next() → 策略判定:
# 只读检查 → 放行(返回 intent)
# 写到工作区外 → 拒绝(返回 undefined 或改写)
# 判定结果回传 tool-fs → 执行或拒绝 → tool/result
# 对比 shell(15 页):这里没有 ctx.get('policy') 调用——策略是「拦」下来的
# 如果第二个插件也监听 fs/write-intent 且不调 next: # 它短路掉第一个 policy 的判定(08 页 waterfall 语义) # → 决策冲突 → 第一个 policy 形同虚设 # 教训:事件 gate 型 policy 的组合必须靠 policy 内部编排, # 不能指望事件链叠加(26 页易错点)
§1挂载条目
fs-observation-policy → dsh-fs-observation-policy——写意图的观察策略(事件 gate 的消费者)tool-fs → dsh-tool-fs(1517 行)——文件读写工具(inject = ['tools','fs','systemPrompt'],src/index.ts:22)tool-fs-search → dsh-tool-fs-search(1578 行)——文件搜索工具tool-str-replace-editor → dsh-tool-str-replace-editor(553 行)——字符串替换编辑fs-sandbox → dsh-fs-sandbox——沙箱化 fs 提供方(static inject = ['sandboxPolicy'],src/index.ts:60)
§2包文件地图
| 包 | 规模 | 角色 |
|---|---|---|
packages/fs/fs/ | — | Service Definition:FileSystem extends Service(src/index.ts:86)+ 事件声明——fs/write-intent 是 waterfall(src/index.ts:58) |
packages/fs/fs-observation-policy/ | — | Policy:监听 write-intent,占据唯一决策槽、不调 next()(src/index.ts:116-119)——这是与 Service 型 seam 最大的区别 |
packages/fs/tool-fs/ | 12 文件 / 1517 行 | Consumer:文件读写工具(read 结果带 diff 视图——05 页老站提过的 presentResult meta) |
packages/fs/tool-fs-search/ | 8 文件 / 1578 行 | 搜索工具(内置 grep 语义) |
§3机制:事件 gate 的完整形态
- provider 发意图:fs 工具要写文件前,发出
fs/write-intent(waterfall)——携带目标与 actor。 - policy 拦截:fs-observation-policy 的监听器不调 next()(占据决策槽)——返回策略的判定(放行/改写/拒绝)。这就是「事件 gate」:policy 不通过
ctx.get被调用,而是通过事件链被拦下来。 - 观察回传:
fs/observed报告实际发生的变化(供注入、UI 更新等下游)。
与 15 页的 shell 对比:shell 的连接是 ctx.shell.resolve → run(调用链),fs 的连接是 intent → 事件 → policy(拦截链)。前者适合「能力替换」,后者适合「行为管制」。
§4关键代码
58 'fs/write-intent'(target: FsTarget, actor: object | undefined, next: () => FsWriteIntent | undefined | Promise<FsWriteIntent | undefined>): Promise<FsWriteIntent | undefined>
86export abstract class FileSystem extends Service {
116 // fs/write-intent: occupy the single decision slot — do NOT call next().
119 ctx.on('fs/write-intent', (target, actor) => Promise.resolve().then(() => gate.writeIntent(target, actor)))
waterfall 事件声明:返回值是 FsWriteIntent | undefined——policy 的判定就是返回值(放行/改写/拒绝)。
FileSystem 的 Definition 与 shell 同构(都是 Service),区别在「连接方式」——fs 多了一层事件 gate。
注释就是判定口诀的原文:occupy the single decision slot, do NOT call next()。事件 gate 型 policy 的典型形态——不调 next = 短路(08 页 waterfall 语义),决策完全由 policy 给出。
§5易错点
事件 gate 的监听器不调 next() 是有意的——但这意味着只能有一个 policy 占据决策槽。第二个 policy 也会短路掉第一个的判定。多 policy 组合要靠 policy 内部编排,不能指望事件链叠加。
fs 与 subprocess 共享执行世界(15 页提过):把 fs + subprocess 提供方一起指向远程沙箱(e2b),bash、PTY、LSP 全部跟着搬——这就是为什么它们是两个独立的 seam 却设计成可指向同一后端。