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]
111 pub fn is_call_allowed(&self, bare_tool: &str, capability: &str) -> bool {
112 if self.disabled_tools.contains(bare_tool) {
113 return false;
114 }
115 self.is_tool_enabled(capability)
116 }
117
118 #[must_use]
123 pub fn is_invoke_allowed(&self, command: &str) -> bool {
124 if self.disabled_tools.contains("invoke_command") {
125 return false;
126 }
127 match self.profile {
128 PrivacyProfile::FullControl => true,
129 PrivacyProfile::Test => self
130 .command_allowlist
131 .as_ref()
132 .is_some_and(|al| al.contains(command)),
133 PrivacyProfile::Observe => false,
134 }
135 }
136
137 #[must_use]
139 pub fn redact_output(&self, output: &str) -> String {
140 if self.redaction_enabled {
141 self.redactor.redact(output)
142 } else {
143 output.to_string()
144 }
145 }
146}
147
148#[must_use]
155fn is_allowed_by_profile(profile: PrivacyProfile, tool_or_action: &str) -> bool {
156 match profile {
157 PrivacyProfile::FullControl => true,
158 PrivacyProfile::Test => matches!(
159 tool_or_action,
160 "dom_snapshot"
162 | "find_elements"
163 | "get_registry"
164 | "get_memory_stats"
165 | "get_plugin_info"
166 | "get_diagnostics"
167 | "detect_ghost_commands"
168 | "check_ipc_integrity"
169 | "resolve_command"
170 | "wait_for"
171 | "app_state"
172 | "interact"
180 | "interact.click"
181 | "interact.double_click"
182 | "interact.hover"
183 | "interact.focus"
184 | "interact.scroll_into_view"
185 | "interact.select_option"
186 | "fill"
188 | "input"
189 | "input.fill"
190 | "type_text"
191 | "input.type_text"
192 | "input.press_key"
193 | "storage"
195 | "set_storage"
196 | "storage.set"
197 | "delete_storage"
198 | "storage.delete"
199 | "storage.get"
200 | "storage.get_cookies"
201 | "get_storage"
202 | "get_cookies"
203 | "recording"
205 | "recording.start"
206 | "recording.stop"
207 | "recording.checkpoint"
208 | "recording.list_checkpoints"
209 | "recording.get_events"
210 | "recording.events_between"
211 | "recording.get_replay"
212 | "recording.export"
213 | "recording.import"
214 | "logs"
216 | "logs.console"
217 | "logs.network"
218 | "logs.ipc"
219 | "logs.navigation"
220 | "logs.dialogs"
221 | "logs.events"
222 | "logs.slow_ipc"
223 | "logs.clear"
226 | "inspect"
228 | "inspect.styles"
229 | "inspect.bounds"
230 | "inspect.highlight"
231 | "inspect.clear_highlights"
232 | "inspect.audit_a11y"
233 | "inspect.performance"
234 | "list_windows"
236 | "window"
237 | "window.get_state"
238 | "window.list"
239 | "window.introspectability"
240 | "get_window_state"
241 | "navigate"
246 | "navigate.go_back"
247 | "navigate.get_history"
248 | "navigate.get_dialog_log"
249 ),
250 PrivacyProfile::Observe => matches!(
251 tool_or_action,
252 "dom_snapshot"
253 | "find_elements"
254 | "get_registry"
255 | "get_memory_stats"
256 | "get_plugin_info"
257 | "get_diagnostics"
258 | "detect_ghost_commands"
259 | "check_ipc_integrity"
260 | "resolve_command"
261 | "app_state"
262 | "logs"
263 | "logs.console"
264 | "logs.network"
265 | "logs.ipc"
266 | "logs.navigation"
267 | "logs.dialogs"
268 | "logs.events"
269 | "logs.slow_ipc"
270 | "inspect"
273 | "inspect.styles"
274 | "inspect.bounds"
275 | "inspect.audit_a11y"
278 | "inspect.performance"
279 | "list_windows"
280 | "window"
281 | "window.get_state"
282 | "window.list"
283 | "window.introspectability"
284 | "get_window_state"
285 | "wait_for"
286 ),
287 }
288}
289
290#[must_use]
292pub fn observe_privacy_config() -> PrivacyConfig {
293 PrivacyConfig {
294 profile: PrivacyProfile::Observe,
295 command_allowlist: None,
296 command_blocklist: HashSet::new(),
297 disabled_tools: HashSet::new(),
298 storage_key_blocklist: HashSet::new(),
299 redactor: Redactor::default(),
300 redaction_enabled: true,
301 }
302}
303
304#[must_use]
306pub fn test_privacy_config() -> PrivacyConfig {
307 PrivacyConfig {
308 profile: PrivacyProfile::Test,
309 command_allowlist: None,
310 command_blocklist: HashSet::new(),
311 disabled_tools: HashSet::new(),
312 storage_key_blocklist: HashSet::new(),
313 redactor: Redactor::default(),
314 redaction_enabled: true,
315 }
316}
317
318#[must_use]
323pub fn strict_privacy_config() -> PrivacyConfig {
324 observe_privacy_config()
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
334 fn storage_key_blocklist_protects_keys() {
335 let mut config = PrivacyConfig::default();
336 assert!(config.is_storage_key_allowed("auth"));
338 config.storage_key_blocklist = ["auth", "license_tier"]
340 .iter()
341 .map(ToString::to_string)
342 .collect();
343 assert!(!config.is_storage_key_allowed("auth"));
344 assert!(!config.is_storage_key_allowed("license_tier"));
345 assert!(config.is_storage_key_allowed("theme"));
346 }
347
348 #[test]
349 fn default_allows_all_commands() {
350 let config = PrivacyConfig::default();
351 assert!(config.is_command_allowed("get_settings"));
352 assert!(config.is_command_allowed("anything"));
353 }
354
355 #[test]
356 fn blocklist_blocks() {
357 let mut config = PrivacyConfig::default();
358 config.command_blocklist.insert("save_api_key".to_string());
359 assert!(!config.is_command_allowed("save_api_key"));
360 assert!(config.is_command_allowed("get_settings"));
361 }
362
363 #[test]
364 fn allowlist_restricts() {
365 let mut allow = HashSet::new();
366 allow.insert("get_settings".to_string());
367 allow.insert("get_monitoring_status".to_string());
368 let config = PrivacyConfig {
369 command_allowlist: Some(allow),
370 ..Default::default()
371 };
372 assert!(config.is_command_allowed("get_settings"));
373 assert!(!config.is_command_allowed("save_api_key"));
374 }
375
376 #[test]
377 fn blocklist_wins_over_allowlist() {
378 let mut allow = HashSet::new();
379 allow.insert("save_api_key".to_string());
380 let mut block = HashSet::new();
381 block.insert("save_api_key".to_string());
382 let config = PrivacyConfig {
383 command_allowlist: Some(allow),
384 command_blocklist: block,
385 ..Default::default()
386 };
387 assert!(!config.is_command_allowed("save_api_key"));
388 }
389
390 #[test]
393 fn full_control_allows_everything() {
394 let config = PrivacyConfig::default();
395 assert_eq!(config.profile, PrivacyProfile::FullControl);
396 assert!(config.is_tool_enabled("eval_js"));
397 assert!(config.is_tool_enabled("screenshot"));
398 assert!(config.is_tool_enabled("invoke_command"));
399 assert!(config.is_tool_enabled("interact"));
400 assert!(config.is_tool_enabled("interact.click"));
401 assert!(config.is_tool_enabled("input.fill"));
402 assert!(config.is_tool_enabled("window.manage"));
403 assert!(config.is_tool_enabled("navigate"));
404 assert!(config.is_tool_enabled("navigate.go_to"));
405 assert!(config.is_tool_enabled("css.inject"));
406 assert!(config.is_tool_enabled("recording"));
407 assert!(config.is_tool_enabled("storage.set"));
408 assert!(config.is_tool_enabled("set_dialog_response"));
409 }
410
411 #[test]
414 fn test_profile_allows_interactions() {
415 let config = test_privacy_config();
416 assert!(config.is_tool_enabled("interact"));
417 assert!(config.is_tool_enabled("interact.click"));
418 assert!(config.is_tool_enabled("interact.double_click"));
419 assert!(config.is_tool_enabled("interact.hover"));
420 assert!(config.is_tool_enabled("interact.focus"));
421 assert!(config.is_tool_enabled("interact.scroll_into_view"));
422 assert!(config.is_tool_enabled("interact.select_option"));
423 }
424
425 #[test]
426 fn test_profile_allows_input() {
427 let config = test_privacy_config();
428 assert!(config.is_tool_enabled("fill"));
429 assert!(config.is_tool_enabled("input.fill"));
430 assert!(config.is_tool_enabled("type_text"));
431 assert!(config.is_tool_enabled("input.type_text"));
432 assert!(config.is_tool_enabled("input.press_key"));
433 }
434
435 #[test]
436 fn test_profile_allows_storage_writes() {
437 let config = test_privacy_config();
438 assert!(config.is_tool_enabled("set_storage"));
439 assert!(config.is_tool_enabled("storage.set"));
440 assert!(config.is_tool_enabled("delete_storage"));
441 assert!(config.is_tool_enabled("storage.delete"));
442 }
443
444 #[test]
445 fn test_profile_allows_recording() {
446 let config = test_privacy_config();
447 assert!(config.is_tool_enabled("recording"));
448 assert!(config.is_tool_enabled("recording.start"));
449 assert!(config.is_tool_enabled("recording.stop"));
450 }
451
452 #[test]
453 fn test_profile_blocks_eval_and_screenshot() {
454 let config = test_privacy_config();
455 assert!(!config.is_tool_enabled("eval_js"));
456 assert!(!config.is_tool_enabled("screenshot"));
457 }
458
459 #[test]
460 fn test_profile_blocks_navigation_mutations() {
461 let config = test_privacy_config();
462 assert!(config.is_tool_enabled("navigate"));
466 assert!(!config.is_tool_enabled("navigate.go_to"));
467 assert!(!config.is_tool_enabled("set_dialog_response"));
468 assert!(!config.is_tool_enabled("navigate.set_dialog_response"));
469 }
470
471 #[test]
472 fn test_profile_blocks_window_mutations() {
473 let config = test_privacy_config();
474 assert!(!config.is_tool_enabled("window.manage"));
475 assert!(!config.is_tool_enabled("window.resize"));
476 assert!(!config.is_tool_enabled("window.move_to"));
477 assert!(!config.is_tool_enabled("window.set_title"));
478 }
479
480 #[test]
481 fn test_profile_blocks_css_injection() {
482 let config = test_privacy_config();
483 assert!(!config.is_tool_enabled("inject_css"));
484 assert!(!config.is_tool_enabled("css.inject"));
485 assert!(!config.is_tool_enabled("css.remove"));
486 }
487
488 #[test]
489 fn test_profile_blocks_invoke_command() {
490 let config = test_privacy_config();
491 assert!(!config.is_tool_enabled("invoke_command"));
492 }
493
494 #[test]
495 fn test_profile_allows_read_only_tools() {
496 let config = test_privacy_config();
497 assert!(config.is_tool_enabled("dom_snapshot"));
498 assert!(config.is_tool_enabled("find_elements"));
499 assert!(config.is_tool_enabled("detect_ghost_commands"));
500 assert!(config.is_tool_enabled("check_ipc_integrity"));
501 assert!(config.is_tool_enabled("get_registry"));
502 assert!(config.is_tool_enabled("get_memory_stats"));
503 assert!(config.is_tool_enabled("get_plugin_info"));
504 assert!(config.is_tool_enabled("resolve_command"));
505 assert!(config.is_tool_enabled("wait_for"));
506 }
507
508 #[test]
509 fn test_profile_blocks_arbitrary_eval_assertions() {
510 let config = test_privacy_config();
513 assert!(!config.is_tool_enabled("verify_state"));
514 assert!(!config.is_tool_enabled("assert_semantic"));
515 }
516
517 #[test]
518 fn test_profile_allows_navigation_reads_and_introspectability() {
519 let config = test_privacy_config();
522 assert!(config.is_tool_enabled("navigate.go_back"));
523 assert!(config.is_tool_enabled("navigate.get_history"));
524 assert!(config.is_tool_enabled("navigate.get_dialog_log"));
525 assert!(config.is_tool_enabled("window.introspectability"));
526 assert!(!config.is_tool_enabled("navigate.go_to"));
528 assert!(!config.is_tool_enabled("navigate.set_dialog_response"));
529 }
530
531 #[test]
534 fn observe_blocks_all_interactions() {
535 let config = observe_privacy_config();
536 assert!(!config.is_tool_enabled("interact"));
537 assert!(!config.is_tool_enabled("interact.click"));
538 assert!(!config.is_tool_enabled("interact.double_click"));
539 assert!(!config.is_tool_enabled("interact.hover"));
540 assert!(!config.is_tool_enabled("interact.focus"));
541 assert!(!config.is_tool_enabled("interact.scroll_into_view"));
542 assert!(!config.is_tool_enabled("interact.select_option"));
543 }
544
545 #[test]
546 fn observe_blocks_all_input() {
547 let config = observe_privacy_config();
548 assert!(!config.is_tool_enabled("fill"));
549 assert!(!config.is_tool_enabled("input.fill"));
550 assert!(!config.is_tool_enabled("type_text"));
551 assert!(!config.is_tool_enabled("input.type_text"));
552 assert!(!config.is_tool_enabled("input.press_key"));
553 }
554
555 #[test]
556 fn observe_blocks_storage_writes() {
557 let config = observe_privacy_config();
558 assert!(!config.is_tool_enabled("set_storage"));
559 assert!(!config.is_tool_enabled("storage.set"));
560 assert!(!config.is_tool_enabled("delete_storage"));
561 assert!(!config.is_tool_enabled("storage.delete"));
562 }
563
564 #[test]
565 fn observe_blocks_recording() {
566 let config = observe_privacy_config();
567 assert!(!config.is_tool_enabled("recording"));
568 assert!(!config.is_tool_enabled("recording.start"));
569 assert!(!config.is_tool_enabled("recording.stop"));
570 }
571
572 #[test]
573 fn observe_blocks_dangerous_tools() {
574 let config = observe_privacy_config();
575 assert!(!config.is_tool_enabled("eval_js"));
576 assert!(!config.is_tool_enabled("screenshot"));
577 assert!(!config.is_tool_enabled("invoke_command"));
578 assert!(!config.is_tool_enabled("navigate"));
579 assert!(!config.is_tool_enabled("navigate.go_to"));
580 assert!(!config.is_tool_enabled("inject_css"));
581 assert!(!config.is_tool_enabled("css.inject"));
582 assert!(!config.is_tool_enabled("css.remove"));
583 assert!(!config.is_tool_enabled("window.manage"));
584 assert!(!config.is_tool_enabled("window.resize"));
585 assert!(!config.is_tool_enabled("window.move_to"));
586 assert!(!config.is_tool_enabled("window.set_title"));
587 }
588
589 #[test]
590 fn observe_allows_read_only_tools() {
591 let config = observe_privacy_config();
592 assert!(config.is_tool_enabled("dom_snapshot"));
593 assert!(config.is_tool_enabled("find_elements"));
594 assert!(config.is_tool_enabled("detect_ghost_commands"));
595 assert!(config.is_tool_enabled("check_ipc_integrity"));
596 assert!(config.is_tool_enabled("get_registry"));
597 assert!(config.is_tool_enabled("get_memory_stats"));
598 assert!(config.is_tool_enabled("get_plugin_info"));
599 assert!(config.is_tool_enabled("get_diagnostics"));
600 assert!(config.is_tool_enabled("resolve_command"));
601 assert!(config.is_tool_enabled("wait_for"));
602 assert!(config.is_tool_enabled("window")); }
604
605 #[test]
606 fn observe_blocks_eval_dependent_tools() {
607 let config = observe_privacy_config();
608 assert!(!config.is_tool_enabled("verify_state"));
611 assert!(!config.is_tool_enabled("assert_semantic"));
612 }
613
614 #[test]
615 fn observe_allows_read_actions_on_compound_tools() {
616 let config = observe_privacy_config();
617 assert!(config.is_tool_enabled("window.get_state"));
619 assert!(config.is_tool_enabled("window.list"));
620 assert!(config.is_tool_enabled("logs"));
622 assert!(config.is_tool_enabled("logs.console"));
623 assert!(config.is_tool_enabled("logs.network"));
624 assert!(config.is_tool_enabled("logs.ipc"));
625 assert!(config.is_tool_enabled("logs.navigation"));
626 assert!(config.is_tool_enabled("logs.dialogs"));
627 assert!(config.is_tool_enabled("logs.events"));
628 assert!(config.is_tool_enabled("logs.slow_ipc"));
629 assert!(config.is_tool_enabled("inspect"));
631 assert!(config.is_tool_enabled("inspect.styles"));
632 assert!(config.is_tool_enabled("inspect.bounds"));
633 assert!(config.is_tool_enabled("inspect.audit_a11y"));
634 assert!(config.is_tool_enabled("inspect.performance"));
635 assert!(!config.is_tool_enabled("inspect.highlight"));
637 assert!(!config.is_tool_enabled("inspect.clear_highlights"));
638 assert!(!config.is_tool_enabled("logs.clear"));
639 }
640
641 #[test]
642 fn observe_allows_app_state_probe_reads() {
643 assert!(observe_privacy_config().is_tool_enabled("app_state"));
645 }
646
647 #[test]
648 fn test_profile_allows_log_clearing_and_highlight() {
649 let config = test_privacy_config();
652 assert!(config.is_tool_enabled("logs.clear"));
653 assert!(config.is_tool_enabled("inspect.highlight"));
654 assert!(config.is_tool_enabled("inspect.clear_highlights"));
655 assert!(config.is_tool_enabled("app_state"));
656 }
657
658 #[test]
659 fn observe_blocks_unlisted_tools() {
660 let config = observe_privacy_config();
661 assert!(!config.is_tool_enabled("navigate.get_history"));
663 assert!(!config.is_tool_enabled("navigate.get_dialog_log"));
664 assert!(!config.is_tool_enabled("css.get_styles"));
665 assert!(!config.is_tool_enabled("css.get_computed"));
666 assert!(!config.is_tool_enabled("storage.get"));
667 assert!(!config.is_tool_enabled("storage.get_cookies"));
668 assert!(!config.is_tool_enabled("navigate.go_back"));
669 }
670
671 #[test]
672 fn observe_enables_redaction() {
673 let config = observe_privacy_config();
674 assert!(config.redaction_enabled);
675 }
676
677 #[test]
680 fn disabled_tools_override_full_control() {
681 let mut disabled = HashSet::new();
682 disabled.insert("eval_js".to_string());
683 let config = PrivacyConfig {
684 profile: PrivacyProfile::FullControl,
685 disabled_tools: disabled,
686 ..Default::default()
687 };
688 assert!(!config.is_tool_enabled("eval_js"));
689 assert!(config.is_tool_enabled("screenshot"));
690 }
691
692 #[test]
693 fn disabled_tools_stack_with_profile() {
694 let mut disabled = HashSet::new();
695 disabled.insert("dom_snapshot".to_string());
696 let mut config = test_privacy_config();
697 config.disabled_tools = disabled;
698 assert!(!config.is_tool_enabled("dom_snapshot"));
700 assert!(!config.is_tool_enabled("eval_js"));
702 }
703
704 #[test]
707 fn invoke_allowed_in_full_control() {
708 let config = PrivacyConfig::default();
709 assert!(config.is_invoke_allowed("any_command"));
710 }
711
712 #[test]
713 fn invoke_blocked_in_observe() {
714 let config = observe_privacy_config();
715 assert!(!config.is_invoke_allowed("any_command"));
716 }
717
718 #[test]
719 fn invoke_allowed_in_test_with_allowlist() {
720 let mut allow = HashSet::new();
721 allow.insert("greet".to_string());
722 let mut config = test_privacy_config();
723 config.command_allowlist = Some(allow);
724 assert!(config.is_invoke_allowed("greet"));
725 assert!(!config.is_invoke_allowed("delete_user"));
726 }
727
728 #[test]
729 fn invoke_blocked_in_test_without_allowlist() {
730 let config = test_privacy_config();
731 assert!(!config.is_invoke_allowed("greet"));
732 }
733
734 #[test]
737 fn strict_privacy_is_observe_profile() {
738 let config = strict_privacy_config();
739 assert_eq!(config.profile, PrivacyProfile::Observe);
740 assert!(config.redaction_enabled);
741 }
742
743 #[test]
746 fn strict_mode_disables_dangerous_tools() {
747 let config = strict_privacy_config();
748 assert!(!config.is_tool_enabled("eval_js"));
749 assert!(!config.is_tool_enabled("screenshot"));
750 assert!(!config.is_tool_enabled("inject_css"));
751 assert!(!config.is_tool_enabled("navigate"));
752 assert!(!config.is_tool_enabled("invoke_command"));
753 assert!(config.is_tool_enabled("dom_snapshot"));
754 assert!(config.is_tool_enabled("get_memory_stats"));
755 assert!(config.redaction_enabled);
756 }
757
758 #[test]
759 fn strict_mode_blocks_window_mutations() {
760 let config = strict_privacy_config();
761 assert!(!config.is_tool_enabled("window.manage"));
762 assert!(!config.is_tool_enabled("window.resize"));
763 assert!(!config.is_tool_enabled("window.move_to"));
764 assert!(!config.is_tool_enabled("window.set_title"));
765 assert!(config.is_tool_enabled("window"));
766 }
767
768 #[test]
769 fn default_allows_all_actions() {
770 let config = PrivacyConfig::default();
771 assert!(config.is_tool_enabled("invoke_command"));
772 assert!(config.is_tool_enabled("window.manage"));
773 assert!(config.is_tool_enabled("window.resize"));
774 assert!(config.is_tool_enabled("window.move_to"));
775 assert!(config.is_tool_enabled("window.set_title"));
776 }
777
778 #[test]
781 fn redaction_when_enabled() {
782 let config = PrivacyConfig {
783 redaction_enabled: true,
784 ..Default::default()
785 };
786 let output = config.redact_output("key is sk-abc123def456ghi789jkl012mno");
787 assert!(output.contains("[REDACTED]"));
788 }
789
790 #[test]
791 fn no_redaction_when_disabled() {
792 let config = PrivacyConfig::default();
793 let input = "key is sk-abc123def456ghi789jkl012mno";
794 assert_eq!(config.redact_output(input), input);
795 }
796
797 #[test]
800 fn profile_display() {
801 assert_eq!(PrivacyProfile::Observe.to_string(), "observe");
802 assert_eq!(PrivacyProfile::Test.to_string(), "test");
803 assert_eq!(PrivacyProfile::FullControl.to_string(), "full_control");
804 }
805}