victauri_plugin/
privacy.rs1use std::collections::HashSet;
2
3use crate::redaction::Redactor;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum PrivacyProfile {
17 Observe,
20 Test,
24 #[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#[derive(Default)]
46pub struct PrivacyConfig {
47 pub profile: PrivacyProfile,
49 pub command_allowlist: Option<HashSet<String>>,
51 pub command_blocklist: HashSet<String>,
53 pub disabled_tools: HashSet<String>,
55 pub storage_key_blocklist: HashSet<String>,
58 pub redactor: Redactor,
60 pub redaction_enabled: bool,
62}
63
64impl PrivacyConfig {
65 #[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 #[must_use]
81 pub fn is_storage_key_allowed(&self, key: &str) -> bool {
82 !self.storage_key_blocklist.contains(key)
83 }
84
85 #[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 #[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 #[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#[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 "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 | "app_state"
149 | "verify_state"
151 | "assert_semantic"
152 | "interact"
154 | "interact.click"
155 | "interact.double_click"
156 | "interact.hover"
157 | "interact.focus"
158 | "interact.scroll_into_view"
159 | "interact.select_option"
160 | "fill"
162 | "input"
163 | "input.fill"
164 | "type_text"
165 | "input.type_text"
166 | "input.press_key"
167 | "storage"
169 | "set_storage"
170 | "storage.set"
171 | "delete_storage"
172 | "storage.delete"
173 | "storage.get"
174 | "storage.get_cookies"
175 | "get_storage"
176 | "get_cookies"
177 | "recording"
179 | "recording.start"
180 | "recording.stop"
181 | "recording.checkpoint"
182 | "recording.list_checkpoints"
183 | "recording.get_events"
184 | "recording.events_between"
185 | "recording.get_replay"
186 | "recording.export"
187 | "recording.import"
188 | "logs"
190 | "logs.console"
191 | "logs.network"
192 | "logs.ipc"
193 | "logs.navigation"
194 | "logs.dialogs"
195 | "logs.events"
196 | "logs.slow_ipc"
197 | "logs.clear"
200 | "inspect"
202 | "inspect.styles"
203 | "inspect.bounds"
204 | "inspect.highlight"
205 | "inspect.clear_highlights"
206 | "inspect.audit_a11y"
207 | "inspect.performance"
208 | "list_windows"
210 | "window"
211 | "window.get_state"
212 | "window.list"
213 | "get_window_state"
214 | "navigate.go_back"
216 | "navigate.get_history"
217 | "navigate.get_dialog_log"
218 ),
219 PrivacyProfile::Observe => matches!(
220 tool_or_action,
221 "dom_snapshot"
222 | "find_elements"
223 | "get_registry"
224 | "get_memory_stats"
225 | "get_plugin_info"
226 | "get_diagnostics"
227 | "detect_ghost_commands"
228 | "check_ipc_integrity"
229 | "resolve_command"
230 | "app_state"
231 | "logs"
232 | "logs.console"
233 | "logs.network"
234 | "logs.ipc"
235 | "logs.navigation"
236 | "logs.dialogs"
237 | "logs.events"
238 | "logs.slow_ipc"
239 | "inspect"
242 | "inspect.styles"
243 | "inspect.bounds"
244 | "inspect.audit_a11y"
247 | "inspect.performance"
248 | "list_windows"
249 | "window"
250 | "window.get_state"
251 | "window.list"
252 | "get_window_state"
253 | "wait_for"
254 ),
255 }
256}
257
258#[must_use]
260pub fn observe_privacy_config() -> PrivacyConfig {
261 PrivacyConfig {
262 profile: PrivacyProfile::Observe,
263 command_allowlist: None,
264 command_blocklist: HashSet::new(),
265 disabled_tools: HashSet::new(),
266 storage_key_blocklist: HashSet::new(),
267 redactor: Redactor::default(),
268 redaction_enabled: true,
269 }
270}
271
272#[must_use]
274pub fn test_privacy_config() -> PrivacyConfig {
275 PrivacyConfig {
276 profile: PrivacyProfile::Test,
277 command_allowlist: None,
278 command_blocklist: HashSet::new(),
279 disabled_tools: HashSet::new(),
280 storage_key_blocklist: HashSet::new(),
281 redactor: Redactor::default(),
282 redaction_enabled: true,
283 }
284}
285
286#[must_use]
291pub fn strict_privacy_config() -> PrivacyConfig {
292 observe_privacy_config()
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
302 fn storage_key_blocklist_protects_keys() {
303 let mut config = PrivacyConfig::default();
304 assert!(config.is_storage_key_allowed("auth"));
306 config.storage_key_blocklist = ["auth", "license_tier"]
308 .iter()
309 .map(ToString::to_string)
310 .collect();
311 assert!(!config.is_storage_key_allowed("auth"));
312 assert!(!config.is_storage_key_allowed("license_tier"));
313 assert!(config.is_storage_key_allowed("theme"));
314 }
315
316 #[test]
317 fn default_allows_all_commands() {
318 let config = PrivacyConfig::default();
319 assert!(config.is_command_allowed("get_settings"));
320 assert!(config.is_command_allowed("anything"));
321 }
322
323 #[test]
324 fn blocklist_blocks() {
325 let mut config = PrivacyConfig::default();
326 config.command_blocklist.insert("save_api_key".to_string());
327 assert!(!config.is_command_allowed("save_api_key"));
328 assert!(config.is_command_allowed("get_settings"));
329 }
330
331 #[test]
332 fn allowlist_restricts() {
333 let mut allow = HashSet::new();
334 allow.insert("get_settings".to_string());
335 allow.insert("get_monitoring_status".to_string());
336 let config = PrivacyConfig {
337 command_allowlist: Some(allow),
338 ..Default::default()
339 };
340 assert!(config.is_command_allowed("get_settings"));
341 assert!(!config.is_command_allowed("save_api_key"));
342 }
343
344 #[test]
345 fn blocklist_wins_over_allowlist() {
346 let mut allow = HashSet::new();
347 allow.insert("save_api_key".to_string());
348 let mut block = HashSet::new();
349 block.insert("save_api_key".to_string());
350 let config = PrivacyConfig {
351 command_allowlist: Some(allow),
352 command_blocklist: block,
353 ..Default::default()
354 };
355 assert!(!config.is_command_allowed("save_api_key"));
356 }
357
358 #[test]
361 fn full_control_allows_everything() {
362 let config = PrivacyConfig::default();
363 assert_eq!(config.profile, PrivacyProfile::FullControl);
364 assert!(config.is_tool_enabled("eval_js"));
365 assert!(config.is_tool_enabled("screenshot"));
366 assert!(config.is_tool_enabled("invoke_command"));
367 assert!(config.is_tool_enabled("interact"));
368 assert!(config.is_tool_enabled("interact.click"));
369 assert!(config.is_tool_enabled("input.fill"));
370 assert!(config.is_tool_enabled("window.manage"));
371 assert!(config.is_tool_enabled("navigate"));
372 assert!(config.is_tool_enabled("navigate.go_to"));
373 assert!(config.is_tool_enabled("css.inject"));
374 assert!(config.is_tool_enabled("recording"));
375 assert!(config.is_tool_enabled("storage.set"));
376 assert!(config.is_tool_enabled("set_dialog_response"));
377 }
378
379 #[test]
382 fn test_profile_allows_interactions() {
383 let config = test_privacy_config();
384 assert!(config.is_tool_enabled("interact"));
385 assert!(config.is_tool_enabled("interact.click"));
386 assert!(config.is_tool_enabled("interact.double_click"));
387 assert!(config.is_tool_enabled("interact.hover"));
388 assert!(config.is_tool_enabled("interact.focus"));
389 assert!(config.is_tool_enabled("interact.scroll_into_view"));
390 assert!(config.is_tool_enabled("interact.select_option"));
391 }
392
393 #[test]
394 fn test_profile_allows_input() {
395 let config = test_privacy_config();
396 assert!(config.is_tool_enabled("fill"));
397 assert!(config.is_tool_enabled("input.fill"));
398 assert!(config.is_tool_enabled("type_text"));
399 assert!(config.is_tool_enabled("input.type_text"));
400 assert!(config.is_tool_enabled("input.press_key"));
401 }
402
403 #[test]
404 fn test_profile_allows_storage_writes() {
405 let config = test_privacy_config();
406 assert!(config.is_tool_enabled("set_storage"));
407 assert!(config.is_tool_enabled("storage.set"));
408 assert!(config.is_tool_enabled("delete_storage"));
409 assert!(config.is_tool_enabled("storage.delete"));
410 }
411
412 #[test]
413 fn test_profile_allows_recording() {
414 let config = test_privacy_config();
415 assert!(config.is_tool_enabled("recording"));
416 assert!(config.is_tool_enabled("recording.start"));
417 assert!(config.is_tool_enabled("recording.stop"));
418 }
419
420 #[test]
421 fn test_profile_blocks_eval_and_screenshot() {
422 let config = test_privacy_config();
423 assert!(!config.is_tool_enabled("eval_js"));
424 assert!(!config.is_tool_enabled("screenshot"));
425 }
426
427 #[test]
428 fn test_profile_blocks_navigation() {
429 let config = test_privacy_config();
430 assert!(!config.is_tool_enabled("navigate"));
431 assert!(!config.is_tool_enabled("navigate.go_to"));
432 assert!(!config.is_tool_enabled("set_dialog_response"));
433 assert!(!config.is_tool_enabled("navigate.set_dialog_response"));
434 }
435
436 #[test]
437 fn test_profile_blocks_window_mutations() {
438 let config = test_privacy_config();
439 assert!(!config.is_tool_enabled("window.manage"));
440 assert!(!config.is_tool_enabled("window.resize"));
441 assert!(!config.is_tool_enabled("window.move_to"));
442 assert!(!config.is_tool_enabled("window.set_title"));
443 }
444
445 #[test]
446 fn test_profile_blocks_css_injection() {
447 let config = test_privacy_config();
448 assert!(!config.is_tool_enabled("inject_css"));
449 assert!(!config.is_tool_enabled("css.inject"));
450 assert!(!config.is_tool_enabled("css.remove"));
451 }
452
453 #[test]
454 fn test_profile_blocks_invoke_command() {
455 let config = test_privacy_config();
456 assert!(!config.is_tool_enabled("invoke_command"));
457 }
458
459 #[test]
460 fn test_profile_allows_read_only_tools() {
461 let config = test_privacy_config();
462 assert!(config.is_tool_enabled("dom_snapshot"));
463 assert!(config.is_tool_enabled("find_elements"));
464 assert!(config.is_tool_enabled("verify_state"));
465 assert!(config.is_tool_enabled("detect_ghost_commands"));
466 assert!(config.is_tool_enabled("check_ipc_integrity"));
467 assert!(config.is_tool_enabled("get_registry"));
468 assert!(config.is_tool_enabled("get_memory_stats"));
469 assert!(config.is_tool_enabled("get_plugin_info"));
470 assert!(config.is_tool_enabled("resolve_command"));
471 assert!(config.is_tool_enabled("wait_for"));
472 assert!(config.is_tool_enabled("assert_semantic"));
473 }
474
475 #[test]
478 fn observe_blocks_all_interactions() {
479 let config = observe_privacy_config();
480 assert!(!config.is_tool_enabled("interact"));
481 assert!(!config.is_tool_enabled("interact.click"));
482 assert!(!config.is_tool_enabled("interact.double_click"));
483 assert!(!config.is_tool_enabled("interact.hover"));
484 assert!(!config.is_tool_enabled("interact.focus"));
485 assert!(!config.is_tool_enabled("interact.scroll_into_view"));
486 assert!(!config.is_tool_enabled("interact.select_option"));
487 }
488
489 #[test]
490 fn observe_blocks_all_input() {
491 let config = observe_privacy_config();
492 assert!(!config.is_tool_enabled("fill"));
493 assert!(!config.is_tool_enabled("input.fill"));
494 assert!(!config.is_tool_enabled("type_text"));
495 assert!(!config.is_tool_enabled("input.type_text"));
496 assert!(!config.is_tool_enabled("input.press_key"));
497 }
498
499 #[test]
500 fn observe_blocks_storage_writes() {
501 let config = observe_privacy_config();
502 assert!(!config.is_tool_enabled("set_storage"));
503 assert!(!config.is_tool_enabled("storage.set"));
504 assert!(!config.is_tool_enabled("delete_storage"));
505 assert!(!config.is_tool_enabled("storage.delete"));
506 }
507
508 #[test]
509 fn observe_blocks_recording() {
510 let config = observe_privacy_config();
511 assert!(!config.is_tool_enabled("recording"));
512 assert!(!config.is_tool_enabled("recording.start"));
513 assert!(!config.is_tool_enabled("recording.stop"));
514 }
515
516 #[test]
517 fn observe_blocks_dangerous_tools() {
518 let config = observe_privacy_config();
519 assert!(!config.is_tool_enabled("eval_js"));
520 assert!(!config.is_tool_enabled("screenshot"));
521 assert!(!config.is_tool_enabled("invoke_command"));
522 assert!(!config.is_tool_enabled("navigate"));
523 assert!(!config.is_tool_enabled("navigate.go_to"));
524 assert!(!config.is_tool_enabled("inject_css"));
525 assert!(!config.is_tool_enabled("css.inject"));
526 assert!(!config.is_tool_enabled("css.remove"));
527 assert!(!config.is_tool_enabled("window.manage"));
528 assert!(!config.is_tool_enabled("window.resize"));
529 assert!(!config.is_tool_enabled("window.move_to"));
530 assert!(!config.is_tool_enabled("window.set_title"));
531 }
532
533 #[test]
534 fn observe_allows_read_only_tools() {
535 let config = observe_privacy_config();
536 assert!(config.is_tool_enabled("dom_snapshot"));
537 assert!(config.is_tool_enabled("find_elements"));
538 assert!(config.is_tool_enabled("detect_ghost_commands"));
539 assert!(config.is_tool_enabled("check_ipc_integrity"));
540 assert!(config.is_tool_enabled("get_registry"));
541 assert!(config.is_tool_enabled("get_memory_stats"));
542 assert!(config.is_tool_enabled("get_plugin_info"));
543 assert!(config.is_tool_enabled("get_diagnostics"));
544 assert!(config.is_tool_enabled("resolve_command"));
545 assert!(config.is_tool_enabled("wait_for"));
546 assert!(config.is_tool_enabled("window")); }
548
549 #[test]
550 fn observe_blocks_eval_dependent_tools() {
551 let config = observe_privacy_config();
552 assert!(!config.is_tool_enabled("verify_state"));
555 assert!(!config.is_tool_enabled("assert_semantic"));
556 }
557
558 #[test]
559 fn observe_allows_read_actions_on_compound_tools() {
560 let config = observe_privacy_config();
561 assert!(config.is_tool_enabled("window.get_state"));
563 assert!(config.is_tool_enabled("window.list"));
564 assert!(config.is_tool_enabled("logs"));
566 assert!(config.is_tool_enabled("logs.console"));
567 assert!(config.is_tool_enabled("logs.network"));
568 assert!(config.is_tool_enabled("logs.ipc"));
569 assert!(config.is_tool_enabled("logs.navigation"));
570 assert!(config.is_tool_enabled("logs.dialogs"));
571 assert!(config.is_tool_enabled("logs.events"));
572 assert!(config.is_tool_enabled("logs.slow_ipc"));
573 assert!(config.is_tool_enabled("inspect"));
575 assert!(config.is_tool_enabled("inspect.styles"));
576 assert!(config.is_tool_enabled("inspect.bounds"));
577 assert!(config.is_tool_enabled("inspect.audit_a11y"));
578 assert!(config.is_tool_enabled("inspect.performance"));
579 assert!(!config.is_tool_enabled("inspect.highlight"));
581 assert!(!config.is_tool_enabled("inspect.clear_highlights"));
582 assert!(!config.is_tool_enabled("logs.clear"));
583 }
584
585 #[test]
586 fn observe_allows_app_state_probe_reads() {
587 assert!(observe_privacy_config().is_tool_enabled("app_state"));
589 }
590
591 #[test]
592 fn test_profile_allows_log_clearing_and_highlight() {
593 let config = test_privacy_config();
596 assert!(config.is_tool_enabled("logs.clear"));
597 assert!(config.is_tool_enabled("inspect.highlight"));
598 assert!(config.is_tool_enabled("inspect.clear_highlights"));
599 assert!(config.is_tool_enabled("app_state"));
600 }
601
602 #[test]
603 fn observe_blocks_unlisted_tools() {
604 let config = observe_privacy_config();
605 assert!(!config.is_tool_enabled("navigate.get_history"));
607 assert!(!config.is_tool_enabled("navigate.get_dialog_log"));
608 assert!(!config.is_tool_enabled("css.get_styles"));
609 assert!(!config.is_tool_enabled("css.get_computed"));
610 assert!(!config.is_tool_enabled("storage.get"));
611 assert!(!config.is_tool_enabled("storage.get_cookies"));
612 assert!(!config.is_tool_enabled("navigate.go_back"));
613 }
614
615 #[test]
616 fn observe_enables_redaction() {
617 let config = observe_privacy_config();
618 assert!(config.redaction_enabled);
619 }
620
621 #[test]
624 fn disabled_tools_override_full_control() {
625 let mut disabled = HashSet::new();
626 disabled.insert("eval_js".to_string());
627 let config = PrivacyConfig {
628 profile: PrivacyProfile::FullControl,
629 disabled_tools: disabled,
630 ..Default::default()
631 };
632 assert!(!config.is_tool_enabled("eval_js"));
633 assert!(config.is_tool_enabled("screenshot"));
634 }
635
636 #[test]
637 fn disabled_tools_stack_with_profile() {
638 let mut disabled = HashSet::new();
639 disabled.insert("dom_snapshot".to_string());
640 let mut config = test_privacy_config();
641 config.disabled_tools = disabled;
642 assert!(!config.is_tool_enabled("dom_snapshot"));
644 assert!(!config.is_tool_enabled("eval_js"));
646 }
647
648 #[test]
651 fn invoke_allowed_in_full_control() {
652 let config = PrivacyConfig::default();
653 assert!(config.is_invoke_allowed("any_command"));
654 }
655
656 #[test]
657 fn invoke_blocked_in_observe() {
658 let config = observe_privacy_config();
659 assert!(!config.is_invoke_allowed("any_command"));
660 }
661
662 #[test]
663 fn invoke_allowed_in_test_with_allowlist() {
664 let mut allow = HashSet::new();
665 allow.insert("greet".to_string());
666 let mut config = test_privacy_config();
667 config.command_allowlist = Some(allow);
668 assert!(config.is_invoke_allowed("greet"));
669 assert!(!config.is_invoke_allowed("delete_user"));
670 }
671
672 #[test]
673 fn invoke_blocked_in_test_without_allowlist() {
674 let config = test_privacy_config();
675 assert!(!config.is_invoke_allowed("greet"));
676 }
677
678 #[test]
681 fn strict_privacy_is_observe_profile() {
682 let config = strict_privacy_config();
683 assert_eq!(config.profile, PrivacyProfile::Observe);
684 assert!(config.redaction_enabled);
685 }
686
687 #[test]
690 fn strict_mode_disables_dangerous_tools() {
691 let config = strict_privacy_config();
692 assert!(!config.is_tool_enabled("eval_js"));
693 assert!(!config.is_tool_enabled("screenshot"));
694 assert!(!config.is_tool_enabled("inject_css"));
695 assert!(!config.is_tool_enabled("navigate"));
696 assert!(!config.is_tool_enabled("invoke_command"));
697 assert!(config.is_tool_enabled("dom_snapshot"));
698 assert!(config.is_tool_enabled("get_memory_stats"));
699 assert!(config.redaction_enabled);
700 }
701
702 #[test]
703 fn strict_mode_blocks_window_mutations() {
704 let config = strict_privacy_config();
705 assert!(!config.is_tool_enabled("window.manage"));
706 assert!(!config.is_tool_enabled("window.resize"));
707 assert!(!config.is_tool_enabled("window.move_to"));
708 assert!(!config.is_tool_enabled("window.set_title"));
709 assert!(config.is_tool_enabled("window"));
710 }
711
712 #[test]
713 fn default_allows_all_actions() {
714 let config = PrivacyConfig::default();
715 assert!(config.is_tool_enabled("invoke_command"));
716 assert!(config.is_tool_enabled("window.manage"));
717 assert!(config.is_tool_enabled("window.resize"));
718 assert!(config.is_tool_enabled("window.move_to"));
719 assert!(config.is_tool_enabled("window.set_title"));
720 }
721
722 #[test]
725 fn redaction_when_enabled() {
726 let config = PrivacyConfig {
727 redaction_enabled: true,
728 ..Default::default()
729 };
730 let output = config.redact_output("key is sk-abc123def456ghi789jkl012mno");
731 assert!(output.contains("[REDACTED]"));
732 }
733
734 #[test]
735 fn no_redaction_when_disabled() {
736 let config = PrivacyConfig::default();
737 let input = "key is sk-abc123def456ghi789jkl012mno";
738 assert_eq!(config.redact_output(input), input);
739 }
740
741 #[test]
744 fn profile_display() {
745 assert_eq!(PrivacyProfile::Observe.to_string(), "observe");
746 assert_eq!(PrivacyProfile::Test.to_string(), "test");
747 assert_eq!(PrivacyProfile::FullControl.to_string(), "full_control");
748 }
749}