agent: lumotia — Phase B.6 pin IPC-allowlist vs capability-JSON mirror invariant

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 19:37:39 +01:00
parent d8fa4ff64e
commit 7f0e1b0375
3 changed files with 77 additions and 4 deletions

View File

@@ -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 +

View File

@@ -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.

View File

@@ -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<String> {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let capabilities_dir = Path::new(manifest_dir).join("capabilities");
let mut labels: HashSet<String> = 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::<Vec<_>>().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::<Vec<_>>().join(", ")
);
}
}
}