Merge pull request #1119 from deepseek-harness/feat/ripgrep-packaged-binary

feat(fs-search): spawn the packaged ripgrep binary through the subprocess seam
This commit is contained in:
Huanqi Cao
2026-08-03 23:31:29 +08:00
committed by GitHub
39 changed files with 1238 additions and 850 deletions

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-01-packaged-ripgrep-search.md
2026-08-01-packaged-ripgrep-search.md: 7c515618a18b61bd90177a6fdf19bbd52e564209
2026-08-01-packaged-ripgrep-search.zh.md: f2b1a12c737f772bff6a6c91c17f7453dbc89748

View File

@@ -0,0 +1,38 @@
# Agent Note: Packaged ripgrep spawn for glob/grep
Status: implemented
English | [中文](2026-08-01-packaged-ripgrep-search.zh.md)
> Supersedes [bash-backed grep/glob discovery](../../archived/feature/2026-07-09-bash-backed-grep-glob-discovery.md): the v1 decision's explicitly deferred alternative — directly spawning ripgrep — is now what ships.
## Problem
The `glob`/`grep` tools ran through the bash executor seam, which made a system `rg` install a host dependency. On Windows and container images there is no `rg` on `PATH` by default, so the tools silently vanished there; a deployment could only discover that from the load-time probe warning. The bash seam also forced the whole model-visible argument surface through one shell-quoting helper, because a shell sat between the tool and ripgrep — the [bash-backed note](../../archived/feature/2026-07-09-bash-backed-grep-glob-discovery.md) recorded that coupling as the v1 trade-off and named direct spawn as the reasonable follow-up if the shell-string domain ever proved too sensitive. It did: every model value had to survive POSIX single-quoting, the probe had to be scripted in tests, and the executor's own timeout classification duplicated what the cooperative tool-timeout policy already owns.
## Decision
`@deepseek-ai/dsh-tool-fs-search` now runs the PACKAGED ripgrep binary (`@vscode/ripgrep`, an npm dependency whose optional platform packages ship the binary) through the `ctx.subprocess` seam: `runRipgrep()` spawns `rgPath` with a plain argv vector prefixed by `--no-config`, collect-mode stdout/stderr, `graceMs`, and `exec.signal` forwarded. `rgPath` resolves lazily at the first call (memoized per process): `@vscode/ripgrep` resolves its platform package at module evaluation, so a static import would turn a missing or corrupt platform package (`--omit=optional`, partial install) into a Loader-composition failure — the load-time failure mode this change exists to remove. There is no shell layer, so the shell-quoting boundary is gone from execution; the `singleQuote` helper and its shell-spawning tests are deleted with it. The raw streams request the seam's diagnostic-tail collect shape (no spill files — the tool never reads a raw spill path; a lossy stdout read fails as `SEARCH_RAW_OUTPUT_OVERFLOW`). The terminate grace and the stderr tail budget are validated `Config` fields (`graceMs` default 3000, `stderrMaxBytes` default 64 KiB), no longer inherited from bash-local's config. Registration is unconditional — the load-time `command -v rg` probe and the conditional registration decision are deleted, and with them the "rg not found" warning. The package injects `tools`, `systemPrompt`, and `subprocess`.
Exit semantics stay tool-owned: exit 0 is success with results, exit 1 is a successful empty search, anything else classifies into the existing `SEARCH_*` vocabulary (invalid pattern, launch failure, signal kill, raw-output overflow). Timeout is the cooperative tool-call budget attached to the tool definitions: `@deepseek-ai/dsh-timeout-policy` aborts `exec.signal`, the subprocess seam's terminate escalation provides the hard kill, and the tool reports `SEARCH_ABORTED`. The working directory is the session header cwd when present, else `process.cwd()` — there is no executor config to default through anymore, so the tool owns the fallback.
The `fs-glob-sampling` ACP snapshot scenario now executes the real packaged binary against a prepared workspace whose fixed mtimes pin the `--sort=modified` order, replacing the PATH-injected `rg` stand-in (POSIX-only, because the displayed paths carry `/` separators the session-log comparison cannot normalize).
## Alternatives considered
**Keep the bash seam and probe, but document `rg` as a required host dependency.** Rejected: the host dependency is exactly the failure this change removes, and Windows support for the discovery tools was the point of the exercise; a documented requirement is still a requirement.
**Make `rgPath` injectable (a config field or env override) so tests and snapshots keep substituting a stand-in binary.** Rejected: it adds a public deployment surface whose only consumer would be test seams, and the real binary is deterministic enough to pin directly through fixture mtimes — the packaged binary is the deployment, so tests should exercise it.
**Switch to a pure-JS glob/search engine (e.g. `picomatch`/`tinyglobby`).** Rejected: the [dependency-swaps audit](../../rejected/simplification/2026-07-26-dependency-swaps-rejected-by-nih-audit.md) already rejected that on the "no glob engine exists" evidence; ripgrep semantics (`--sort=modified`, VCS pruning, JSON transport, regex dialect) are the tool contract.
## Consequences
- The discovery tools work on every platform the packaged binary covers (darwin/linux/win32, x64/arm64) with no host install; the shipped TUI/Web rosters gain `glob`/`grep` as fixed members ([even-out-shipped-tool-rosters](../feature/2026-07-31-even-out-shipped-tool-rosters.md)).
- The shell-string attack surface is gone: hostile patterns are inert argv elements, pinned by the integration suite, which now runs on Windows too (it previously self-skipped without a system `rg`).
- The spawn is unconfined (a plain `ctx.subprocess` call), so `--no-config` is prepended: a host `RIPGREP_CONFIG_PATH` (or an `rg.conf` beside the binary) can otherwise inject a `--pre` preprocessor that executes an arbitrary command for every matched file. With `--no-config`, no config file — and therefore no preprocessor — can reach the search.
- The raw-output overflow path changed shape: the old bash-backed route inherited bash-local's always-on spill and could leave an unread multi-megabyte temp file; the subprocess seam now collects without spill, and overflow is a pure error (`SEARCH_RAW_OUTPUT_OVERFLOW`, "narrow pattern, path, or include and retry") with zero content returned.
- Load-time failure modes changed: a broken subprocess seam now fails the first search call (`SEARCH_FAILED`) instead of failing plugin load through the probe; a missing binary is a launch failure with the packaged path, not a PATH problem.
- The integration suite's fixture dropped a filename Windows cannot represent (`"` in a name), keeping the suite replayable on every platform.
- Regenerating `THIRD_PARTY_NOTICES.md` surfaced a latent generator bug the new dependency made visible: Node's `fs.globSync` returns OS-native separators, so on Windows the `/`-suffixed dev-area prefixes in the notices tiering never matched and dev-only packages (test tooling, support leaves) were mis-tiered as runtime. The generator now normalizes manifest paths at ingestion, and the notices are platform-independent.
- The `@vscode/ripgrep` dependency adds its MIT row to the runtime tier, and pnpm 11's truncated virtual-store directory names needed a content-scan fallback in the notices generator's metadata lookup.

View File

@@ -0,0 +1,38 @@
# Agent Note: glob/grep 改用打包的 ripgrep 二进制直接 spawn
Status: implemented
[English](2026-08-01-packaged-ripgrep-search.md) | 中文
> 取代 [bash 承载的 grep/glob 发现工具](../../archived/feature/2026-07-09-bash-backed-grep-glob-discovery.md):v1 决策中明确延期的方案——直接 spawn ripgrep——现在成为实际交付的实现。
## 问题
`glob`/`grep` 工具经由 bash 执行器 seam 运行,这使系统 `rg` 安装成为宿主依赖。Windows 和容器镜像的 `PATH` 默认没有 `rg`,工具在那里会静默消失;部署方只能从加载期探针警告里发现这一点。bash seam 还迫使整个模型可见参数面经过一个 shell 引号工具,因为工具与 ripgrep 之间隔着一层 shell——[bash 承载决策](../../archived/feature/2026-07-09-bash-backed-grep-glob-discovery.md) 把这种耦合记为 v1 的取舍,并把直接 spawn 列为 shell 字符串域一旦被证明过于敏感时的合理后续。它确实被证明了:每个模型值都要经受 POSIX 单引号转义,探针要在测试里脚本化,执行器自身的超时分类还与协作式工具超时策略已有的职责重复。
## 决策
`@deepseek-ai/dsh-tool-fs-search` 现在运行 PACKAGED(打包的)ripgrep 二进制(`@vscode/ripgrep`,一个 npm 依赖,其可选平台包随附二进制),经由 `ctx.subprocess` seam:`runRipgrep()` 以纯 argv 向量 spawn `rgPath`,向量前缀 `--no-config`,配以 collect 模式 stdout/stderr、`graceMs` 与转发的 `exec.signal`。`rgPath` 在首次调用时懒解析(进程内 memoize):`@vscode/ripgrep` 在模块求值阶段解析其平台包,静态导入会把平台包缺失/损坏(`--omit=optional`、安装不全)变成 Loader 组合加载失败——这正是本次改动要消除的加载期失败模式。不再有 shell 层,执行路径上的 shell 引号边界随之消失;`singleQuote` 工具与其 shell spawn 测试一并删除。原始流使用 seam 的诊断尾部 collect 形态(无 spill 文件——工具从不读取原始 spill 路径;lossy stdout 读取以 `SEARCH_RAW_OUTPUT_OVERFLOW` 失败)。终止宽限与 stderr 尾部预算成为经校验的 `Config` 字段(`graceMs` 默认 3000,`stderrMaxBytes` 默认 64 KiB),不再继承自 bash-local 的配置。注册变为无条件——加载期 `command -v rg` 探针与条件注册决策被删除,连同那条 "rg not found" 警告。本包注入 `tools`、`systemPrompt` 与 `subprocess`。
退出语义仍由工具拥有:退出码 0 为有结果的成功,1 为成功的空搜索,其余归入既有 `SEARCH_*` 词汇(无效模式、启动失败、信号杀死、原始输出溢出)。超时是挂在工具定义上的协作式工具调用预算:`@deepseek-ai/dsh-timeout-policy` 中止 `exec.signal`,subprocess seam 的终止升级提供硬终止,工具报告 `SEARCH_ABORTED`。工作目录为会话 header cwd(存在时),否则为 `process.cwd()`——不再有执行器配置可供默认化,因此回退由工具自己拥有。
`fs-glob-sampling` ACP 快照场景改为执行真实的打包二进制,作用于一个用固定 mtime 钉住 `--sort=modified` 顺序的预制工作区,取代 PATH 注入的 `rg` 替身(仅 POSIX:展示路径携带 `/` 分隔符,会话日志比较无法归一化)。
## 备选方案
**保留 bash seam 与探针,仅把 `rg` 记为必需宿主依赖。** 否决:宿主依赖正是本次改动要消除的失败模式,而让发现工具支持 Windows 正是此举的目的;写进文档的依赖仍是依赖。
**让 `rgPath` 可注入(配置字段或环境变量覆盖),让测试与快照继续替换替身二进制。** 否决:这会新增一个只有测试 seam 会消费的公开部署面,而真实二进制本身足够确定——通过 fixture mtime 即可直接钉住;打包二进制就是部署形态,测试应当拿它来测。
**改用纯 JS 的 glob/搜索引擎(如 `picomatch`/`tinyglobby`)。** 否决:[依赖替换审计](../../rejected/simplification/2026-07-26-dependency-swaps-rejected-by-nih-audit.md) 已基于"不存在 glob 引擎"的证据否决过该方向;ripgrep 语义(`--sort=modified`、VCS 剪枝、JSON 传输、正则方言)就是工具契约。
## 后果
- 发现工具在打包二进制覆盖的每个平台(darwin/linux/win32,x64/arm64)上开箱即用,无需宿主安装;交付的 TUI/Web 工具清单把 `glob`/`grep` 变为固定成员(见 [拉平交付的工具清单](../feature/2026-07-31-even-out-shipped-tool-rosters.md))。
- shell 字符串攻击面消失:恶意模式只是惰性 argv 元素,由集成套件钉住;该套件现在也在 Windows 上运行(此前没有系统 `rg` 时它自行跳过)。
- spawn 不受沙箱约束(普通的 `ctx.subprocess` 调用),因此前缀 `--no-config`:宿主的 `RIPGREP_CONFIG_PATH`(或二进制旁的 `rg.conf`)否则可注入 `--pre` 预处理器,对每个匹配文件执行任意命令。加上 `--no-config` 后,任何配置文件——因而任何预处理器——都无法触及搜索。
- 原始输出溢出路径的形态改变:旧的 bash 承载路径继承了 bash-local 常开的 spill,可能留下没人读的多 MB 临时文件;subprocess seam 现在无 spill 收集,溢出是纯粹的错误(`SEARCH_RAW_OUTPUT_OVERFLOW`,"narrow pattern, path, or include and retry"),不返回任何内容。
- 加载期失败模式改变:subprocess seam 损坏现在让首次搜索调用失败(`SEARCH_FAILED`),而非通过探针使插件加载失败;二进制缺失是带打包路径的启动失败,而不是 PATH 问题。
- 集成套件的 fixture 去掉了 Windows 无法表示的文件名(名称含 `"`),保证套件在每个平台都能重放。
- 重新生成 `THIRD_PARTY_NOTICES.md` 暴露了一个由新依赖带出的潜在生成器 bug:Node 的 `fs.globSync` 返回操作系统原生分隔符,因此在 Windows 上 notices 分层中带 `/` 后缀的 dev 区前缀永远匹配不上,dev-only 包(测试工具、support 叶子)被错分为 runtime。生成器现在在入口处归一化清单路径,notices 与平台无关。
- `@vscode/ripgrep` 依赖为 runtime 层增加其 MIT 行;pnpm 11 截断的虚拟存储目录名需要在 notices 生成器的元数据查找中增加内容扫描回退。