DSH / Atlas
2026-08-01implementedarchitecture

Packaged ripgrep spawn for glob/grep

glob/grep 改用打包的 ripgrep 二进制直接 spawn

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 betwee

English

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 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-tool-call-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 hooks, 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 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).
  • 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.

中文

问题

glob/grep 工具经由 bash 执行器 seam 运行,这使系统 rg 安装成为宿主依赖。Windows 和容器镜像的 PATH 默认没有 rg,工具在那里会静默消失;部署方只能从加载期探针警告里发现这一点。bash seam 还迫使整个模型可见参数面经过一个 shell 引号工具,因为工具与 ripgrep 之间隔着一层 shell——bash 承载决策 把这种耦合记为 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.signalrgPath 在首次调用时懒解析(进程内 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" 警告。本包注入 toolssystemPromptsubprocess

退出语义仍由工具拥有:退出码 0 为有结果的成功,1 为成功的空搜索,其余归入既有 SEARCH_* 词汇(无效模式、启动失败、信号杀死、原始输出溢出)。超时是挂在工具定义上的协作式工具调用预算:@deepseek-ai/dsh-tool-call-timeout-policy 中止 exec.signal,subprocess seam 的终止升级提供硬终止,工具报告 SEARCH_ABORTED。工作目录为会话 header cwd(存在时),否则为 process.cwd()——不再有执行器配置可供默认化,因此回退由工具自己拥有。

fs-glob-sampling ACP(Agent Client Protocol)快照场景改为执行真实的打包二进制,作用于一个用固定 mtime 钉住 --sort=modified 顺序的预制工作区,取代 PATH 注入的 rg 替身(仅 POSIX:展示路径携带 / 分隔符,会话日志比较无法归一化)。

备选方案

保留 bash seam 与探针,仅把 rg 记为必需宿主依赖。 否决:宿主依赖正是本次改动要消除的失败模式,而让发现工具支持 Windows 正是此举的目的;写进文档的依赖仍是依赖。

rgPath 可注入(配置字段或环境变量覆盖),让测试与快照继续使用替身二进制。 否决:这会新增一个只有测试钩子会消费的公开部署面,而真实二进制本身具有足够的确定性——通过 fixture(测试前置数据)的 mtime 即可直接钉住;打包二进制就是部署形态,测试应当拿它来测。

改用纯 JS 的 glob/搜索引擎(如 picomatch/tinyglobby)。 否决:依赖替换审计 已基于「不存在 glob 引擎」的证据否决过该方向;ripgrep 语义(--sort=modified、VCS 剪枝、JSON 传输、正则方言)就是工具约定。

后果

  • 发现工具在打包二进制覆盖的每个平台(darwin/linux/win32,x64/arm64)上开箱即用,无需宿主安装;交付的 TUI/Web 工具清单把 glob/grep 变为固定成员(见 拉平交付的工具清单)。
  • 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 叶子)被错分为运行时。生成器现在在入口处归一化 manifest(元数据清单)路径,notices 与平台无关。
  • @vscode/ripgrep 依赖为运行时层增加其 MIT 行;pnpm 11 截断的虚拟存储目录名需要在 notices 生成器的元数据查找中增加内容扫描回退。