Merge commit 'ecbf75a5e70f662b6420375140cf12eb6bac7860' into worktree/retarget-pr828-20260729
# Conflicts: # packages/hooks/README.i18n.yaml # packages/hooks/README.zh.md # packages/hooks/hook-protocol/README.i18n.yaml # packages/hooks/hook-protocol/README.zh.md # packages/hooks/hooks-claude/README.i18n.yaml # packages/hooks/hooks-claude/README.zh.md # packages/hooks/hooks-codex/README.i18n.yaml # packages/hooks/hooks-codex/README.zh.md
This commit is contained in:
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/hooks/hook-protocol/README.md
|
||||
README.md: 3a44aaaf17034b310a91ac9686bd9a0d6690de11
|
||||
README.zh.md: 2adae13dd10cc0a0c38791be604b83283698d892
|
||||
README.zh.md: 3e3e56f9af740a973b5765aa57467ebe09a27c83
|
||||
|
||||
@@ -2,34 +2,34 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
Claude Code/Codex hook 协议格式的**共享核心**。它不是 cordis 插件:不注册也不注入任何内容。它是一个**库**,提供两个桥接插件(`@deepseek-ai/dsh-hooks-claude`、`@deepseek-ai/dsh-hooks-codex`)导入的方言无关原语,使两者都无需重复实现协议中相同的部分。
|
||||
Claude Code/Codex hook 协议格式(wire format)的**共享核心**。它不是 Cordis 插件:不注册也不注入任何内容。它是一个**库**,提供两个桥接插件(`@deepseek-ai/dsh-hooks-claude`、`@deepseek-ai/dsh-hooks-codex`)导入的方言无关原语,使两者都无需重复实现协议中相同的部分。
|
||||
|
||||
共享 lib 存在的原因是:Codex 有意重新实现了 Claude Code hook 协议的一个*子集*,包括相同的 `hooks.json` matcher group 形状、相同的退出码/stdout 输出契约以及相同的 command hook 执行模式。真正共享的部分位于此处;每个桥接只拥有不同之处。
|
||||
共享 lib 存在的原因是:Codex 有意重新实现了 Claude Code hook 协议的一个*子集*,包括相同的 `hooks.json` matcher group 结构、相同的退出码/stdout 输出契约以及相同的 command hook 执行模式。真正共享的部分位于此处;每个桥接只负责不同的部分。
|
||||
|
||||
## 共享内容(此处)与每方言内容(桥接)
|
||||
## 共享内容(此处)与各方言内容(桥接)
|
||||
|
||||
| 关注点 | 此处(`dsh-hook-protocol`) | 桥接(`dsh-hooks-claude` / `-codex`) |
|
||||
|---|---|---|
|
||||
| Matcher 校验 + 测试 | `compileMatchers(patterns, mode)` 从同一 registry 提供诊断与配置生命周期内的重复匹配;Codex 使用有界且跨重载稳定的 Rust-regex interner;`matcherDiagnostic`/`matchesMatcher` 是收敛的一次性 helper | 选择自身原生正则 `mode`(`claude` = JavaScript,`codex` = Rust `regex`),将可运行的唯一 pattern 只编译一次,拒绝带有 registry 诊断的配置组,并在失败或 teardown 时释放配置 registry |
|
||||
| Matcher 校验 + 测试 | `compileMatchers(patterns, mode)` 从同一注册表提供诊断与配置生命周期内的重复匹配;Codex 使用有界且跨重载稳定的 Rust 正则 interner;`matcherDiagnostic`/`matchesMatcher` 是隔离的一次性辅助函数 | 选择自身原生正则 `mode`(`claude` = JavaScript,`codex` = Rust `regex`),将可运行的唯一 pattern 只编译一次,拒绝带有注册表诊断的配置组,并在失败或 teardown 时释放配置注册表 |
|
||||
| 运行 hook | `runHook(bash, hook, opts, now)`:通过 `ctx.bash` 提供 stdin payload + env,再解码 | 构造每个事件的 stdin **payload** + 该方言的 **env** |
|
||||
| 解码输出 | `parseHookOutput(exit, stdout, stderr)` → 中性 `HookOutput` | 将中性 `HookOutput` 映射到 seam 特定的类型化 Decision |
|
||||
| 合并 N 个 hook | `mergeHookOutputs(outputs)` → 最严格的 `MergedHookOutcome` | (无) |
|
||||
| 持久记录 | `appendHookInvoked` / `appendHookResult`(`hook/*` 会话事件;结果的 `decision`/`stderrSummary` 从此处的 `HookOutput` 派生) | 在每次调用前后调用它们 |
|
||||
| 脱离运行完全停稳 | `createDetachedRuns()`:跟踪发射后不再等待的运行链;`drain()` 先 abort,再等待它们 | 将 `signal` 传给每个脱离的 `runHook`,并将 `drain` 注册为 effect disposer |
|
||||
| 脱离运行完全停稳 | `createDetachedRuns()`:跟踪触发后不等待的运行链;`drain()` 先 abort,再等待它们 | 将 `signal` 传给每个脱离的 `runHook`,并将 `drain` 注册为 effect disposer |
|
||||
|
||||
## 原语
|
||||
|
||||
- **`compileMatchers(matchers, mode)` / `matcherDiagnostic(matcher, mode)` / `matchesMatcher(matcher, query, mode)`**:缺失、`''` 或 `'*'` 时匹配全部;两种方言都将纯 `[A-Za-z0-9_|]+` pattern 视为按 pipe 分隔的精确多选。其他 pattern 会用原生方言编译为未锚定正则:Claude Code 使用 JavaScript,Codex 使用 Rust `regex`(包括 `(?i)` 等内联 flag)。桥接解析器会先丢弃没有 matcher 匹配对象的事件所带字段,收集其余可运行 group,再将它们的唯一 pattern 只编译一次。解析器直接从这些实例读取 `registry.diagnostic(pattern)`;pattern 被拒绝时,会先释放配置 registry 再抛错,否则把该 registry 交给运行时,并在 teardown 时先 drain 脱离运行,再释放它。Codex 的有效实例和无效诊断会 intern 在同步 `rregex` 依赖模块上,因此无需使用 `globalThis`,也能跨 hook-protocol/Cordis 重载保留;一次性 helper 共享同一 interner。由于 `rregex` 在 `free()` 后也不能缩小 WASM 分配,进程会有意最多保留 `MAX_INTERNED_CODEX_REGEX_PATTERNS`(128)个不同的非字面 pattern。容量用满后,新的不同 pattern 会在调用 WASM 前被容量诊断拒绝;已经 intern 的 pattern 继续工作,重启进程会重置预算。这样既覆盖相同 pattern 重载,也能在恶意唯一 pattern 重载下保持有界,而无需无界缓存。
|
||||
- **`runHook(bash, hook, options, now)`**:要求并转发调用方拥有的 `options.signal`,将 `options.payload` 序列化到 hook stdin(当且仅当 `options.trailingNewline` 时添加尾随换行符),在执行器凭证清理后合并 `options.env`(`dsh-bash` 受信任插件表层),遵循 hook 的 `timeoutSec`(否则使用 `options.defaultTimeoutMs`;默认值属于桥接,其配置默认为 lib 的 `DEFAULT_HOOK_TIMEOUT_MS` 10 分钟参考值),再解码结果(将 `options.expectedEventName` 传递给 codec)。因此取消会到达执行器的进程组终止与 join 边界。它绝不抛出异常:执行器拒绝(基础设施故障)会变为 `HookOutput`,其 `exitCode: undefined`(非阻塞错误)。`now` 会被注入,以便测试持续时间。
|
||||
- **`parseHookOutput(exitCode, stdout, stderr, expectedEventName?)`** 解码退出状态与结构化 stdout。退出码 2 使用 stderr 阻塞;其他失败不阻塞。匹配的 hook 特定权限决策会覆盖遗留顶层决策;事件判别字段不匹配或缺失只会抑制事件特定字段。顶层字段仍与事件无关,成功但非 JSON 的输出会留给桥接处理。
|
||||
- **`mergeHookOutputs(outputs)`**:折叠在一个点上匹配的每个 hook 结果:权限优先级为 **deny > ask > allow**,首个 `continue:false` 使 halt 粘滞,阻塞原因用 `\n\n` 连接,`additionalContext`/`systemMessages` 按顺序累积。
|
||||
- **`createDetachedRuns()`**:为脱离运行的 emit 形状点跟踪完全停稳(没有 seam 等待它们)。桥接会跟踪每条运行链,包括 hook 运行及其 continuation,并将 `drain()` 注册为 effect disposer。drain 会触发 tracker 的 abort `signal`(因此仍在运行的 hook 进程会通过 `runHook` 终止,而不是等待到超时),随后在所有已跟踪链结算后 resolve。因此 `fiber.dispose()` resolve 时,没有脱离 hook 工作会留下并触发已 dispose 的上下文(见 [防御模式](../../../docs/defensive-patterns.md):dispose 必须达到完全停稳)。
|
||||
- **`compileMatchers(matchers, mode)` / `matcherDiagnostic(matcher, mode)` / `matchesMatcher(matcher, query, mode)`**:缺失、`''` 或 `'*'` 时匹配全部;两种方言都将纯 `[A-Za-z0-9_|]+` pattern 视为按 pipe 分隔的精确多选。其他 pattern 会用原生方言编译为未锚定正则:Claude Code 使用 JavaScript,Codex 使用 Rust `regex`(包括 `(?i)` 等内联 flag)。桥接解析器会先丢弃没有 matcher 匹配对象的事件所带字段,收集其余可运行的配置组,再将它们的唯一 pattern 只编译一次。解析器直接从这些实例读取 `registry.diagnostic(pattern)`;pattern 被拒绝时,会先释放配置注册表再抛错,否则把该注册表交给运行时,并在 teardown 时先 drain 脱离运行,再释放它。Codex 的有效实例和无效诊断会 intern 在同步 `rregex` 依赖模块上,因此无需使用 `globalThis`,也能跨 hook-protocol/Cordis 重载保留;一次性辅助函数共享同一 interner。由于 `rregex` 在 `free()` 后也不能缩小 WASM 分配,进程会有意最多保留 `MAX_INTERNED_CODEX_REGEX_PATTERNS`(128)个不同的非字面 pattern。容量用满后,新的不同 pattern 会在调用 WASM 前被容量诊断拒绝;已经 intern 的 pattern 继续工作,重启进程会重置预算。这样既覆盖相同 pattern 重载,也能在恶意唯一 pattern 重载下保持有界,而无需无界缓存。
|
||||
- **`runHook(bash, hook, options, now)`**:要求并转发调用方拥有的 `options.signal`,将 `options.payload` 序列化到 hook stdin(当且仅当 `options.trailingNewline` 时添加尾随换行符),在执行器凭证清理后合并 `options.env`(`dsh-bash` 受信任插件接口),遵循 hook 的 `timeoutSec`(否则使用 `options.defaultTimeoutMs`;默认值属于桥接,其配置默认为 lib 的 `DEFAULT_HOOK_TIMEOUT_MS` 10 分钟参考值),再解码结果(将 `options.expectedEventName` 传递给 codec)。因此取消会到达执行器的进程组终止与 join 边界。它绝不抛出异常:执行器拒绝(基础设施故障)会变为 `HookOutput`,其 `exitCode: undefined`(非阻塞错误)。`now` 会被注入,以便测试持续时间。
|
||||
- **`parseHookOutput(exitCode, stdout, stderr, expectedEventName?)`** 解码退出状态与结构化 stdout。退出码为 2 时,会以 stderr 内容阻止执行;其他失败不阻塞。匹配的 hook 特定权限决策会覆盖遗留顶层决策;事件判别字段不匹配或缺失只会抑制事件特定字段。顶层字段仍与事件无关,成功但非 JSON 的输出会留给桥接处理。
|
||||
- **`mergeHookOutputs(outputs)`**:折叠在一个点上匹配的每个 hook 结果:权限优先级为 **deny > ask > allow**,从首个 `continue:false` 起,halt 状态保持不变,阻塞原因用 `\n\n` 连接,`additionalContext`/`systemMessages` 按顺序累积。
|
||||
- **`createDetachedRuns()`**:跟踪以 emit 形式脱离运行的点是否完全停稳(没有 seam 等待它们)。桥接会跟踪每条运行链,包括 hook 运行及其 continuation,并将 `drain()` 注册为 effect disposer。drain 会触发 tracker 的 abort `signal`(因此仍在运行的 hook 进程会通过 `runHook` 终止,而不是等待到超时),随后在所有已跟踪链结算后 resolve。因此 `fiber.dispose()` resolve 时,不会遗留任何可能作用于已 dispose(资源释放)的上下文的脱离 hook 工作(见 [防御模式](../../../docs/defensive-patterns.md):dispose 必须达到完全停稳)。
|
||||
|
||||
## `hook/*` 会话事件
|
||||
|
||||
通过 declaration merging 合并到 `SessionEventMap`(仅日志,与 `compact/*` 相同;不是 `SurfaceEventType`,没有 `surfaceOp`):`hook/invoked`(hook 命令已运行)与 `hook/result`(其结果,按 `handlerId` 配对,由 `appendHookResult` 拥有决策规则)。Payload 与每事件 JSDoc 位于生成的 [持久化日志事件目录](../../../docs/persistence-catalog.md);`stderrSummary` 会截断到记录的 `stderrSummaryMaxChars`(桥接配置,参考默认值 `DEFAULT_STDERR_SUMMARY_MAX_CHARS` = 500;为空时省略)。
|
||||
通过 declaration merging 合并到 `SessionEventMap`(仅日志,与 `compact/*` 相同;不是 `SurfaceEventType`,没有 `surfaceOp`):`hook/invoked`(hook 命令已运行)与 `hook/result`(其结果,按 `handlerId` 配对,决策规则由 `appendHookResult` 负责)。Payload 与每事件 JSDoc 位于生成的 [持久化日志事件目录](../../../docs/persistence-catalog.md);`stderrSummary` 会截断到记录的 `stderrSummaryMaxChars`(桥接配置,参考默认值 `DEFAULT_STDERR_SUMMARY_MAX_CHARS` = 500;为空时省略)。
|
||||
|
||||
Hook 溯源记录必须位于开启轮次内。轮次中点(`PreToolUse`/`PostToolUse`/`Stop`)按构造满足这条由所有方定义的关系。`SessionStart` 与轮次前的 `UserPromptSubmit` 准入 seam 没有 `hook/*` 记录;获准的上下文改由其带来源的 `user/message` 作为证据,详见 hooks Agent Note。
|
||||
Hook 溯源记录必须位于一个尚未结束的轮次内。轮次中的点(`PreToolUse`/`PostToolUse`/`Stop`)按构造满足这条由所有者定义的关系。`SessionStart` 与轮次前的 `UserPromptSubmit` 准入 seam 没有 `hook/*` 记录;获准的上下文改由其带来源的 `user/message` 作为证据,详见 hooks Agent Note(agent 决策记录)。
|
||||
|
||||
## 模型体验
|
||||
|
||||
@@ -37,7 +37,7 @@ Hook 溯源记录必须位于开启轮次内。轮次中点(`PreToolUse`/`Po
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
不会直接失效;请求前缀变更由具名消费方负责。
|
||||
不会直接失效;请求前缀变更由上述消费方负责。
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
|
||||
Reference in New Issue
Block a user