Skip to main content

victauri_plugin/
privacy.rs

1use std::collections::HashSet;
2
3use crate::redaction::Redactor;
4
5/// Privacy profile controlling which MCP tools and actions are permitted.
6///
7/// The three tiers form a strict hierarchy: `Observe ⊂ Test ⊂ FullControl`.
8/// Each higher tier inherits all permissions from the tier below and adds more.
9///
10/// | Profile | Can read | Can interact | Can mutate | Can eval/screenshot |
11/// |---|---|---|---|---|
12/// | `Observe` | Yes | No | No | No |
13/// | `Test` | Yes | Yes | Storage writes | No |
14/// | `FullControl` | Yes | Yes | Yes | Yes |
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum PrivacyProfile {
17    /// Read-only observation. Snapshots, logs, registry, accessibility, performance,
18    /// window state — but no clicks, no input, no eval, no screenshots, no mutations.
19    Observe,
20    /// Observation + UI interactions + storage writes + recording. Suitable for
21    /// automated testing. Eval, screenshot, CSS injection, navigation, and
22    /// `invoke_command` (unless allowlisted) remain blocked.
23    Test,
24    /// Everything permitted. No restrictions. This is the default.
25    #[default]
26    FullControl,
27}
28
29impl std::fmt::Display for PrivacyProfile {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        match self {
32            Self::Observe => write!(f, "observe"),
33            Self::Test => write!(f, "test"),
34            Self::FullControl => write!(f, "full_control"),
35        }
36    }
37}
38
39/// Privacy controls for the MCP server.
40///
41/// Combines a [`PrivacyProfile`] (tiered permission matrix) with fine-grained
42/// overrides: command allowlists/blocklists, per-tool disabling, and output redaction.
43///
44/// **Precedence:** explicit `disabled_tools` overrides → profile matrix → allowlist/blocklist.
45#[derive(Default)]
46pub struct PrivacyConfig {
47    /// The active privacy profile tier.
48    pub profile: PrivacyProfile,
49    /// If set, only these Tauri commands can be invoked (positive allowlist).
50    pub command_allowlist: Option<HashSet<String>>,
51    /// Tauri commands that are always blocked, even if on the allowlist.
52    pub command_blocklist: HashSet<String>,
53    /// MCP tool/action names explicitly disabled (override layer on top of profile).
54    pub disabled_tools: HashSet<String>,
55    /// localStorage/sessionStorage keys that `storage.set` must never write
56    /// (operator-configured, since which keys carry app trust is app-specific).
57    pub storage_key_blocklist: HashSet<String>,
58    /// Output redactor with regex and JSON-key matching.
59    pub redactor: Redactor,
60    /// Whether output redaction is active.
61    pub redaction_enabled: bool,
62}
63
64impl PrivacyConfig {
65    /// Returns `true` if the Tauri command passes both the allowlist and blocklist.
66    #[must_use]
67    pub fn is_command_allowed(&self, command: &str) -> bool {
68        if self.command_blocklist.contains(command) {
69            return false;
70        }
71        match &self.command_allowlist {
72            Some(allow) => allow.contains(command),
73            None => true,
74        }
75    }
76
77    /// Returns `true` if `storage.set` may write the given storage key (i.e. the
78    /// key is not in the operator's `storage_key_blocklist`). Use this to protect
79    /// keys an app trusts for auth/role/tier/feature-flag decisions (audit #33).
80    #[must_use]
81    pub fn is_storage_key_allowed(&self, key: &str) -> bool {
82        !self.storage_key_blocklist.contains(key)
83    }
84
85    /// Returns `true` if the given tool or qualified action (e.g. `"window.manage"`)
86    /// is permitted by the current profile AND not in the explicit disabled set.
87    #[must_use]
88    pub fn is_tool_enabled(&self, tool_or_action: &str) -> bool {
89        if self.disabled_tools.contains(tool_or_action) {
90            return false;
91        }
92        is_allowed_by_profile(self.profile, tool_or_action)
93    }
94
95    /// Check whether `invoke_command` is allowed for a specific command name.
96    ///
97    /// In `Test` profile, `invoke_command` is only allowed if the command is on the
98    /// allowlist. In `FullControl`, it's always allowed. In `Observe`, always blocked.
99    #[must_use]
100    pub fn is_invoke_allowed(&self, command: &str) -> bool {
101        if self.disabled_tools.contains("invoke_command") {
102            return false;
103        }
104        match self.profile {
105            PrivacyProfile::FullControl => true,
106            PrivacyProfile::Test => self
107                .command_allowlist
108                .as_ref()
109                .is_some_and(|al| al.contains(command)),
110            PrivacyProfile::Observe => false,
111        }
112    }
113
114    /// Apply redaction rules to the output string if redaction is enabled.
115    #[must_use]
116    pub fn redact_output(&self, output: &str) -> String {
117        if self.redaction_enabled {
118            self.redactor.redact(output)
119        } else {
120            output.to_string()
121        }
122    }
123}
124
125/// The permission matrix. Maps `(profile, tool_or_action)` → allowed.
126///
127/// Naming convention: standalone tools use bare names (`"eval_js"`), compound tool
128/// actions use dot-qualified names (`"window.manage"`, `"input.fill"`).
129///
130/// Everything not explicitly listed defaults to allowed (open-world for new tools).
131#[must_use]
132fn is_allowed_by_profile(profile: PrivacyProfile, tool_or_action: &str) -> bool {
133    match profile {
134        PrivacyProfile::FullControl => true,
135        PrivacyProfile::Test => matches!(
136            tool_or_action,
137            // Read-only observation (superset of Observe)
138            "dom_snapshot"
139                | "find_elements"
140                | "get_registry"
141                | "get_memory_stats"
142                | "get_plugin_info"
143                | "get_diagnostics"
144                | "detect_ghost_commands"
145                | "check_ipc_integrity"
146                | "resolve_command"
147                | "wait_for"
148                // Assertions (use eval internally but are test-oriented)
149                | "verify_state"
150                | "assert_semantic"
151                // Interactions
152                | "interact"
153                | "interact.click"
154                | "interact.double_click"
155                | "interact.hover"
156                | "interact.focus"
157                | "interact.scroll_into_view"
158                | "interact.select_option"
159                // Input
160                | "fill"
161                | "input"
162                | "input.fill"
163                | "type_text"
164                | "input.type_text"
165                | "input.press_key"
166                // Storage (read + write)
167                | "storage"
168                | "set_storage"
169                | "storage.set"
170                | "delete_storage"
171                | "storage.delete"
172                | "storage.get"
173                | "storage.get_cookies"
174                | "get_storage"
175                | "get_cookies"
176                // Recording
177                | "recording"
178                | "recording.start"
179                | "recording.stop"
180                | "recording.checkpoint"
181                | "recording.list_checkpoints"
182                | "recording.get_events"
183                | "recording.events_between"
184                | "recording.get_replay"
185                | "recording.export"
186                | "recording.import"
187                // Logs (all read-only)
188                | "logs"
189                | "logs.console"
190                | "logs.network"
191                | "logs.ipc"
192                | "logs.navigation"
193                | "logs.dialogs"
194                | "logs.events"
195                | "logs.slow_ipc"
196                // Inspect (read-only + visual debug)
197                | "inspect"
198                | "inspect.styles"
199                | "inspect.bounds"
200                | "inspect.highlight"
201                | "inspect.clear_highlights"
202                | "inspect.audit_a11y"
203                | "inspect.performance"
204                // Window (read-only)
205                | "list_windows"
206                | "window"
207                | "window.get_state"
208                | "window.list"
209                | "get_window_state"
210                // Navigate (read-only actions)
211                | "navigate.go_back"
212                | "navigate.get_history"
213                | "navigate.get_dialog_log"
214        ),
215        PrivacyProfile::Observe => matches!(
216            tool_or_action,
217            "dom_snapshot"
218                | "find_elements"
219                | "get_registry"
220                | "get_memory_stats"
221                | "get_plugin_info"
222                | "get_diagnostics"
223                | "detect_ghost_commands"
224                | "check_ipc_integrity"
225                | "resolve_command"
226                | "logs"
227                | "logs.console"
228                | "logs.network"
229                | "logs.ipc"
230                | "logs.navigation"
231                | "logs.dialogs"
232                | "logs.events"
233                | "logs.slow_ipc"
234                | "inspect"
235                | "inspect.styles"
236                | "inspect.bounds"
237                | "inspect.highlight"
238                | "inspect.clear_highlights"
239                | "inspect.audit_a11y"
240                | "inspect.performance"
241                | "list_windows"
242                | "window"
243                | "window.get_state"
244                | "window.list"
245                | "get_window_state"
246                | "wait_for"
247        ),
248    }
249}
250
251/// Create a [`PrivacyConfig`] for the `Observe` profile with redaction enabled.
252#[must_use]
253pub fn observe_privacy_config() -> PrivacyConfig {
254    PrivacyConfig {
255        profile: PrivacyProfile::Observe,
256        command_allowlist: None,
257        command_blocklist: HashSet::new(),
258        disabled_tools: HashSet::new(),
259        storage_key_blocklist: HashSet::new(),
260        redactor: Redactor::default(),
261        redaction_enabled: true,
262    }
263}
264
265/// Create a [`PrivacyConfig`] for the `Test` profile with redaction enabled.
266#[must_use]
267pub fn test_privacy_config() -> PrivacyConfig {
268    PrivacyConfig {
269        profile: PrivacyProfile::Test,
270        command_allowlist: None,
271        command_blocklist: HashSet::new(),
272        disabled_tools: HashSet::new(),
273        storage_key_blocklist: HashSet::new(),
274        redactor: Redactor::default(),
275        redaction_enabled: true,
276    }
277}
278
279/// Create a [`PrivacyConfig`] that disables dangerous tools and enables redaction.
280///
281/// This is an alias for [`observe_privacy_config()`] — strict mode maps to the
282/// `Observe` profile.
283#[must_use]
284pub fn strict_privacy_config() -> PrivacyConfig {
285    observe_privacy_config()
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    // ── Command filtering ──────────────────────────────────────────────────
293
294    #[test]
295    fn storage_key_blocklist_protects_keys() {
296        let mut config = PrivacyConfig::default();
297        // Default: every key is writable.
298        assert!(config.is_storage_key_allowed("auth"));
299        // Once blocked, protected keys are rejected; others still allowed.
300        config.storage_key_blocklist = ["auth", "license_tier"]
301            .iter()
302            .map(ToString::to_string)
303            .collect();
304        assert!(!config.is_storage_key_allowed("auth"));
305        assert!(!config.is_storage_key_allowed("license_tier"));
306        assert!(config.is_storage_key_allowed("theme"));
307    }
308
309    #[test]
310    fn default_allows_all_commands() {
311        let config = PrivacyConfig::default();
312        assert!(config.is_command_allowed("get_settings"));
313        assert!(config.is_command_allowed("anything"));
314    }
315
316    #[test]
317    fn blocklist_blocks() {
318        let mut config = PrivacyConfig::default();
319        config.command_blocklist.insert("save_api_key".to_string());
320        assert!(!config.is_command_allowed("save_api_key"));
321        assert!(config.is_command_allowed("get_settings"));
322    }
323
324    #[test]
325    fn allowlist_restricts() {
326        let mut allow = HashSet::new();
327        allow.insert("get_settings".to_string());
328        allow.insert("get_monitoring_status".to_string());
329        let config = PrivacyConfig {
330            command_allowlist: Some(allow),
331            ..Default::default()
332        };
333        assert!(config.is_command_allowed("get_settings"));
334        assert!(!config.is_command_allowed("save_api_key"));
335    }
336
337    #[test]
338    fn blocklist_wins_over_allowlist() {
339        let mut allow = HashSet::new();
340        allow.insert("save_api_key".to_string());
341        let mut block = HashSet::new();
342        block.insert("save_api_key".to_string());
343        let config = PrivacyConfig {
344            command_allowlist: Some(allow),
345            command_blocklist: block,
346            ..Default::default()
347        };
348        assert!(!config.is_command_allowed("save_api_key"));
349    }
350
351    // ── Profile: FullControl ───────────────────────────────────────────────
352
353    #[test]
354    fn full_control_allows_everything() {
355        let config = PrivacyConfig::default();
356        assert_eq!(config.profile, PrivacyProfile::FullControl);
357        assert!(config.is_tool_enabled("eval_js"));
358        assert!(config.is_tool_enabled("screenshot"));
359        assert!(config.is_tool_enabled("invoke_command"));
360        assert!(config.is_tool_enabled("interact"));
361        assert!(config.is_tool_enabled("interact.click"));
362        assert!(config.is_tool_enabled("input.fill"));
363        assert!(config.is_tool_enabled("window.manage"));
364        assert!(config.is_tool_enabled("navigate"));
365        assert!(config.is_tool_enabled("navigate.go_to"));
366        assert!(config.is_tool_enabled("css.inject"));
367        assert!(config.is_tool_enabled("recording"));
368        assert!(config.is_tool_enabled("storage.set"));
369        assert!(config.is_tool_enabled("set_dialog_response"));
370    }
371
372    // ── Profile: Test ──────────────────────────────────────────────────────
373
374    #[test]
375    fn test_profile_allows_interactions() {
376        let config = test_privacy_config();
377        assert!(config.is_tool_enabled("interact"));
378        assert!(config.is_tool_enabled("interact.click"));
379        assert!(config.is_tool_enabled("interact.double_click"));
380        assert!(config.is_tool_enabled("interact.hover"));
381        assert!(config.is_tool_enabled("interact.focus"));
382        assert!(config.is_tool_enabled("interact.scroll_into_view"));
383        assert!(config.is_tool_enabled("interact.select_option"));
384    }
385
386    #[test]
387    fn test_profile_allows_input() {
388        let config = test_privacy_config();
389        assert!(config.is_tool_enabled("fill"));
390        assert!(config.is_tool_enabled("input.fill"));
391        assert!(config.is_tool_enabled("type_text"));
392        assert!(config.is_tool_enabled("input.type_text"));
393        assert!(config.is_tool_enabled("input.press_key"));
394    }
395
396    #[test]
397    fn test_profile_allows_storage_writes() {
398        let config = test_privacy_config();
399        assert!(config.is_tool_enabled("set_storage"));
400        assert!(config.is_tool_enabled("storage.set"));
401        assert!(config.is_tool_enabled("delete_storage"));
402        assert!(config.is_tool_enabled("storage.delete"));
403    }
404
405    #[test]
406    fn test_profile_allows_recording() {
407        let config = test_privacy_config();
408        assert!(config.is_tool_enabled("recording"));
409        assert!(config.is_tool_enabled("recording.start"));
410        assert!(config.is_tool_enabled("recording.stop"));
411    }
412
413    #[test]
414    fn test_profile_blocks_eval_and_screenshot() {
415        let config = test_privacy_config();
416        assert!(!config.is_tool_enabled("eval_js"));
417        assert!(!config.is_tool_enabled("screenshot"));
418    }
419
420    #[test]
421    fn test_profile_blocks_navigation() {
422        let config = test_privacy_config();
423        assert!(!config.is_tool_enabled("navigate"));
424        assert!(!config.is_tool_enabled("navigate.go_to"));
425        assert!(!config.is_tool_enabled("set_dialog_response"));
426        assert!(!config.is_tool_enabled("navigate.set_dialog_response"));
427    }
428
429    #[test]
430    fn test_profile_blocks_window_mutations() {
431        let config = test_privacy_config();
432        assert!(!config.is_tool_enabled("window.manage"));
433        assert!(!config.is_tool_enabled("window.resize"));
434        assert!(!config.is_tool_enabled("window.move_to"));
435        assert!(!config.is_tool_enabled("window.set_title"));
436    }
437
438    #[test]
439    fn test_profile_blocks_css_injection() {
440        let config = test_privacy_config();
441        assert!(!config.is_tool_enabled("inject_css"));
442        assert!(!config.is_tool_enabled("css.inject"));
443        assert!(!config.is_tool_enabled("css.remove"));
444    }
445
446    #[test]
447    fn test_profile_blocks_invoke_command() {
448        let config = test_privacy_config();
449        assert!(!config.is_tool_enabled("invoke_command"));
450    }
451
452    #[test]
453    fn test_profile_allows_read_only_tools() {
454        let config = test_privacy_config();
455        assert!(config.is_tool_enabled("dom_snapshot"));
456        assert!(config.is_tool_enabled("find_elements"));
457        assert!(config.is_tool_enabled("verify_state"));
458        assert!(config.is_tool_enabled("detect_ghost_commands"));
459        assert!(config.is_tool_enabled("check_ipc_integrity"));
460        assert!(config.is_tool_enabled("get_registry"));
461        assert!(config.is_tool_enabled("get_memory_stats"));
462        assert!(config.is_tool_enabled("get_plugin_info"));
463        assert!(config.is_tool_enabled("resolve_command"));
464        assert!(config.is_tool_enabled("wait_for"));
465        assert!(config.is_tool_enabled("assert_semantic"));
466    }
467
468    // ── Profile: Observe ───────────────────────────────────────────────────
469
470    #[test]
471    fn observe_blocks_all_interactions() {
472        let config = observe_privacy_config();
473        assert!(!config.is_tool_enabled("interact"));
474        assert!(!config.is_tool_enabled("interact.click"));
475        assert!(!config.is_tool_enabled("interact.double_click"));
476        assert!(!config.is_tool_enabled("interact.hover"));
477        assert!(!config.is_tool_enabled("interact.focus"));
478        assert!(!config.is_tool_enabled("interact.scroll_into_view"));
479        assert!(!config.is_tool_enabled("interact.select_option"));
480    }
481
482    #[test]
483    fn observe_blocks_all_input() {
484        let config = observe_privacy_config();
485        assert!(!config.is_tool_enabled("fill"));
486        assert!(!config.is_tool_enabled("input.fill"));
487        assert!(!config.is_tool_enabled("type_text"));
488        assert!(!config.is_tool_enabled("input.type_text"));
489        assert!(!config.is_tool_enabled("input.press_key"));
490    }
491
492    #[test]
493    fn observe_blocks_storage_writes() {
494        let config = observe_privacy_config();
495        assert!(!config.is_tool_enabled("set_storage"));
496        assert!(!config.is_tool_enabled("storage.set"));
497        assert!(!config.is_tool_enabled("delete_storage"));
498        assert!(!config.is_tool_enabled("storage.delete"));
499    }
500
501    #[test]
502    fn observe_blocks_recording() {
503        let config = observe_privacy_config();
504        assert!(!config.is_tool_enabled("recording"));
505        assert!(!config.is_tool_enabled("recording.start"));
506        assert!(!config.is_tool_enabled("recording.stop"));
507    }
508
509    #[test]
510    fn observe_blocks_dangerous_tools() {
511        let config = observe_privacy_config();
512        assert!(!config.is_tool_enabled("eval_js"));
513        assert!(!config.is_tool_enabled("screenshot"));
514        assert!(!config.is_tool_enabled("invoke_command"));
515        assert!(!config.is_tool_enabled("navigate"));
516        assert!(!config.is_tool_enabled("navigate.go_to"));
517        assert!(!config.is_tool_enabled("inject_css"));
518        assert!(!config.is_tool_enabled("css.inject"));
519        assert!(!config.is_tool_enabled("css.remove"));
520        assert!(!config.is_tool_enabled("window.manage"));
521        assert!(!config.is_tool_enabled("window.resize"));
522        assert!(!config.is_tool_enabled("window.move_to"));
523        assert!(!config.is_tool_enabled("window.set_title"));
524    }
525
526    #[test]
527    fn observe_allows_read_only_tools() {
528        let config = observe_privacy_config();
529        assert!(config.is_tool_enabled("dom_snapshot"));
530        assert!(config.is_tool_enabled("find_elements"));
531        assert!(config.is_tool_enabled("detect_ghost_commands"));
532        assert!(config.is_tool_enabled("check_ipc_integrity"));
533        assert!(config.is_tool_enabled("get_registry"));
534        assert!(config.is_tool_enabled("get_memory_stats"));
535        assert!(config.is_tool_enabled("get_plugin_info"));
536        assert!(config.is_tool_enabled("get_diagnostics"));
537        assert!(config.is_tool_enabled("resolve_command"));
538        assert!(config.is_tool_enabled("wait_for"));
539        assert!(config.is_tool_enabled("window")); // the compound tool itself (get_state, list)
540    }
541
542    #[test]
543    fn observe_blocks_eval_dependent_tools() {
544        let config = observe_privacy_config();
545        // verify_state and assert_semantic depend on eval_js internally,
546        // so they are excluded from the Observe allowlist.
547        assert!(!config.is_tool_enabled("verify_state"));
548        assert!(!config.is_tool_enabled("assert_semantic"));
549    }
550
551    #[test]
552    fn observe_allows_read_actions_on_compound_tools() {
553        let config = observe_privacy_config();
554        // Window read actions
555        assert!(config.is_tool_enabled("window.get_state"));
556        assert!(config.is_tool_enabled("window.list"));
557        // Logs (all read-only)
558        assert!(config.is_tool_enabled("logs"));
559        assert!(config.is_tool_enabled("logs.console"));
560        assert!(config.is_tool_enabled("logs.network"));
561        assert!(config.is_tool_enabled("logs.ipc"));
562        assert!(config.is_tool_enabled("logs.navigation"));
563        assert!(config.is_tool_enabled("logs.dialogs"));
564        assert!(config.is_tool_enabled("logs.events"));
565        assert!(config.is_tool_enabled("logs.slow_ipc"));
566        // Inspect (read-only)
567        assert!(config.is_tool_enabled("inspect"));
568        assert!(config.is_tool_enabled("inspect.styles"));
569        assert!(config.is_tool_enabled("inspect.bounds"));
570        assert!(config.is_tool_enabled("inspect.highlight"));
571        assert!(config.is_tool_enabled("inspect.clear_highlights"));
572        assert!(config.is_tool_enabled("inspect.audit_a11y"));
573        assert!(config.is_tool_enabled("inspect.performance"));
574    }
575
576    #[test]
577    fn observe_blocks_unlisted_tools() {
578        let config = observe_privacy_config();
579        // Tools not in the closed-world allowlist are blocked
580        assert!(!config.is_tool_enabled("navigate.get_history"));
581        assert!(!config.is_tool_enabled("navigate.get_dialog_log"));
582        assert!(!config.is_tool_enabled("css.get_styles"));
583        assert!(!config.is_tool_enabled("css.get_computed"));
584        assert!(!config.is_tool_enabled("storage.get"));
585        assert!(!config.is_tool_enabled("storage.get_cookies"));
586        assert!(!config.is_tool_enabled("navigate.go_back"));
587    }
588
589    #[test]
590    fn observe_enables_redaction() {
591        let config = observe_privacy_config();
592        assert!(config.redaction_enabled);
593    }
594
595    // ── Explicit disable overrides profile ─────────────────────────────────
596
597    #[test]
598    fn disabled_tools_override_full_control() {
599        let mut disabled = HashSet::new();
600        disabled.insert("eval_js".to_string());
601        let config = PrivacyConfig {
602            profile: PrivacyProfile::FullControl,
603            disabled_tools: disabled,
604            ..Default::default()
605        };
606        assert!(!config.is_tool_enabled("eval_js"));
607        assert!(config.is_tool_enabled("screenshot"));
608    }
609
610    #[test]
611    fn disabled_tools_stack_with_profile() {
612        let mut disabled = HashSet::new();
613        disabled.insert("dom_snapshot".to_string());
614        let mut config = test_privacy_config();
615        config.disabled_tools = disabled;
616        // Profile allows dom_snapshot, but explicit disable overrides
617        assert!(!config.is_tool_enabled("dom_snapshot"));
618        // Profile blocks eval_js
619        assert!(!config.is_tool_enabled("eval_js"));
620    }
621
622    // ── invoke_command special handling ─────────────────────────────────────
623
624    #[test]
625    fn invoke_allowed_in_full_control() {
626        let config = PrivacyConfig::default();
627        assert!(config.is_invoke_allowed("any_command"));
628    }
629
630    #[test]
631    fn invoke_blocked_in_observe() {
632        let config = observe_privacy_config();
633        assert!(!config.is_invoke_allowed("any_command"));
634    }
635
636    #[test]
637    fn invoke_allowed_in_test_with_allowlist() {
638        let mut allow = HashSet::new();
639        allow.insert("greet".to_string());
640        let mut config = test_privacy_config();
641        config.command_allowlist = Some(allow);
642        assert!(config.is_invoke_allowed("greet"));
643        assert!(!config.is_invoke_allowed("delete_user"));
644    }
645
646    #[test]
647    fn invoke_blocked_in_test_without_allowlist() {
648        let config = test_privacy_config();
649        assert!(!config.is_invoke_allowed("greet"));
650    }
651
652    // ── strict_privacy_config is Observe ────────────────────────────────────
653
654    #[test]
655    fn strict_privacy_is_observe_profile() {
656        let config = strict_privacy_config();
657        assert_eq!(config.profile, PrivacyProfile::Observe);
658        assert!(config.redaction_enabled);
659    }
660
661    // ── Backward compatibility ──────────────────────────────────────────────
662
663    #[test]
664    fn strict_mode_disables_dangerous_tools() {
665        let config = strict_privacy_config();
666        assert!(!config.is_tool_enabled("eval_js"));
667        assert!(!config.is_tool_enabled("screenshot"));
668        assert!(!config.is_tool_enabled("inject_css"));
669        assert!(!config.is_tool_enabled("navigate"));
670        assert!(!config.is_tool_enabled("invoke_command"));
671        assert!(config.is_tool_enabled("dom_snapshot"));
672        assert!(config.is_tool_enabled("get_memory_stats"));
673        assert!(config.redaction_enabled);
674    }
675
676    #[test]
677    fn strict_mode_blocks_window_mutations() {
678        let config = strict_privacy_config();
679        assert!(!config.is_tool_enabled("window.manage"));
680        assert!(!config.is_tool_enabled("window.resize"));
681        assert!(!config.is_tool_enabled("window.move_to"));
682        assert!(!config.is_tool_enabled("window.set_title"));
683        assert!(config.is_tool_enabled("window"));
684    }
685
686    #[test]
687    fn default_allows_all_actions() {
688        let config = PrivacyConfig::default();
689        assert!(config.is_tool_enabled("invoke_command"));
690        assert!(config.is_tool_enabled("window.manage"));
691        assert!(config.is_tool_enabled("window.resize"));
692        assert!(config.is_tool_enabled("window.move_to"));
693        assert!(config.is_tool_enabled("window.set_title"));
694    }
695
696    // ── Redaction ───────────────────────────────────────────────────────────
697
698    #[test]
699    fn redaction_when_enabled() {
700        let config = PrivacyConfig {
701            redaction_enabled: true,
702            ..Default::default()
703        };
704        let output = config.redact_output("key is sk-abc123def456ghi789jkl012mno");
705        assert!(output.contains("[REDACTED]"));
706    }
707
708    #[test]
709    fn no_redaction_when_disabled() {
710        let config = PrivacyConfig::default();
711        let input = "key is sk-abc123def456ghi789jkl012mno";
712        assert_eq!(config.redact_output(input), input);
713    }
714
715    // ── Display ─────────────────────────────────────────────────────────────
716
717    #[test]
718    fn profile_display() {
719        assert_eq!(PrivacyProfile::Observe.to_string(), "observe");
720        assert_eq!(PrivacyProfile::Test.to_string(), "test");
721        assert_eq!(PrivacyProfile::FullControl.to_string(), "full_control");
722    }
723}