news 2026/9/25 3:24:54

oh-my-opencode-slim 的 search-path-guard:在 tool.execute.before 中精准拦截无效 grep/glob 搜索路径

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
oh-my-opencode-slim 的 search-path-guard:在 tool.execute.before 中精准拦截无效 grep/glob 搜索路径
  • 人工智能
  • AI Agent
  • Agent 编排
  • AI 技能

【免费下载链接】oh-my-opencode-slim

Lean, fine tuned Opencode multi agent suite · Mix any models · Auto delegate tasks

项目地址:https://gitcode.com/gh_mirrors/oh/oh-my-opencode-slim
点击查看免费下载

导读

在 OpenCode 多智能体插件体系里,grep与glob是 Agent 检索仓库信息最频繁调用的宿主工具,而一旦路径参数指向不存在或结构非法的位置,宿主只会抛出一条晦涩难懂的ripgrep execution failed错误,让排查无从下手。oh-my-opencode-slim 通过src/hooks/search-path-guard/这个tool.execute.before钩子,在搜索工具真正执行之前完成路径解析、文件系统校验与分级错误报告,把"宿主失败"提前转化为"可行动的提示"。读完本文,你将掌握该钩子的设计骨架、路径解析的宿主语义细节、错误分类策略,以及它如何在插件主流程中与其他钩子协同,并可通过仓库中的完整测试用例复现每一种拦截与放行行为。

钩子的职责边界:只服务 grep 与 glob

search-path-guard 的全部职责可以浓缩为一句话:在宿主工具执行前校验搜索路径,防止 ripgrep 执行失败。它挂载在tool.execute.before阶段,拦截范围被刻意收窄为grep与glob两类工具(源码见 index.ts 中的if (input.tool !== 'grep' && input.tool !== 'glob') return;),从而实现对搜索操作的高精度路径校验,同时避免干扰其他工具类别——例如read、bash、write等工具的路径参数完全不受影响,这一点在测试中也有明确覆盖。

钩子要解决的三类痛点非常具体:

  • 路径存在性:解析后的路径必须真实存在,ENOTDIR(路径某个组成部分不是目录)这类非法路径要给出可行动的报错,而不是让宿主把原始错误抛给调用者;
  • 错误可读性:缺失路径要上报"带指引"的帮助信息,替代晦涩的ripgrep execution failed;
  • 宿主语义对齐:路径解析必须严格镜像宿主工具各自的语义——v1 的 grep 使用path.join,v1 的 glob 使用path.resolve,而 v2 宿主对两者都使用path.resolve。

核心架构:工厂函数 + 可注入路径操作

钩子的整体结构是标准插件钩子工厂模式(codemap.md):

  • createSearchPathGuardHook(ctx):接收PluginInput(含ctx.directory与可选的ctx.hostFlavor),返回tool.execute.before处理器;
  • resolveSearchPath():实现与宿主工具逐字节对齐的路径解析规则;
  • Path 参数提取:只从工具调用的args.path中取值;
  • Stat 校验:通过fs.statSync做文件系统检查,并按错误码分派处理;
  • 错误上报:依据错误类型给出带实操指引的提示。

值得强调的是"无外部配置"的设计取向:行为完全由宿主 flavor 探测驱动,不需要用户维护任何额外配置文件。两个可注入/可探测的入口分别是hostFlavor(可选参数,用于 v1/v2 宿主差异)与pathOperations(注入的路径工具,专为确定性测试设计)。

resolveSearchPath 的精确实现

路径解析是全部语义的核心,源码实现(index.ts)如下:

export function resolveSearchPath( tool: string, hostFlavor: string | undefined, directory: string | undefined, raw: string, pathOperations: PathOperations = path, ): string | null { if (!directory) return null; if (pathOperations.isAbsolute(raw)) return raw; if (hostFlavor === 'v2' || tool === 'glob') { return pathOperations.resolve(directory, raw); } return pathOperations.join(directory, raw); }

几个关键设计决策:

  1. 无基准目录则保守放行:ctx.directory不可用时返回null,上层据此"绝不拦截"(注释明确写着 "Without a resolution base, never block (conservative fallback)"),避免在缺少工作区上下文时误伤正常调用;
  2. 绝对路径快速通道:isAbsolute(raw)为真时原样返回,跳过拼接逻辑——这既是性能优化,也保证绝对路径不会因 join/resolve 的语义差异而被错误改写;
  3. v1 与 v2 的差异被精确复刻:v1 grep 走join、v1 glob 走resolve、v2 一律走resolve。这个区别在 Windows 上尤其重要,因为join与resolve对驱动器相对路径(如C:src)的处理结果完全不同。源码注释明确点出:"Without a resolution base, never block" 之外,还特别提到这是为了对齐宿主:"Mirror each host tool's resolution rule exactly... that distinction matters for relative paths on Windows, including drive-relative paths such asC:src."

错误处理策略:ENOENT 上报、ENOTDIR 阻断、其余透传

钩子的错误分类是它区别于"一刀切拦截"的关键(对应 codemap.md 的 Error Handling Strategy 与 Error Reporting Flow):

错误码语义钩子行为错误消息示例
ENOENT路径(含断链的软链接)确实不存在上报缺失,抛出带指引的错误Search path does not exist: {resolved} (from "{raw}"). ... Verify the target path, or list its parent directory to find the correct location.
ENOTDIR路径的某个组成部分不是目录阻断非法路径,抛出明确错误Search path is invalid: {resolved} (from "{raw}"). A path component is not a directory (ENOTDIR), so the {tool} search was blocked before ripgrep ran. ...
其他(权限、I/O 等)stat 失败但语义不属于上述两类透传,保持原始含义,绝不误诊不修改、不追加,仅记录日志

这个"三态分流"非常克制:ENOTDIR与ENOENT是 Agent 最容易踩中的两类错误(路径写错、把文件当目录用),因此值得在宿主执行前拦截并改写为可行动的提示;而权限问题(EACCES 等)可能是真实的宿主环境限制,透传比武断阻断更诚实。源码中的注释佐证了这一点:"Any other stat failure (permissions or I/O) keeps its original meaning: pass through and never misdiagnose."

钩子执行流程总览

原文档给出的完整流程,自上而下贯穿一次 grep/glob 调用的生命周期:

Tool execution (grep/glob) ↓ Tool execute before hook ↓ Validate tool type (grep/glob only) ↓ Extract path argument from tool args ↓ Resolve path using host-appropriate resolution ↓ Validate path exists and is accessible ├─ ENOENT → report missing path ├─ ENOTDIR → block invalid path └─ other error → pass through ↓ Proceed with original tool execution (success case)

路径解析子流程(即上文的resolveSearchPath):

Input: raw path, directory, hostFlavor ↓ Check if absolute path (return as-is) ↓ Resolve using host-specific logic: - v2 or glob → path.resolve(directory, raw) - v1 grep → path.join(directory, raw) ↓ Return resolved path or null if directory unavailable

主流程中的注册顺序:一次深思熟虑的编排

search-path-guard 并非孤立运行,它在插件主入口 src/index.ts 中的注册位置体现了严格的时序考量。createSearchPathGuardHook(ctx)在插件初始化时创建(src/index.ts),随后被组装进tool.execute.before复合处理器(src/index.ts):

'tool.execute.before': async (input, output) => { await applyPatch'tool.execute.before'; // Rewrite guessed non-existing absolute paths BEFORE the search guard: // the guard blocks grep/glob on missing paths, so running the rescue // after it would never see a rescuable path (#1143). await absolutePathRescue'tool.execute.before'; await searchPathGuard'tool.execute.before'; await taskSessionManagerHook'tool.execute.before'; // Record a call only after all rejecting before-hooks have accepted it. // In particular, search-path-guard can reject grep/glob before the host // emits tool.execute.after; running the loop guard first would leave a // pending call-key entry with no completion to consume it. await toolLoopGuard'tool.execute.before'; },

这里透露出两个关键协作细节:

  1. absolute-path-rescue 必须先于 search-path-guard 执行:absolute-path-rescue 负责把 Agent 猜错的绝对路径重锚定到工作区真实位置,而 guard 会直接阻断不存在的路径。如果两者顺序颠倒,被 rescue 视为可挽救的路径在 guard 阶段就会被拦下,永远没有机会被改写(对应 issue #1143 的教训,注释明确说明了这一点);
  2. tool-loop-guard 必须后于 search-path-guard 执行:tool-loop-guard 负责记录工具调用键,而 search-path-guard 可能在宿主发出tool.execute.after之前就拒绝 grep/glob。若 loop guard 先记录,就会留下一个没有完成事件来消费的悬空调用键(pending call-key),因此调用记录必须放在所有可能拒绝的 before-hook 接受之后。

此外,钩子整体通过 src/hooks/index.ts 的 barrel 导出(export { createSearchPathGuardHook } from './search-path-guard';)提供给主入口,与 apply-patch、absolute-path-rescue、tool-loop-guard 同属"工具拦截"这一钩子类别(见 hooks/codemap.md 的 Hook Categories 表)。

细节语义:字面量、空白与 Windows 驱动路径

源码注释与测试共同揭示了几个容易被忽略、但直接影响 Agent 行为的边界语义(index.ts):

  • 宿主不做 trim:路径参数按原样使用,前后空白是有效字符。因此' padded-missing-dir '会被当作真实路径名参与解析并最终因不存在而被阻断——这符合"镜像宿主"原则,因为宿主从不裁剪路径。测试同时验证了带空白前缀的目录名可以正常通过('spaced dir '目录存在时放行);
  • 字面量'undefined'/'null'是普通相对路径:上游宿主把它们当作普通相对路径解析(schema 描述中的说明并不代表运行时特殊处理),因此钩子也按普通路径对待——指向不存在的'undefined'路径时同样会触发 ENOENT 阻断,测试用path.join(tempRoot, 'undefined')断言了这一行为;
  • 空字符串与缺失/非字符串参数一律放行:args缺失、path缺失、path: 42、path: null、path: ''都不会被拦截。其中空字符串解析结果等价于实例目录本身,没有可阻断的对象;
  • ENOENT 涵盖断链软链接:源码注释指出 broken symlink 属于"genuine missing path",同样上报缺失而非透传,这与上游正在修复的行为保持一致。

测试验证:18 个场景覆盖全部语义分支

index.test.ts是理解钩子行为的另一份权威文档,它通过mkdtemp构造真实临时目录、注入 fakedirectory/hostFlavor,并借助path.win32注入模拟 Windows 语义(这正是pathOperations可注入设计的价值——没有 Windows CI 也能确定性验证 Windows 行为)。测试矩阵(index.test.ts)覆盖:

  • 缺失路径阻断:绝对路径不存在(grep)、相对路径不存在于目录下(glob)均抛出Search path does not exist且消息包含解析后的完整路径;
  • 有效路径放行:存在的绝对文件(grep/glob)、存在的相对目录(glob)均无异常;
  • 目录缺失保守放行:directory为假值时绝不阻断相对路径;
  • 工具过滤:read、bash等非 grep/glob 工具即使路径缺失也直接忽略;
  • 参数形状容错:undefined、空对象、非字符串、null、空字符串均忽略;
  • ENOTDIR 阻断:把普通文件当路径组成部分('reg-file.txt/child'),grep 与 glob 均抛出包含not a directory与ENOTDIR的可行动错误;
  • join/resolve 语义分离:同一相对路径下,grep 解析为path.join(directory, raw)而 glob 解析为path.resolve(directory, raw),测试分别断言;
  • Windows 驱动相对路径:在 win32 平台上,'C:missing-search-path'按各自语义解析;v2 的 grep 对'C:src'使用 win32resolve语义,v1 使用join语义(通过path.win32注入验证,即使跑在非 Windows CI 上)。

依赖、性能与可观测性

依赖清单

钩子的依赖极简(codemap.md):

  • Node.jsfs.statSync:文件系统校验;
  • node:path:isAbsolute/join/resolve路径解析(且类型上仅Pick这三个方法,约束了注入面的范围);
  • 结构化日志器:来自 src/utils/logger.ts 的log(),记录验证决策与错误分类;该日志器在落盘前统一经过redactSecretsForLog脱敏,保证路径等上下文不会泄露凭据;
  • 插件 SDK:PluginInput类型用于钩子注册与hostFlavor/directory上下文获取。

性能设计

  • 早期过滤:只处理 grep/glob,其余工具零开销;
  • 单次 stat:每次校验最多一次statSync,无额外系统调用;
  • 绝对路径快速通道:跳过拼接、直接返回;
  • 确定性可注入:pathOperations让测试无需真实文件系统也能覆盖平台差异,生产中则使用 Node 原生路径实现,与宿主完全一致。

可观测性与无副作用承诺

  • 日志追踪:阻断非法路径、透传 stat 错误、上报缺失路径时都会记录带上下文的日志条目(工具名、原始路径、解析路径、错误码);
  • 错误消息即指引:两类错误文案都包含"验证目标路径 / 列出父目录寻找正确位置 / 检查每个父组件是否为目录"等可执行建议;
  • 只拦截不改写:钩子对有效路径不做任何修改,唯一副作用是"提前阻断无效调用",因此对宿主行为是透明且安全的。

总结:从"宿主报错"到"钩子指引"的范式转变

search-path-guard 的价值不在于发明新的搜索能力,而在于把失败边界前移:与其让 Agent 面对ripgrep execution failed这类无法归因的宿主错误,不如在tool.execute.before阶段就用一次 stat 调用判断出"路径不存在(ENOENT)"还是"路径结构非法(ENOTDIR)",并分别给出可行动的修复指引;对权限/IO 等不属于路径问题的错误则保持克制、原样透传。它与 absolute-path-rescue(先改写猜测路径)、tool-loop-guard(后记录调用键)的注册顺序,以及 join/resolve 的宿主语义镜像,共同构成了一个严谨、可测试、低开销的工具调用前校验层——任何在 OpenCode 生态中构建插件的开发者,都可以把这套"错误分类 + 语义镜像 + 注入式测试"的方法论直接迁移到自己的钩子设计中。

  • 人工智能
  • AI Agent
  • Agent 编排
  • AI 技能

【免费下载链接】oh-my-opencode-slim

Lean, fine tuned Opencode multi agent suite · Mix any models · Auto delegate tasks

项目地址:https://gitcode.com/gh_mirrors/oh/oh-my-opencode-slim
点击查看免费下载
上一篇:量化策略失效预警:FinRL-Library漂移检测终极指南
下一篇:Local Deep Research v1.0 迁移指南:用户认证、SQLCipher 加密数据库与 FastAPI 部署契约变更

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 3:23:48

just-bash威胁模型深度拆解:AI Agent沙盒的3类攻击者与5道信任边界

just-bash威胁模型深度拆解:AI Agent沙盒的3类攻击者与5道信任边界 【免费下载链接】just-bash Bash for Agents 项目地址: https://gitcode.com/gh_mirrors/ju/just-bash just-bash 是一个为 AI Agent 打造的沙盒 Bash 解释器——用 TypeScript 实现、内置内…

作者头像 李华
网站建设 2026/9/25 3:20:01

Oceanology_FluidNinja水体波纹交互条件

插件:Oceanology_Plugin、WaterInteractionPlugin、FluidNinjaLive一、可以实现水体波纹交互的条件1.必须是蓝图 2.蓝图轴心也可产生交互,要不想要轴心交互需将模型体碰撞复杂度改为“将复杂碰撞改为简单碰撞” 3.必须是UE自带的几何体才会产生交互&…

作者头像 李华