fix: OCP code-audit P1+P2 hardening (crash bugs + multi-tenant gates) (#106)

* fix(daemon): P1-1 guard proc.stdin against EPIPE crash

In spawnClaudeProcess, attach an error listener on proc.stdin BEFORE
the write/end calls so an EPIPE (child closed stdin mid-write) is
swallowed and logged rather than thrown as an unhandled exception.

The existing proc.on("error") listener is on the ChildProcess object,
NOT on the stdin Writable — it does not catch stdin write errors.

Hardening per OCP code audit; entry-surface contract unchanged for
single-user default path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(tui): P1-3 remove tool_use from isTerminalLine; only turn_duration is terminal

In interactive TUI mode, stop_reason=tool_use does NOT mean the turn is
complete. Claude handles the tool call internally and continues generating —
the transcript advances to another assistant entry. Treating tool_use as
terminal truncated tool-using turns mid-flight.

Only {type:"system", subtype:"turn_duration"} is the authoritative
completion marker (claude CLI v2.1.157+ interactive session transcript).

Updated two unit tests that previously asserted tool_use → true; they now
assert false (the correct behaviour). The real-fixture terminal detection
test is unaffected because the fixture uses turn_duration.

Hardening per OCP code audit; TUI path behaviour fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(tui): P2-5 add -l (literal) flag to send-keys prompt paste

A prompt that equals a tmux key token (e.g. "C-c", "Escape") would be
interpreted as that key binding rather than typed as literal text.

The -l flag forces literal character-by-character input. The separate
Enter key event afterward deliberately omits -l so tmux sends a real
carriage-return keypress to submit the prompt line.

Authority: tmux send-keys(1) § -l flag. Hardening per OCP code audit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(audit): String-coerce parsed.error + stale tool_use comment (review fast-follow)

Folds the 2 minor findings from the independent review of the audit fixes:
- String(parsed.error) before .slice/message in callClaude + callClaudeStreaming
  (defensive: claude could emit a non-string result/error_message).
- correct the readTuiTranscript comment that still listed tool_use as terminal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
dtzp555-max
2026-05-31 13:17:36 +10:00
committed by GitHub
co-authored by taodeng Claude Opus 4.8
parent a30b20978c
commit 05a984df89
4 changed files with 166 additions and 41 deletions
+7 -1
View File
@@ -223,9 +223,15 @@ export async function runTuiTurn({
// 2. Submit prompt body via `"$(cat file)"` — byte-safe for any content —
// then settle, then send a SEPARATE Enter key event to submit the line.
//
// The `-l` (literal) flag is required on the paste send-keys call so that
// a prompt that happens to equal a tmux key token (e.g. "C-c", "Escape")
// is typed literally as text rather than being interpreted as a key binding.
// The SEPARATE Enter event below deliberately omits -l so that tmux sends a
// real keypress (carriage return) to submit the prompt line.
spawnSync(
"sh",
["-c", `${shq(TMUX)} send-keys -t ${shq(tmuxName)} -- "$(cat ${shq(promptFile)})"`],
["-c", `${shq(TMUX)} send-keys -t ${shq(tmuxName)} -l -- "$(cat ${shq(promptFile)})"`],
{ env, encoding: "utf8" },
);
await sleep(PASTE_SETTLE_MS);
+11 -5
View File
@@ -52,12 +52,18 @@ export function parseTranscriptLines(text) {
}
// A line marks the assistant turn complete when it is the turn_duration system
// event, or an assistant message that stopped to hand off to a tool.
// event. That is the ONLY reliable terminal marker in interactive TUI mode.
//
// Why tool_use is NOT a terminal marker:
// In interactive claude, when the model decides to call a tool (stop_reason=
// "tool_use"), claude handles the tool call internally and then continues
// generating — the turn is NOT complete. The transcript advances to another
// assistant entry after the tool result. Only {type:"system",
// subtype:"turn_duration"} signals that claude has fully finished the turn.
// Treating tool_use as terminal would truncate tool-using turns mid-flight.
export function isTerminalLine(obj) {
if (!obj || typeof obj !== "object") return false;
if (obj.type === "system" && obj.subtype === "turn_duration") return true;
const sr = (obj.message && obj.message.stop_reason) || obj.stop_reason;
return sr === "tool_use";
return obj.type === "system" && obj.subtype === "turn_duration";
}
// Text of the LAST assistant turn: concatenate its text content blocks
@@ -96,7 +102,7 @@ export function verifyEntrypoint(events) {
return null;
}
// Block until the session transcript is terminal (turn_duration / tool_use) or
// Block until the session transcript is terminal (turn_duration) or
// the wall-clock cap elapses, polling the file (no fs.watch — robust over NFS /
// editors). Returns the latest assistant text. On cap with text, returns the
// partial text; on cap with no text at all, throws.