diff --git a/docs/LANDMINES.md b/docs/LANDMINES.md index c5e6a3d..75ff7a4 100644 --- a/docs/LANDMINES.md +++ b/docs/LANDMINES.md @@ -473,6 +473,14 @@ fix is ever reverted or bypassed. Referenced from CLAUDE.md. one real scenario (CLI drops `assistant`/`user` entirely) this tripwire exists to catch. Stayed a named allowlist. Run `npm run check` (now includes `schema-drift:check`) before trusting any future change here. + **Recurred 2026-11-06:** simulating the tripwire over 30 days of real + transcripts, five Claude sessions (CLI v2.1.299-310) false-tripped, worst + streak 43, on `api-request`, `api-request-blob`, + `artifact-autoreact-ledger`, `queue-operation` and `system` lines. Codex + peaked at 19 of 20 on `event_msg`. All six added to the recognized list; + worst streak is now 12 (Claude) and 4 (Codex). Before adding a type, + replay recent `~/.claude/projects` and `~/.codex/sessions` files through + the allowlist and print envelope types only, never message content. - **Copy-paste out of a Claude Code pane silently breaks long commands (found 2026-09-27).** Two distinct bugs were conflated live before the diff --git a/docs/TESTING.md b/docs/TESTING.md index 328d862..153636b 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -7054,3 +7054,15 @@ their screenshots), reported the mid-drag text selection fixed above, asked for the view pills, and accepted the phase. Not individually confirmed from the manual list: release outside the window (3), Finder file drop alongside board drags (5), external rename during a drag (6). + +### Schema-drift side-channel allowlist (2026-11-06) + +Automated: `npm run check` pins the six new types as recognized and the +2026-09-12 types as still flagged. Offline replay over 30 days of real +transcripts (161 Claude, 80 Codex files, envelope types only): 0 sessions +reach the threshold, down from 5. + +Manual, installed app after `npm run reinstall`: run a long Claude session +with subagents and confirm no "transcript format doesn't match" banner +appears. Not yet run; the banner isn't persisted, so the old false trip was +only reproduced by replay. diff --git a/scripts/decision-integrity-check.ts b/scripts/decision-integrity-check.ts index dfc8ba4..e5e2ca8 100644 --- a/scripts/decision-integrity-check.ts +++ b/scripts/decision-integrity-check.ts @@ -122,6 +122,18 @@ for (const line of [ ]) { assert.equal(transcriptEnvelopeType(line), "unrecognized", `${line} must be flagged, not silently ignored`); } +// Side-channel lines that false-tripped the tripwire on CLI v2.1.299-310 and +// nearly did on Codex (2026-11-06). +for (const type of [ + "api-request", + "api-request-blob", + "artifact-autoreact-ledger", + "queue-operation", + "system", + "event_msg", +]) { + assert.equal(transcriptEnvelopeType(JSON.stringify({ type })), "recognized", `${type} is a known side channel`); +} assert.equal(transcriptEnvelopeType("not json"), "unparseable"); assert.equal(transcriptEnvelopeType('{"type":"assistant"'), "unparseable", "a partial line mid-flush is not drift"); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8d32aec..52149f8 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3475,9 +3475,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.41" +version = "0.23.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ "log", "once_cell", @@ -3499,9 +3499,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "ring", "rustls-pki-types", @@ -5493,9 +5493,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.8" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171" dependencies = [ "rustls-pki-types", ] diff --git a/src/lib/decisions.ts b/src/lib/decisions.ts index 1b8169a..1fdded5 100644 --- a/src/lib/decisions.ts +++ b/src/lib/decisions.ts @@ -47,6 +47,12 @@ const assistantBuf = new Map(); // session_id -> pendi // allowlist, updated by hand each time a new benign side-channel type // ships (see decision-integrity-check.ts's regression test, which pins the // 2026-09-12 types as still-must-flag — never add those here). +// +// Recurrence 2026-11-06: simulated over 30 days of real transcripts, five +// Claude sessions (CLI v2.1.299-310) false-tripped on api-request, +// api-request-blob, artifact-autoreact-ledger, queue-operation and system +// lines; Codex peaked at 19 of 20 on event_msg. With these added the worst +// streak is 12 (Claude) and 4 (Codex). const SCHEMA_DRIFT_THRESHOLD = 20; const unrecognizedStreak = new Map(); // session_id -> consecutive unrecognized-envelope lines const driftWarned = new Set(); // session_id already warned — fire once, not per line @@ -79,6 +85,12 @@ export function transcriptEnvelopeType(line: string): "recognized" | "unrecogniz obj.type === "pi_message" || obj.type === "deepseek_message" || obj.type === "attachment" || + obj.type === "api-request" || + obj.type === "api-request-blob" || + obj.type === "artifact-autoreact-ledger" || + obj.type === "queue-operation" || + obj.type === "system" || + obj.type === "event_msg" || isAntigravityEnvelope(obj) ? "recognized" : "unrecognized";