fix(hooks): remove speculative regex runtime
This commit is contained in:
@@ -10,7 +10,7 @@ Why a shared lib at all: Codex deliberately reimplements a *subset* of the Claud
|
||||
|
||||
| Concern | Here (`dsh-hook-protocol`) | The bridge (`dsh-hooks-claude` / `-codex`) |
|
||||
|---|---|---|
|
||||
| Matcher validation + test | `compileMatchers(patterns, mode)` exposes diagnostics and repeated config-lifetime matching from one registry; Codex uses a bounded reload-stable Rust-regex interner; `matcherDiagnostic` / `matchesMatcher` are contained one-shot helpers | picks its native regex `mode` (`claude` = JavaScript, `codex` = Rust `regex`), compiles the unique runnable patterns once, rejects a group carrying a registry diagnostic, and disposes its config registry on failure or teardown |
|
||||
| Matcher validation + test | `matcherDiagnostic(pattern, mode)` for parse-time diagnostics; `matchesMatcher(pattern, query, mode)` for contained runtime matching | picks its `mode` (`claude` = literal-or-regex, `codex` = always regex) and rejects a config group carrying a diagnostic |
|
||||
| Run a hook | `runHook(bash, hook, opts, now)` — stdin payload + env via `ctx.bash`, decode | builds the per-event stdin **payload** + the dialect's **env** |
|
||||
| Decode output | `parseHookOutput(exit, stdout, stderr)` → neutral `HookOutput` | maps the neutral `HookOutput` onto a seam-specific typed Decision |
|
||||
| Merge N hooks | `mergeHookOutputs(outputs)` → most-restrictive `MergedHookOutcome` | — |
|
||||
@@ -19,7 +19,7 @@ Why a shared lib at all: Codex deliberately reimplements a *subset* of the Claud
|
||||
|
||||
## Primitives
|
||||
|
||||
- **`compileMatchers(matchers, mode)` / `matcherDiagnostic(matcher, mode)` / `matchesMatcher(matcher, query, mode)`** — match-all on absent/`''`/`'*'`; both dialects treat a pure `[A-Za-z0-9_|]+` pattern as exact pipe-separated alternatives. Other patterns are unanchored regexes compiled in the native dialect: JavaScript for Claude Code, Rust `regex` for Codex (including inline flags such as `(?i)`). A bridge parser first discards matcher fields for events without matcher subjects and collects the remaining runnable groups, then compiles their unique patterns once. It reads `registry.diagnostic(pattern)` from those exact instances, disposes the config registry before throwing on a rejected pattern, or returns that registry for runtime matching and teardown after detached runs drain. Codex's valid instances and invalid diagnostics are interned on the synchronous `rregex` dependency module, so they survive hook-protocol/Cordis reloads without using `globalThis`; one-shot helpers share the same interner. Because `rregex` cannot shrink its WASM allocation after `free()`, the process deliberately retains at most `MAX_INTERNED_CODEX_REGEX_PATTERNS` (128) distinct non-literal patterns. Once full, a new distinct pattern is rejected with a capacity diagnostic before calling WASM; previously interned patterns continue to work, and a process restart resets the budget. This is bounded for both same-pattern and adversarial unique reloads without an unbounded cache.
|
||||
- **`matcherDiagnostic(matcher, mode)` / `matchesMatcher(matcher, query, mode)`** — match-all on absent/`''`/`'*'`; `claude` mode treats a pure `[A-Za-z0-9_|]+` pattern as a literal (pipe = exact-match alternation) and anything else as a regex; `codex` mode is always an unanchored regex. Bridge parsers discard matcher fields for events without matcher subjects, then use `matcherDiagnostic` to reject an invalid consumed regex with a stable diagnostic before registering any hooks. The runtime predicate still contains an invalid pattern as a non-match, so a direct library caller cannot throw into the agent loop.
|
||||
- **`runHook(bash, hook, options, now)`** — require and forward the caller-owned `options.signal`, serialize `options.payload` to the hook's stdin (with a trailing newline iff `options.trailingNewline`), merge `options.env` after the executor's credential scrub (the `dsh-bash` trusted-plugin surface), honor the hook's `timeoutSec` (else `options.defaultTimeoutMs` — the bridge owns the default, its config defaulting to the lib's `DEFAULT_HOOK_TIMEOUT_MS` 10-minute reference), and decode the result (threading `options.expectedEventName` to the codec). Cancellation therefore reaches the executor's process-group kill and join boundary. Never throws: an executor rejection (infra fault) becomes a `HookOutput` with `exitCode: undefined` (a non-blocking error). `now` is injected for testable durations.
|
||||
- **`parseHookOutput(exitCode, stdout, stderr, expectedEventName?)`** decodes exit status and structured stdout. Exit 2 blocks with stderr; other failures are non-blocking. A matching hook-specific permission decision overrides the legacy top-level decision; mismatched or missing event discriminators suppress only event-specific fields. Top-level fields remain event-agnostic, and successful non-JSON output is left to the bridge.
|
||||
- **`mergeHookOutputs(outputs)`** — fold the results of every hook that matched one point: permission precedence **deny > ask > allow**, halt sticky on the first `continue:false`, block reasons joined with `\n\n`, `additionalContext`/`systemMessages` accumulated in order.
|
||||
|
||||
Reference in New Issue
Block a user