fix(providers): guard stdin EPIPE on fast-failing CLI spawn (P1 crash-hardening)

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 <claude-sonnet-4-6> <noreply@anthropic.com>
This commit is contained in:
2026-05-31 17:17:54 +10:00
co-authored by Claude <claude-sonnet-4-6> <noreply@anthropic.com>
parent 74e67fdca3
commit a503441ea3
3 changed files with 40 additions and 0 deletions
+13
View File
@@ -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();