DeepSeek Harness 覆写 Diff 基础有界化:diffBasisMaxBytes的职责划分与描述符级安全上限实现
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
导读
本文以 DeepSeek Harness 仓库中已实现的 Agent Note《在提供方限制覆写上下文 diff 基础》为核心,剖析dsh-fs-local为覆写(overwrite)操作生成的上下文 diff(contextual diff)基础引入的有界读取机制:新增部署配置LocalFileSystem.Config.diffBasisMaxBytes(默认 10 MiB),在提供方侧对仅用于展示的旧文件预读施加安全上限,关闭此前 result-time applied-hunk diff 架构笔记 中记录的暂缓事项。读完本文,你将理解该上限为何必须落在"描述符读取"而非"路径 stat 预检"上、before: null的整文件回退如何保证写入永不因展示性预读而失败,以及提供方(fs-local/fs-sandbox)与消费方(tool-fs)之间如何划分 diff 计算的职责边界。
一、背景:覆写展示性预读为何需要上限
在 DeepSeek Harness 的文件系统能力缝(capability seam)中,dsh-fs-local负责实现ctx.fs约定的宿主文件系统后端。当模型调用写入工具覆写一个已存在文件时,提供方会在FsWriteOutcome.before字段中返回完整旧文件内容,供消费方(dsh-tool-fs)在结果期生成"覆写上下文 diff"(applied-hunk diff),让 UI 以 diff 卡片的形式展示这次变更到底改了什么。
问题在于:这个before预读仅用于展示,却在旧实现中没有上限:
- 大文件被覆写时,提供方可能为生成 diff 基础而分配整个旧文件到内存;
- 仅依靠覆写前较早的路径
stat结果做大小检查,无法真正实施上限——外部进程可以在stat与读取之间替换文件或把文件扩大; - 即使旧文件本身很小,如果替换内容(新内容)很大,上下文 hunk 的尺寸也会趋近于替换内容本身的规模。
也就是说,这条"尽力而为(best-effort)"的展示路径存在演变为无上限分配的风险。本次改动正是关闭了 result-time applied-hunk diff 笔记 中记录的"暂缓上限"事项。
二、决策:提供方侧的有界 diff 基础
本 Agent Note 的核心决策可以概括为:提供方拥有对before基础的上限控制,因为它是提供方可选提供、尽力而为的内容;而 diff 的计算、保留与展示仍归消费方tool-fs所有。
2.1 新配置项diffBasisMaxBytes
在 fs-local 源码 中,LocalFileSystem.Config新增了diffBasisMaxBytes:
export interface Config { /** Base directory for relative paths. Defaults to `process.cwd()`. */ cwd?: string /** * Exclusive UTF-8 byte limit on each overwrite-diff side, capped by the * runtime's safe allocation/decode maximum. Defaults to 10 MiB. */ diffBasisMaxBytes?: number }- 类型与校验:必须是"不超过运行时 Buffer 分配和字符串解码上限的正安全整数"。运行时上限取
node:buffer常量MAX_LENGTH与MAX_STRING_LENGTH中的较小者(fs-local 源码)。 - 默认值:
10 * 1024 * 1024,即 10 MiB(fs-local 源码)。 - 非法值拒绝:构造
LocalFileSystem时,非安全整数、小于等于 0、超过运行时上限的值都会抛出fs-local: diffBasisMaxBytes must be a positive safe integer no greater than ${MAX_DIFF_BASIS_BYTES}(fs-local 源码)。 - 语义:它是排他(exclusive)上限——只有当替换内容的 UTF-8 字节数严格低于该值,且为生成基础而打开的旧文件最终也低于该值时,覆写才提供
before。
2.2 写入路径中的判定逻辑
在 fs-local 的writeText实现 中可以看到完整的判定与降级流程:
const diffable = existing !== null && Buffer.byteLength(content, 'utf8') < this.config.diffBasisMaxBytes const before = diffable ? await readTextForDiff(target.targetKey, this.config.diffBasisMaxBytes, signal) : null await writeFileAtomic(...)关键点:
- 旧文件存在(
existing !== null)且替换内容(UTF-8 字节)低于上限时,才尝试读取基础; - 读取本身交给
readTextForDiff,由它独立裁决旧文件一侧是否合格; - 无论
before是字符串还是null,原子写入都会照常执行——上限只影响展示,不影响提交; after返回 LF 规范化后的新内容,与before共用同一套换行基准,避免 CRLF 覆写被误判为每一行都变化(行尾恢复属于存储细节,applied-hunk diff 忽略它)。
2.3 描述符级有界读取:readTextForDiff
上限的真正实施点在 fs-local 的fsio.ts中的readTextForDiff。它的设计要点是:先打开文件拿到文件描述符,再对描述符自身执行 stat 与分块读取,从而规避路径 stat 与读取之间的 TOCTOU 窗口:
const handle = await open(absolutePath, 'r') let buffer: Buffer let total = 0 let openedSize = 0 try { const info = await handle.stat() if (!info.isFile()) return null if (info.size >= maxBytes) return null openedSize = info.size // One extra byte detects growth after stat without retaining per-read backing buffers. buffer = Buffer.allocUnsafe(openedSize + 1) while (total < buffer.length) { const length = Math.min(buffer.length - total, DIFF_BASIS_READ_CHUNK_BYTES) const { bytesRead } = await handle.read(buffer, total, length, null) if (bytesRead === 0) break total += bytesRead } } finally { await handle.close() }这套流程逐条落实了 Agent Note 中的约束:
- 先 open,再 stat 描述符:检查的是"即将真正读取的那个文件对象",而不是可能已被外部替换的路径;
- 描述符 stat 后大小变化也返回
null:total !== openedSize时放弃(fsio.ts),即使最终大小仍低于上限——因为部分前缀会成为错误的 diff 基础,误导 diff 展示; - 多分配一个字节用于检测读取后增长:
Buffer.allocUnsafe(openedSize + 1)让分块读取在读到边界后还能多读一字节,配合total !== openedSize检查,能在不保留额外缓冲的前提下发现文件在读取期间被扩大; - 取消响应:分块循环中反复检查
AbortSignal,取消以FS_ABORTED向上传播; - 二进制 / 无效 UTF-8 拒绝:
basis.includes(0)检测 NUL 字节,TextDecoder('utf-8', { fatal: true })严格解码,解码失败返回null; - errno 吞并:描述符阶段的任何 errno(旧文件在调用方预检之后、基础读取打开之前被删除或变得不可读,或读取故障)都只返回
null——已提交的写入绝不能因为一次展示性预读失败而失败;只有取消和非 errno 故障继续向上传播。
2.4 测试验证
fs-local 的测试 覆盖了该配置的行为契约:
- 默认值:未传配置时
diffBasisMaxBytes为10 * 1024 * 1024(filesystem.spec.ts); - 非法值拒绝:
[0, -1, 1.5, maxDiffBasisBytes + 1, Number.MAX_SAFE_INTEGER + 1]等非正、小数、超上限、不安全整数都会被拒绝并抛出上述错误消息(filesystem.spec.ts); - 测试辅助函数
remountWithDiffLimit通过重新挂载LocalFileSystem插件来验证不同上限下的行为(filesystem.spec.ts)。
三、职责划分:提供方与消费方各管一段
本次决策最值得注意的设计是把同一条before合格规则完整地放在提供方,而不是分散到两个插件中:
| 关注点 | 归属方 | 依据 |
|---|---|---|
before上限配置(diffBasisMaxBytes) | 提供方dsh-fs-local/dsh-fs-sandbox | fs-local 源码 |
| 有界基础读取(描述符级) | 提供方dsh-fs-local | fsio.ts |
| diff 计算、保留与展示 | 消费方dsh-tool-fs | tool-fs write.ts |
读取路由流式阈值(readStreamMinSize) | 消费方dsh-tool-fs(独立策略) | tool-fs read.ts |
3.1 消费方如何消费before: null
tool-fs的写入工具在presentationMeta中把before === null直接映射为空 diff 列表,绝不调用 diff 计算(tool-fs write.ts):
presentationMeta: (args, value) => ({ diffs: value.before === null ? [] : computeHunkDiffs(args.file_path, value.before, value.after) .map(({ path, oldText, newText }) => ({ path, oldText, newText })), }),而当before有值时,computeHunkDiffs(tool-fs diff.ts)基于diff库的structuredPatch对 LF 规范化的 before/after 逐 hunk 生成{ path, oldText, newText }结构,附着在工具结果上供 UI 渲染 diff 卡片。这就是 Agent Note 中"消费方使用既有的整文件回退"的落地形态:before: null时没有上下文 hunk,但写入确认照常返回,模型看到的仍是"Updated"确认与 diff 卡片的降级表现。
3.2 与readStreamMinSize的关系
diffBasisMaxBytes与tool-fs的readStreamMinSize(文件读取时流式/整读的路由阈值)是相互独立的配置:
readStreamMinSize属于消费方,决定read工具对大文件走streamText还是readText(tool-fs read.ts);diffBasisMaxBytes属于提供方,决定覆写展示的旧文本获取成本。
Agent Note 明确否决了"在提供方保留一个与读取工具流式阈值相等的硬编码阈值"的替代方案,理由是:读取阈值可由部署配置且归消费方所有,两个同值常量会形成无法强制的一致性耦合;而覆写基础本身是部署层面的内存与展示选择,两者无需共享数值。
四、沙箱后端的继承
dsh-fs-sandbox的SandboxedFileSystem直接继承LocalFileSystem,其插件配置类型就是本地后端的配置原样(fs-sandbox 源码):
/** * Plugin config: the local backend's knobs verbatim (`cwd` resolution default * and `diffBasisMaxBytes` overwrite-presentation bound). The sandbox default * (mode + `workspace-write` fallback root) is NOT here — `ctx.sandboxPolicy` * resolves each calling session for every enforcing capability. */ export type Config = LocalConfig因此diffBasisMaxBytes对沙箱强制后端同样生效:沙箱层只增加每次变更调用的策略栅栏(containment 检查),文本存储机制——包括有界 diff 基础读取——逐字继承自本地实现。这意味着在沙箱模式下,部署同样只需设置一个配置项即可约束覆写展示的旧文本缓冲。
五、部署配置示例
在 fs-local README 的配置示例基础上,加入新的上限配置:
# dsh-fs-local 插件配置 config: cwd: /absolute/path/to/workspace diffBasisMaxBytes: 10485760 # 10 MiB,排他上限;更小的旧文件/新内容才生成 before字段速查:
| 字段 | 默认值 | 含义 |
|---|---|---|
cwd | process.cwd() | 相对路径的基准目录 |
diffBasisMaxBytes | 10 MiB | 每次覆写 diff 一侧的 UTF-8 字节上限;更大的覆写返回before: null |
生成的配置目录(参见 config-catalog)是每个受支持字段及其 JSDoc 的穷尽式真源。
六、被否决的替代方案:为什么"先 stat 再整读"不可行
Agent Note 记录了三个被否决的方案,理解它们能更清楚为什么最终实现长这样:
- 保留与读取工具流式阈值相等的硬编码阈值:否决。读取阈值可由部署配置且归消费方所有;两个同值常量会形成无法强制的一致性耦合,而覆写基础本身是部署层面的内存与展示选择。
- 提供方只限制旧内容一侧,在
tool-fs限制新内容 diff:否决。当提供方配置的成对上限已排除替换内容时,这仍会先获取旧文本;同时把同一条before合格规则拆到两个插件中。消费方仍可自由施加额外的输出限制。 - 信任初次
probe()的大小,再执行普通整文件读取:否决。该大小可能在读取前变旧;描述符读取必须对它真正读取的对象实施上限——这正是readTextForDiff先open再stat的原因。
至于"为任意大的内容对流式生成上下文 diff",本缺陷修复不采用:当前文件系统 seam 返回完整的before/after字符串,当前 diff 实现也消费这两个字符串,流式 diff 需要独立的跨包协议与展示设计,超出了本次有界化修复的范围。
七、后果与权衡
引入diffBasisMaxBytes后的可观察后果:
- 部署可以独立调整覆写基础成本,而不必改变读取路由(
readStreamMinSize无关); - 达到或超过排他上限时,覆写仍会成功,并通过整文件回退保持可见(
before: null→ 空 diff 列表),但不再提供上下文 hunk; - 低于上限时,除调用方持有的替换内容外,提供方仍可能持有接近
diffBasisMaxBytes的旧文本(例如 10 MiB 上限下约 10 MiB 的缓冲); - 对合格覆写而言,有界描述符读取增加了一次
open/stat/read序列的系统调用开销,同时防止陈旧的路径探测把这个序列变成无上限分配——这是本修复的核心收益。
八、延伸阅读
- result-time applied-hunk diff 架构笔记——本次改动关闭其"暂缓上限"事项的上游设计文档;
- fs-local 实现——
Config、构造校验、writeText与editText的完整实现; - fsio.ts 有界读取——
readTextForDiff的描述符级实现细节; - fs-local 测试——默认值、非法值拒绝与上限行为的测试契约;
- fs-sandbox 源码——沙箱后端原样继承本地配置的证明;
- tool-fs 写入工具——消费方对
before: null的整文件回退渲染; - tool-fs diff 计算——
computeHunkDiffs的逐 hunk 生成逻辑。
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考