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 | "verify_state"
150 | "assert_semantic"
151 | "interact"
153 | "interact.click"
154 | "interact.double_click"
155 | "interact.hover"
156 | "interact.focus"
157 | "interact.scroll_into_view"
158 | "interact.select_option"
159 | "fill"
161 | "input"
162 | "input.fill"
163 | "type_text"
164 | "input.type_text"
165 | "input.press_key"
166 | "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"
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"
189 | "logs.console"
190 | "logs.network"
191 | "logs.ipc"
192 | "logs.navigation"
193 | "logs.dialogs"
194 | "logs.events"
195 | "logs.slow_ipc"
196 | "inspect"
198 | "inspect.styles"
199 | "inspect.bounds"
200 | "inspect.highlight"
201 | "inspect.clear_highlights"
202 | "inspect.audit_a11y"
203 | "inspect.performance"
204 | "list_windows"
206 | "window"
207 | "window.get_state"
208 | "window.list"
209 | "get_window_state"
210 | "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#[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#[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#[must_use]
284pub fn strict_privacy_config() -> PrivacyConfig {
285 observe_privacy_config()
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 #[test]
295 fn storage_key_blocklist_protects_keys() {
296 let mut config = PrivacyConfig::default();
297 assert!(config.is_storage_key_allowed("auth"));
299 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 #[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 #[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 #[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")); }
541
542 #[test]
543 fn observe_blocks_eval_dependent_tools() {
544 let config = observe_privacy_config();
545 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 assert!(config.is_tool_enabled("window.get_state"));
556 assert!(config.is_tool_enabled("window.list"));
557 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 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 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 #[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 assert!(!config.is_tool_enabled("dom_snapshot"));
618 assert!(!config.is_tool_enabled("eval_js"));
620 }
621
622 #[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 #[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 #[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 #[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 #[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}