mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(hooks): match Codex Rust regex syntax
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# 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 packages/hooks/hooks-codex/README.md
|
||||
README.md: e906810ed58c3d0204c618c32787af06c91cfb78
|
||||
README.zh.md: 8992cc63edf74d057114d888c396881dc8ee43d6
|
||||
README.md: 0c9a6b22d0990d87ad081db4f2690c5d97357062
|
||||
README.zh.md: d3c88a75208257585255fc36ad6cc0a7a3b5c0f0
|
||||
|
||||
@@ -7,7 +7,7 @@ A cordis plugin that runs the supported subset of a user's existing **Codex** ho
|
||||
This bridge implements a deliberate subset of Codex's current hook protocol:
|
||||
|
||||
- **Five of ten hook points:** `PreToolUse`, `PostToolUse`, `SessionStart`, `UserPromptSubmit`, and `Stop`.
|
||||
- **Regex-only matchers** (no literal fast path; the matcher is always an unanchored regex).
|
||||
- **Native Codex matcher semantics:** pure word/pipe patterns are exact alternatives; other patterns are unanchored Rust `regex` expressions (including inline flags such as `(?i)`).
|
||||
- **snake_case stdin payloads** with `turn_id`/`model` extras, written **without** a trailing newline.
|
||||
- **No Codex plugin env injection and no config-time placeholder substitution** (the command still receives the executor's environment and runs through its shell).
|
||||
- **No pre-tool approval or rewrite path** — a hook can block, but the bridge does not pre-approve or replace tool input.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
该桥接实现 Codex 当前 hook 协议的一个明确子集:
|
||||
|
||||
- **10 个 hook 点中的 5 个:** `PreToolUse`、`PostToolUse`、`SessionStart`、`UserPromptSubmit` 和 `Stop`。
|
||||
- **只使用正则 matcher**(没有字面快速路径;matcher 始终是未锚定正则)。
|
||||
- **原生 Codex matcher 语义:**纯 word/pipe pattern 是精确匹配的多选;其他 pattern 是未锚定的 Rust `regex` 表达式(包括 `(?i)` 等内联 flag)。
|
||||
- **snake_case stdin payload**,携带 `turn_id`/`model` 额外字段,写入时**不带** 尾随换行符。
|
||||
- **没有 Codex 插件 env 注入,也没有配置时 placeholder 替换**(命令仍会接收执行器环境,并通过其 shell 运行)。
|
||||
- **没有工具前批准或改写路径**:hook 可以阻塞,但桥接不会预批准或替换工具输入。
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* Bridge for unmodified Codex command hooks on harness interception seams. It
|
||||
* supports five points (SessionStart, prompt/tool pre/post, Stop), regex-only
|
||||
* matchers, snake_case payloads without a trailing newline, no hook environment
|
||||
* or command substitution, and no pre-tool approval or rewrite path; only
|
||||
* blocking decisions are honored. Shared execution and parsing live in
|
||||
* supports five points (SessionStart, prompt/tool pre/post, Stop), native
|
||||
* literal-or-Rust-regex matchers, snake_case payloads without a trailing
|
||||
* newline, no hook environment or command substitution, and no pre-tool
|
||||
* approval or rewrite path; only blocking decisions are honored. Shared
|
||||
* execution and parsing live in
|
||||
* `dsh-hook-protocol`; see the
|
||||
* [hook-bridges Agent Note](../../../../.agents/notes/implemented/feature/2026-06-30-hook-bridges.md).
|
||||
* @module @deepseek-ai/dsh-hooks-codex
|
||||
@@ -127,7 +128,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
// user's project rather than the server launch directory.
|
||||
const workdir = opts.agent?.session.header.cwd
|
||||
for (const group of groups) {
|
||||
// Codex always interprets matchers as regexes; it has no literal fast path.
|
||||
// The protocol library owns Codex's exact-literal/Rust-regex split.
|
||||
if (!matchesMatcher(group.matcher, matchQuery, 'codex')) continue
|
||||
for (const hook of group.hooks) {
|
||||
const handlerId = nextHandlerId(point)
|
||||
|
||||
@@ -66,11 +66,11 @@ async function waitFor(predicate: () => boolean, timeout = 5000, interval = 10):
|
||||
}
|
||||
|
||||
describe('hooks-codex bridge', () => {
|
||||
it('a PreToolUse hook (exit 2) denies a tool the regex matcher matches as a substring', async () => {
|
||||
it('a PreToolUse hook (exit 2) honors a Rust-regex inline flag matcher', async () => {
|
||||
const dir = configDir()
|
||||
const deny = script(dir, 'deny.sh', '#!/usr/bin/env bash\necho "codex blocked it" >&2\nexit 2\n')
|
||||
// Codex regex matcher: "Bash" is /Bash/ — matches the tool name "Bash".
|
||||
writeHooks(dir, { PreToolUse: [{ matcher: 'Bash', hooks: [{ type: 'command', command: deny }] }] })
|
||||
// `(?i)` is accepted by Rust regex but rejected by JavaScript RegExp.
|
||||
writeHooks(dir, { PreToolUse: [{ matcher: '(?i)^bash$', hooks: [{ type: 'command', command: deny }] }] })
|
||||
|
||||
const adapter = new MockAdapter([toolCallResponse('c1', 'Bash', { command: 'ls' }), textResponse('done')])
|
||||
const ctx = await harness(dir, adapter)
|
||||
|
||||
@@ -61,9 +61,9 @@ describe('parseCodexConfig', () => {
|
||||
expect('matcher' in config.Stop![0]!).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps a matcher when present', () => {
|
||||
const { config } = parseCodexConfig({ PreToolUse: [{ matcher: '^Bash$', hooks: [{ type: 'command', command: 'b.sh' }] }] })
|
||||
expect(config.PreToolUse![0]!.matcher).toBe('^Bash$')
|
||||
it('keeps a valid Rust-regex matcher when present', () => {
|
||||
const { config } = parseCodexConfig({ PreToolUse: [{ matcher: '(?i)^bash$', hooks: [{ type: 'command', command: 'b.sh' }] }] })
|
||||
expect(config.PreToolUse![0]!.matcher).toBe('(?i)^bash$')
|
||||
})
|
||||
|
||||
it('rejects an invalid regex matcher with its event name', () => {
|
||||
@@ -72,6 +72,12 @@ describe('parseCodexConfig', () => {
|
||||
})).toThrow('invalid codex regex matcher "[" on event "PreToolUse"')
|
||||
})
|
||||
|
||||
it('rejects JavaScript-only regex syntax that Codex cannot execute', () => {
|
||||
expect(() => parseCodexConfig({
|
||||
PreToolUse: [{ matcher: '(?=Bash)', hooks: [{ type: 'command', command: 's.sh' }] }],
|
||||
})).toThrow('invalid codex regex matcher "(?=Bash)" on event "PreToolUse"')
|
||||
})
|
||||
|
||||
it('discards matcher fields on events without matcher subjects before validation', () => {
|
||||
const { config } = parseCodexConfig({
|
||||
UserPromptSubmit: [{ matcher: '[', hooks: [{ type: 'command', command: 'prompt.sh' }] }],
|
||||
|
||||
Reference in New Issue
Block a user