- 人工智能
- AI Agent
- Agent 框架
- DeepSeek
【免费下载链接】deepseek-harness
DeepSeek Harness: Everything is a Plugin.
本篇文章聚焦 DeepSeek Harness 中glob/grep两个文件发现与内容搜索工具的架构演进:它们从依赖宿主环境rg的 bash 执行缝(seam),迁移为通过ctx.subprocess直接派生随 npm 包分发的内置 ripgrep 二进制(@vscode/ripgrep)。你将理解这次决策的问题背景、纯 argv 直连的调用链、--no-config安全边界、懒解析的加载期失败模式设计、完整的SEARCH_*错误词汇表与配置参数默认值,并看到对应的源码、测试与部署组合证据。核心变更记录于仓库内的决策笔记 2026-08-01-packaged-ripgrep-search.md,对应实现位于@deepseek-ai/dsh-tool-fs-search包(tool-fs-search)。
一、问题:v1 的 bash 封装让系统rg成为宿主依赖
在本次变更之前,glob/grep工具通过 bash 执行缝运行,其设计记录在已被归档的 2026-07-09-bash-backed-grep-glob-discovery.md 中。当时的取舍是:不做ctx.fsprovider 方法(避免把本地产品便利变成通用文件系统后端契约),而是让工具走ctx.bash.resolve()→ctx.bash.run()流程,用固定的rg命令模板执行搜索。这个 v1 方案换来的是三个明确代价:
- 系统
rg成为宿主依赖:插件加载时通过command -v rg探测;在 Windows 和精简容器镜像上,rg默认不在PATH中,于是两个工具静默消失——部署方只能从加载期探测警告中发现问题,而模型侧根本没有机会发起搜索。 - 整个模型可见参数面被迫穿过一层 shell 引号辅助函数:因为 shell 位于工具与 ripgrep 之间,所有模型控制的值(
pattern、path、include)都必须先做 POSIX 单引号转义,再拼进命令字符串。v1 笔记把这种耦合明确记录为权衡,并点名"如果 shell 字符串域被证明过于敏感,直接派生 ripgrep 就是合理的后续方案"。 - 职责重复:探测逻辑要在测试里写脚本模拟;执行器的超时分类与协作式工具超时策略(
tool-call-timeout-policy)重复实现。
事实上 v1 笔记的备选方案中就已经写过:"直接从dsh-fs-local派生 ripgrep……它仍是合理的优化方向,如果 bash 封装的搜索被证明对 shell 字符串过于敏感。" 本次变更正是兑现了这个被显式延期的备选项:直接派生 ripgrep。
二、决策:打包二进制 + 子进程缝直连
@deepseek-ai/dsh-tool-fs-search现在通过ctx.subprocess缝运行随包分发的ripgrep 二进制。该二进制来自 npm 依赖@vscode/ripgrep——它通过可选平台包(@vscode/ripgrep-<platform>-<arch>)为 darwin/linux/win32 × x64/arm64 交付可执行文件,见 package.json 中的"@vscode/ripgrep": "^1.18.0"。
2.1 runRipgrep:纯 argv 向量直连
核心执行函数runRipgrep()位于 search-core.ts,其调用形态是:
ctx.subprocess.spawn({ argv: [await resolveRgPath(), '--no-config', ...argv], cwd: workdir, stdio: { stdin: 'ignore', stdout: { maxBytes: rawOutputMaxBytes }, stderr: { maxBytes: stderrMaxBytes }, }, graceMs, signal: exec.signal, })关键点:
- 无 shell 层:工具与 ripgrep 之间不再有 shell,模型提供的每个值都是 argv 数组中的一个普通元素,不存在引号转义边界。
singleQuote辅助函数及其 shell 派生测试随这次变更一并删除。 - collect 模式输出:stdout/stderr 按"诊断尾部"形状收集,不产生 spill 文件——工具从不读取原始 spill 路径;如果 stdout 是有损读取(超出
rawOutputMaxBytes预算仍未完整保留),搜索以SEARCH_RAW_OUTPUT_OVERFLOW失败,而不是解析一份静默截断的流(completeStdout实现了这条规则)。 - 超时与终止:
graceMs(terminate 升级的宽限期)与exec.signal原样转发给子进程缝;协作式工具调用超时由@deepseek-ai/dsh-tool-call-timeout-policy通过 abort 信号触发,子进程缝的 terminate 升级负责硬杀,工具最终上报SEARCH_ABORTED。
2.2 rgPath 懒解析:把"缺二进制"从加载期错误变成调用期错误
rgPath的解析被设计为首次调用时懒解析、进程内记忆化(resolveRgPath(),同一文件内)。动机很明确:@vscode/ripgrep在模块求值阶段就会解析其平台包,如果用静态 import,一个缺失或损坏的平台包(例如--omit=optional安装、部分安装)会直接变成Loader 组合失败——而"把加载期失败模式移除"正是这次变更要解决的核心目标之一。
懒解析让失败降级为:第一次搜索调用时报SEARCH_FAILED(启动失败),而不是插件加载失败。源码注释明确写着"Resolving at the call boundary keeps a missing or corrupt binary at the first search call asSEARCH_FAILED, rather than failing the Loader composition."。单文件运行时(pkg 打包)无法从虚拟文件系统派生原生辅助程序,因此resolveRgPath还额外支持可执行文件的-rg侧边车(sidecar)路径。
@vscode/ripgrep没有自带类型声明,包内用 ripgrep.d.ts 声明了最小的模块面:命名导出rgPath: string。
2.3 注册无条件化:删除探测与条件注册
v1 的加载期command -v rg探测、条件注册逻辑以及 "rg not found" 警告全部删除。现在注册是无条件的,因为二进制总是可用(它是 npm 依赖)。包注入的服务也从['tools', 'systemPrompt', 'bash']变为['tools', 'systemPrompt', 'subprocess'],见 index.ts。spillStore仍然通过ctx.get('spillStore')机会式读取,因为格式化结果的 spill 是可选的。
2.4 工作目录归属工具
执行工作目录为会话头中的 cwd(exec.agent.session.header.cwd),缺失时回退到process.cwd()。由于不再有执行器配置可"默认穿透",工作目录回退逻辑由工具自己持有。返回路径相对解析后的工作目录展示(toWorkdirRelative),且仅在"工作目录与文件系统read根为同一工作区"的共置部署中可被后续读取——这是 v1 就记录的部署要求,本次未做运行时校验。
三、执行细节与安全边界
3.1 --no-config:阻断配置注入的任意命令预处理器
派生是不受限制的(普通ctx.subprocess调用,而非受沙箱配置约束的派生),因此每次调用都在 argv 前强制加上--no-config。原因:宿主若设置了RIPGREP_CONFIG_PATH(或二进制旁存在rg.conf),ripgrep 会读取该配置并可能注入一个--pre预处理器——为每个匹配文件执行任意命令。--no-config保证没有任何配置文件(因此也没有预处理器)能到达搜索过程。这在 search-core.ts 的runRipgrepJSDoc 中有明确说明。
3.2 glob 的 argv 模板
buildGlobCommand(glob.ts)构建固定模板:
--files --glob=<pattern> --sort=modified --no-ignore --hidden --glob=!**/<vcs> --glob=!**/<vcs>/** (对 .git/.svn/.hg/.bzr/.jj/.sl 各一对) [-- <path>]细节值得展开:
--files只列文件、不列目录;--sort=modified保证修改时间序;--no-ignore --hidden覆盖被忽略与隐藏文件。- VCS 元数据排除是双 glob 对:每个 VCS 目录名同时生成裸形式
--glob=!**/.git(遍历时剪枝该目录)和内容形式--glob=!**/.git/**(当搜索根就是或位于该目录内部时,裸形式对根前缀路径永不匹配,内容形式仍然排除内部)。GLOB_VCS_EXCLUDES = ['.git', '.svn', '.hg', '.bzr', '.jj', '.sl']。 - 搜索根放在
--之后,前导连字符的路径永远不会被解析成 flag。
3.3 grep 的 argv 模板与 --json 解析
buildGrepCommand(grep.ts)构建固定的面向行的rg --json命令:
--json --regexp=<pattern> [--glob=<include>] [-- <path>]选择--json是为了让文件路径、行号、行文本的解析不依赖冒号切分(规避路径含冒号/带格式路径时的歧义)。parseGrepMatches只消费match记录,begin/end/context/summary属于传输帧而跳过;非法 JSON 或缺字段的 match 记录被判定为SEARCH_FAILED;行的 UTF-8 解码失败(ripgrep 发送 base64bytes而非text)时用占位符(line is not valid UTF-8)代替预览,而不是让整个搜索失败。
include校验前置且严格(validateInclude):必须是一个正向 glob,拒绝空串、拒绝!前缀的否定模式、拒绝逗号分隔列表(花括号内的逗号合法,如*.{ts,tsx})。
3.4 退出码语义仍归工具所有
- 退出码
0:成功且有结果; - 退出码
1:成功的空搜索(noMatches置真); - 其他:分类进既有
SEARCH_*词汇表。
由于没有 shell 层,退出码 127 或 "command not found" 文本不可能出现——启动失败在 spawn 时就以 rejection 呈现。classifyRunFailure通过 stderr 尾文本中的/regex parse error|error parsing glob/i区分SEARCH_INVALID_PATTERN,其余归SEARCH_FAILED。
四、配置参数与默认值
包级Config(index.ts)中,sampleOverCapGlobResults是必需项(无默认值,部署必须显式选择超限分页的排序契约),其余字段均有默认值;所有计数/字节/毫秒字段都经assertPositiveInteger校验,graceMs还受MAX_TIMER_DELAY_MS上限约束。完整表格如下(同时见于包 README.md 与生成的 config-catalog.md):
| 配置键 | 默认值 | 含义 |
|---|---|---|
sampleOverCapGlobResults | 无(必需) | true:超限glob页按顶层条目跨目录抽样;false:保留修改时间序头部 |
globMaxResults | 100 | 单次glob调用内联展示的最大路径数(对应源码GLOB_MAX_RESULTS) |
grepMaxMatches | 250 | 单次grep调用内联保留的最大扁平匹配数(GREP_MAX_MATCHES) |
grepMaxLineBytes | 2000 | 每条匹配行预览的字节上限,切分保留 UTF-8 边界(GREP_MAX_LINE_BYTES) |
rawOutputMaxBytes | 20000000 | 工具将解析的完整原始rgstdout 上限;超出报SEARCH_RAW_OUTPUT_OVERFLOW(RAW_OUTPUT_MAX_BYTES) |
timeoutMs | 30000 | 两个工具定义上的协作式调用预算,经exec.signal强制执行(SEARCH_TIMEOUT_MS) |
graceMs | 3000 | 子进程缝在timeoutMs之外给予的 terminate 升级宽限(SEARCH_GRACE_MS) |
stderrMaxBytes | 65536 | rgstderr 诊断尾部预算(SEARCH_STDERR_MAX_BYTES) |
searchMetaMaxBytes | 65536 | 单次搜索序列化presentationMeta的字节上限,尾部组/路径超过即丢弃(SEARCH_META_MAX_BYTES) |
其中rawOutputMaxBytes与globMaxResults/grepMaxMatches分别对应 Claude Code 搜索工具的 20 MB 原始缓冲与 100/250 的内联上限,属于双层预算(原始收集 + 内联页)的设计参照,而非模式化 schema 先例。grep在 v1 不暴露case_insensitive、head_limit、offset、count、多行、上下文行、输出模式或文件类型过滤——需要上下文的模型用read读匹配文件,需要后续结果的模型跟随返回的 spill 定位器提示。
五、组合方式与部署
5.1 最小组合
需要先挂载一个ctx.subprocess后端;无需宿主rg,也无需文件系统 provider(spill 后端可选,用于让超限结果完整可恢复):
- name: '@deepseek-ai/dsh-subprocess-local' - name: '@deepseek-ai/dsh-tool-fs-search' config: sampleOverCapGlobResults: false - name: '@deepseek-ai/dsh-spill-local'5.2 内置工具花名册:TUI/Web 的固定成员
因为dsh-tool-fs-search派生打包二进制并无条件注册两个工具,glob/grep从"宿主依赖决定是否出现"变成固定成员:TUI 与 Web 两个已发布界面共享同一工具花名册,共享底座 base/cordis.patch.yml 中即可看到tool-fs-search挂载(sampleOverCapGlobResults: false)。这一花名册决策记录在 2026-07-31-even-out-shipped-tool-rosters.md:两个界面端到端测试把glob/grep断言为无条件固定成员,而不是宿主相关的一对工具。
5.3 快照场景:真实二进制 + 固定 mtime
fs-glob-sampling快照场景现在执行真实的打包二进制,针对一个预先准备的、用固定 mtime 钉住--sort=modified顺序的工作区——取代了原来通过注入PATH提供的rg替身(替身是 POSIX 专用的,因为其展示路径带/分隔符,会话日志对比无法归一化)。证据见 snapshots/session/fs-glob-sampling/cordis.yml(sampleOverCapGlobResults: true、globMaxResults: 4)与 snapshots/session/fs-glob-sampling/snapshot.yml(scenario: fs-glob-sampling、platform: posix、workspace.setup: fixed-search-mtimes)。
六、失败模式与错误词汇表
搜索失败使用包所有的HarnessError子类SearchError(带稳定错误码与cause链),而非FsErrorCode——因为这些工具是 spawn 支撑的发现操作,不是ctx.fsprovider 操作。词汇表(search-core.ts):
| 错误码 | 含义 |
|---|---|
SEARCH_INVALID_PATTERN | ripgrep 拒绝了正则或 glob |
SEARCH_FAILED | 搜索无法运行或输出无法解析(启动失败、目标不可达、信号杀死、畸形--json) |
SEARCH_RAW_OUTPUT_OVERFLOW | 原始输出超过rawOutputMaxBytes,或在请求的 stdout 预算内仍保持截断 |
SEARCH_ABORTED | 协作式工具超时或调用方取消 |
错误码通过工具注册表的isError结果的{ name, code }暴露,重试/权限/UI 层可以分支处理而无需解析消息文本。加载期失败模式也随本次变更改变:子进程缝损坏时,失败从"插件加载失败(探测所致)"变为"首次搜索调用报SEARCH_FAILED";二进制缺失则变成"带打包路径的启动失败",而不是 PATH 问题。
七、测试与验证
测试覆盖是本次重构可信度的直接证据:
- 真实集成套件(integration.spec.ts):
dsh-subprocess-local+打包二进制,通过ctx.tools.execute()走真实世界——磁盘上真实文件被发现与搜索、敌意模式保持惰性、真实rgstderr 分类进SEARCH_*。它此前在缺少系统rg时自我跳过,现在每个平台都能跑。 - 敌意模式惰性测试:
grep传入'$(touch pwned)',断言结果是No matches found且 canary 文件不存在——因为模式只是 argv 普通元素,世界未被触碰。这被注释明确钉为"已发布契约",未来任何重新引入 shell 包裹的改动都必须守住。 - 懒解析失败路径(rg-path.spec.ts):mock
@vscode/ripgrep在求值时抛错,证明缺失/损坏平台包(--omit=optional、部分安装)表现为逐调用的SEARCH_FAILED,且解析记忆化后每次调用都一致失败。 - 真实加载路径守卫(load-path.spec.ts):命名空间插件如果误加
export default apply,会被 Loader 的unwrapExports折叠、丢掉inject(即 postmortem 0001)。该测试用真实Loader.prototype.unwrapExports解包模块并挂载,验证name/inject/Config/apply形状与无inject错误启动。 - 夹具的 Windows 可重放性:集成套件夹具删掉了一个 Windows 无法表示的
"文件名,保证套件在所有平台可重放。
八、备选方案与取舍记录
决策笔记记录了三组被拒的备选方案,作为后续提案必须超越的基准证据:
- 保留 bash 缝与探测、仅把
rg文档化为必需宿主依赖——拒绝:宿主依赖恰恰是本次要消除的失败;Windows 支持是本次工作的目标,文档化的需求仍然是需求。 - 让
rgPath可注入(配置字段或环境变量覆盖)——拒绝:它新增一个唯一的消费方是测试钩子的公开部署面;真实二进制已足够确定,直接用夹具 mtime 钉住即可——"打包二进制就是部署,测试就该测它"。 - 切换到纯 JS glob/搜索引擎(如
picomatch/tinyglobby)——拒绝:依赖交换审计(2026-07-26-dependency-swaps-rejected-by-nih-audit.md)基于"不存在满足条件的 glob 引擎"证据已否决该方向;ripgrep 语义(--sort=modified、VCS 剪枝、JSON 传输、正则方言)就是工具契约。
九、迁移后果与连锁影响
- 平台覆盖:发现工具在打包二进制覆盖的每个平台(darwin/linux/win32 × x64/arm64)上无需宿主安装即可工作;Node 部署接收
@vscode/ripgrep平台包,Python SDK 单文件运行时则把目标平台原生二进制复制为-rg侧边车。 - 攻击面消失:shell 字符串注入面不复存在,敌意模式是惰性 argv 元素,由现在能在 Windows 上运行的集成套件钉住。
- 溢出路径改变形状:旧 bash 路径继承 bash-local 常开的 spill,可能遗留一个未被读取的多 MB 临时文件;子进程缝现在无 spill 收集,溢出是纯错误(
SEARCH_RAW_OUTPUT_OVERFLOW,"narrow pattern, path, or include and retry"),零内容返回。 - 第三方声明生成器 bug 被暴露并修复:
@vscode/ripgrep新依赖让THIRD_PARTY_NOTICES.md生成器的一个潜在缺陷显形——Node 的fs.globSync返回 OS 原生分隔符,Windows 上带/后缀的 dev 区前缀永不匹配,导致 dev 专用包被错误分档为 runtime;生成器现在在摄入时归一化 manifest 路径,声明平台无关。同时@vscode/ripgrep的 MIT 行进入 runtime 档,pnpm 11 截断的虚拟存储目录名需要元数据查找中的内容扫描回退。 - 夹具调整:集成套件夹具去掉 Windows 无法表示的文件名,套件在所有平台可重放。
十、小结
从 bash 封装到打包 ripgrep 直连,@deepseek-ai/dsh-tool-fs-search的这次架构决策围绕一条主线:把"系统级前置条件"变成"包内资产",把"加载期失败"变成"调用期失败",把"shell 字符串域"变成"纯 argv 向量域"。配套的懒解析、--no-config安全前缀、SEARCH_*错误词汇表、双层输出预算(原始收集 + 内联页 + 格式化 spill 恢复)与跨平台集成测试,共同构成了一套可在任何受支持平台上零安装运行、行为确定、可测试的文件发现与内容搜索能力。若需继续深入,可阅读 filesystem 子系统文档、subprocess 能力文档 与 tool-fs(read/write/edit 工具) 了解与搜索工具配套的完整文件访问链路。
- 人工智能
- AI Agent
- Agent 框架
- DeepSeek
【免费下载链接】deepseek-harness
DeepSeek Harness: Everything is a Plugin.
相关推荐
DeepSeek Harness 中的 ripgrep 文件发现工具:`glob`/`grep` 的设计演进与源码级实现解析
DeepSeek Harness 中的 ripgrep 文件发现工具: glob / grep 的设计演进与源码级实现解析 DeepSeek Harness(
人工智能AI AgentAgent 框架DeepSeekDeepSeek Harness 模型侧搜索工具架构:grep/glob 发现工具的边界划分、双层预算与结果溢出回收
DeepSeek Harness 模型侧搜索工具架构:grep/glob 发现工具的边界划分、双层预算与结果溢出回收 导读 glob 与 grep 是 Deep
人工智能AI AgentAgent 框架DeepSeekTurborepo 二进制入口深度解析:从 `turbo` crate 的薄封装看 Rust 迁移架构
Turborepo 二进制入口深度解析:从 turbo crate 的薄封装看 Rust 迁移架构 本篇文章以仓库中 crates/turborepo/READ
构建工具开发工具CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考