From a503441ea30cc06c55d6bb27003f8edf951cbe00 Mon Sep 17 00:00:00 2001 From: dtzp555 Date: Sun, 31 May 2026 17:17:54 +1000 Subject: [PATCH] fix(providers): guard stdin EPIPE on fast-failing CLI spawn (P1 crash-hardening) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `proc.stdin.on('error', ...)` guard in anthropic.mjs, codex.mjs, and mistral.mjs before any stdin.write() / stdin.end() call. Prevents a fast-failing CLI (auth error, bad model, oversized prompt) from crashing the single-process daemon via an unhandled EPIPE on the Node stdin Writable. The ChildProcess 'error' event fires on the spawned process itself; it does NOT catch errors on the stdin Writable. The stdin Writable emits its own 'error' event which, without a listener, propagates as an unhandled exception and crashes the daemon. Authority (ALIGNMENT.md Rule 1 + Rule 5): - anthropic: claude CLI v2.1.104 observed behaviour — fast-failing spawn (e.g. auth error, bad model) closes stdin before OLP finishes writing; Node stdin Writable surfaces EPIPE. ALIGNMENT.md Rule 2: no CLI operation changed; guard is a Node stream safety net only. Port source: OCP server.mjs:777-784. - codex: Codex CLI reference https://developers.openai.com/codex/cli/reference — same mechanism; stdin path via `-` positional documented in reference § "Use '-' to pipe the prompt from stdin." ALIGNMENT.md Rule 2 applies. - mistral: Mistral Vibe CLI https://docs.mistral.ai/mistral-vibe/terminal/quickstart — vibe --prompt reads from args not stdin, but stdin.end() can still surface EPIPE if the process exits before the fd is flushed. ALIGNMENT.md Rule 2: no CLI operation changed. A `typeof proc.stdin.on === 'function'` guard is included to protect test-injected mock stdin objects (plain objects without .on()) from breaking the EventEmitter call. Co-Authored-By: Claude --- lib/providers/anthropic.mjs | 13 +++++++++++++ lib/providers/codex.mjs | 13 +++++++++++++ lib/providers/mistral.mjs | 14 ++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/lib/providers/anthropic.mjs b/lib/providers/anthropic.mjs index 45a6d8d..a249990 100644 --- a/lib/providers/anthropic.mjs +++ b/lib/providers/anthropic.mjs @@ -967,6 +967,19 @@ async function* _spawnAndStream(irRequest, authContext, spawnImpl, isolationCtx) stdio: ['pipe', 'pipe', 'pipe'], }); + // Guard stdin writes against EPIPE (child may close stdin before we finish + // writing — e.g. on auth error, bad model, or oversized prompt). + // Port of OCP server.mjs:780. + // Authority: claude CLI v2.1.104 observed behaviour — fast-failing spawn closes + // stdin before OLP finishes writing; Node stdin Writable surfaces EPIPE. + // ALIGNMENT.md Rule 2: no CLI operation changed; guard is a Node stream safety net. + // The typeof guard protects test-injected mock stdin objects that are plain + // objects (not EventEmitters) and do not implement .on(). + if (typeof proc.stdin.on === 'function') { + proc.stdin.on('error', (e) => { + console.error(JSON.stringify({ ts: new Date().toISOString(), level: 'warn', event: 'stdin_write_error', error: e.message })); + }); + } // OCP server.mjs:586-587: proc.stdin.write(prompt); proc.stdin.end(); proc.stdin.write(prompt); proc.stdin.end(); diff --git a/lib/providers/codex.mjs b/lib/providers/codex.mjs index d005387..c3aad10 100644 --- a/lib/providers/codex.mjs +++ b/lib/providers/codex.mjs @@ -505,6 +505,19 @@ async function* _spawnAndStream(irRequest, authContext, spawnImpl, isolationCtx) const proc = spawnImpl(finalBin, finalArgs, { env: finalEnv, stdio: ['pipe', 'pipe', 'pipe'] }); + // Guard stdin writes against EPIPE (child may close stdin before we finish + // writing — e.g. on auth error, bad model, or oversized prompt). + // Port of OCP server.mjs:780. + // Authority: codex CLI Codex CLI reference (https://developers.openai.com/codex/cli/reference) + // observed behaviour — fast-failing spawn closes stdin before OLP finishes writing. + // ALIGNMENT.md Rule 2: no CLI operation changed; guard is a Node stream safety net. + // The typeof guard protects test-injected mock stdin objects that are plain + // objects (not EventEmitters) and do not implement .on(). + if (typeof proc.stdin.on === 'function') { + proc.stdin.on('error', (e) => { + console.error(JSON.stringify({ ts: new Date().toISOString(), level: 'warn', event: 'stdin_write_error', error: e.message })); + }); + } // Write prompt via stdin for multi-line prompts (D6 assumption A1) if (useStdin) { proc.stdin.write(prompt); diff --git a/lib/providers/mistral.mjs b/lib/providers/mistral.mjs index 8a4c7b1..7c29844 100644 --- a/lib/providers/mistral.mjs +++ b/lib/providers/mistral.mjs @@ -500,6 +500,20 @@ async function* _spawnAndStream(irRequest, authContext, spawnImpl) { const proc = spawnImpl(bin, args, { env, stdio: ['pipe', 'pipe', 'pipe'] }); + // Guard stdin writes against EPIPE (child may close stdin before we finish + // writing — vibe --prompt reads from CLI args, but stdin.end() can still + // surface EPIPE if the process exits before the fd is flushed). + // Port of OCP server.mjs:780. + // Authority: Mistral Vibe CLI https://docs.mistral.ai/mistral-vibe/terminal/quickstart + // observed behaviour — fast-failing spawn closes stdin; Node Writable surfaces EPIPE. + // ALIGNMENT.md Rule 2: no CLI operation changed; guard is a Node stream safety net. + // The typeof guard protects test-injected mock stdin objects that are plain + // objects (not EventEmitters) and do not implement .on(). + if (typeof proc.stdin.on === 'function') { + proc.stdin.on('error', (e) => { + console.error(JSON.stringify({ ts: new Date().toISOString(), level: 'warn', event: 'stdin_write_error', error: e.message })); + }); + } // Close stdin immediately — vibe --prompt reads from args, not stdin. proc.stdin.end();