From 7f0e1b03758c369a2c711989d83e3b5ab105ca64 Mon Sep 17 00:00:00 2001 From: Jake Date: Thu, 14 May 2026 19:37:39 +0100 Subject: [PATCH] =?UTF-8?q?agent:=20lumotia=20=E2=80=94=20Phase=20B.6=20pi?= =?UTF-8?q?n=20IPC-allowlist=20vs=20capability-JSON=20mirror=20invariant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase B.6 audit of commits 7aee534 (Trust-3/6 main-window guard + size cap on clipboard + paste surface), 12b413d (broaden clipboard/paste allowlist to documented secondary windows), and f7af7b0 (Trust-4 main- window guard on extract_content_tags_cmd). Existing coverage is strong: * commands/security.rs: 4 tests for ensure_main_window_label + ensure_window_in_set_label accept/reject paths. * commands/clipboard.rs: 3 size-cap tests via a shadow size_check helper. * commands/paste.rs: comprehensive — 4 backend-order, 4 clipboard-restore, 7 terminal-classification, 3 paste-size-cap, plus paste_cap_matches_clipboard_cap that pins the MAX_CLIPBOARD_BYTES == MAX_PASTE_BYTES invariant the commit explicitly cared about. * commands/llm.rs: extract_content_tags_cmd Trust-4 — unconditional ensure_main_window guard with no surface to test beyond what's there. One real residual. The 12b413d commit message states: "mirror the secondary-windows capability grant in src-tauri/capabilities/secondary-windows.json so the IPC trust boundary and the permission grant stay in lock-step." But no test pins the mirror invariant. A future change could: * add a new window to secondary-windows.json and forget to update CLIPBOARD_ALLOWED_WINDOWS or PASTE_REPLACING_ALLOWED_WINDOWS; * typo a label in one of the Rust consts; * remove a window from the JSON while leaving the const intact; * remove a window from the const while leaving the JSON intact. Each of those silently drifts the IPC trust boundary against the capability grant. The two halves stay in lock-step on intent — but the intent lives only in the commit message and a docstring, not in a runtime check. Fix (test-only, no production behaviour change): * Promote CLIPBOARD_ALLOWED_WINDOWS and PASTE_REPLACING_ALLOWED_WINDOWS from private to pub(crate) so a single shared test can reference them. * Cross-reference both consts in a new docstring back to the pinning test. * Add commands::security::tests_capability_mirror::allowlists_match_capability_jsons. The test reads capabilities/main.json + capabilities/secondary-windows.json at CARGO_MANIFEST_DIR, parses with serde_json, collects every label declared in the "windows" arrays, and asserts every label in both Rust allowlists is in that declared set. Asymmetric on purpose: the JSON may legitimately declare windows that don't need clipboard/paste (e.g. tasks-float doesn't), so the test does NOT assert const ⊇ JSON, only const ⊆ JSON. The over-restrict direction is safe; the under-restrict direction is the IPC bypass we care about. Verification: * cargo test -p lumotia --lib commands::security → 5/5 pass including the new allowlists_match_capability_jsons. * cargo fmt --check → clean (applied fmt after the test edit). * cargo clippy -p lumotia --all-targets -- -D warnings → clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- src-tauri/src/commands/clipboard.rs | 7 ++- src-tauri/src/commands/paste.rs | 6 ++- src-tauri/src/commands/security.rs | 68 +++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/commands/clipboard.rs b/src-tauri/src/commands/clipboard.rs index 94fe172..5095a92 100644 --- a/src-tauri/src/commands/clipboard.rs +++ b/src-tauri/src/commands/clipboard.rs @@ -15,8 +15,11 @@ pub(crate) const MAX_CLIPBOARD_BYTES: usize = 1024 * 1024; /// viewer and transcription preview both have legitimate "copy raw text" /// buttons; mirror the secondary-windows capability grant in /// `src-tauri/capabilities/secondary-windows.json` so the IPC trust -/// boundary and the permission set stay in lock-step. -const CLIPBOARD_ALLOWED_WINDOWS: &[&str] = &["main", "transcript-viewer", "transcription-preview"]; +/// boundary and the permission set stay in lock-step. The mirror +/// invariant is pinned by +/// `commands::security::tests_capability_mirror::allowlists_match_capability_jsons`. +pub(crate) const CLIPBOARD_ALLOWED_WINDOWS: &[&str] = + &["main", "transcript-viewer", "transcription-preview"]; /// Copy text to the system clipboard via arboard. Restricted to the /// documented "clipboard-capable" windows (main + transcript-viewer + diff --git a/src-tauri/src/commands/paste.rs b/src-tauri/src/commands/paste.rs index 289ab35..9feac77 100644 --- a/src-tauri/src/commands/paste.rs +++ b/src-tauri/src/commands/paste.rs @@ -26,8 +26,10 @@ use crate::commands::security::{ensure_main_window, ensure_window_in_set}; /// Windows allowed to invoke `paste_text_replacing`. The /// `transcription-preview` window's paste-to-foreground flow legitimately /// uses this command; mirror the secondary-windows capability grant in -/// `src-tauri/capabilities/secondary-windows.json`. -const PASTE_REPLACING_ALLOWED_WINDOWS: &[&str] = &["main", "transcription-preview"]; +/// `src-tauri/capabilities/secondary-windows.json`. The mirror invariant +/// is pinned by +/// `commands::security::tests_capability_mirror::allowlists_match_capability_jsons`. +pub(crate) const PASTE_REPLACING_ALLOWED_WINDOWS: &[&str] = &["main", "transcription-preview"]; /// Refuse-to-paste limit. Matches `commands::clipboard::MAX_CLIPBOARD_BYTES` /// (1 MiB) so paste and copy surfaces share a single rejection rule. diff --git a/src-tauri/src/commands/security.rs b/src-tauri/src/commands/security.rs index 8cbccb3..835ac82 100644 --- a/src-tauri/src/commands/security.rs +++ b/src-tauri/src/commands/security.rs @@ -66,3 +66,71 @@ mod tests { assert!(ensure_window_in_set_label("attacker-popup", allowed).is_err()); } } + +/// Pins the invariant flagged in the 12b413d commit message and the +/// Phase B.6 audit (2026-05-14): every label that the Rust IPC layer +/// allow-lists for clipboard / paste-replacing must also appear in one +/// of the Tauri capability JSONs' "windows" arrays. If the two halves +/// drift, the IPC trust boundary silently disagrees with the permission +/// grant — either the IPC layer permits a window the capability system +/// rejects (call dies with a Tauri permission error), or the capability +/// system permits a window the IPC layer rejects (call dies with the +/// Lumotia rejection message). Either is a maintenance footgun the +/// commit message claimed to address but did not actually pin. +#[cfg(test)] +mod tests_capability_mirror { + use std::collections::HashSet; + use std::path::Path; + + /// Read all `"windows"` labels declared across the capability JSONs + /// at `src-tauri/capabilities/`. The Tauri permission system uses + /// these arrays to scope capability grants per window; the Rust IPC + /// `ensure_window_in_set` callers must reference the same labels. + fn declared_window_labels() -> HashSet { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let capabilities_dir = Path::new(manifest_dir).join("capabilities"); + + let mut labels: HashSet = HashSet::new(); + for filename in ["main.json", "secondary-windows.json"] { + let body = std::fs::read_to_string(capabilities_dir.join(filename)) + .unwrap_or_else(|e| panic!("read {filename}: {e}")); + let json: serde_json::Value = + serde_json::from_str(&body).unwrap_or_else(|e| panic!("parse {filename}: {e}")); + let windows = json["windows"] + .as_array() + .unwrap_or_else(|| panic!("{filename} missing 'windows' array")); + for w in windows { + if let Some(label) = w.as_str() { + labels.insert(label.to_string()); + } + } + } + labels + } + + #[test] + fn allowlists_match_capability_jsons() { + let declared = declared_window_labels(); + + for label in crate::commands::clipboard::CLIPBOARD_ALLOWED_WINDOWS { + assert!( + declared.contains(*label), + "CLIPBOARD_ALLOWED_WINDOWS label {label:?} is not declared in any \ + capabilities JSON ({}). Either remove the label from the Rust const \ + or add the window to capabilities/secondary-windows.json so the IPC \ + trust boundary and the capability grant stay in lock-step.", + declared.iter().cloned().collect::>().join(", ") + ); + } + + for label in crate::commands::paste::PASTE_REPLACING_ALLOWED_WINDOWS { + assert!( + declared.contains(*label), + "PASTE_REPLACING_ALLOWED_WINDOWS label {label:?} is not declared in any \ + capabilities JSON ({}). Either remove the label from the Rust const \ + or add the window to capabilities/secondary-windows.json.", + declared.iter().cloned().collect::>().join(", ") + ); + } + } +}