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 redactor: Redactor,
57 pub redaction_enabled: bool,
59}
60
61impl PrivacyConfig {
62 #[must_use]
64 pub fn is_command_allowed(&self, command: &str) -> bool {
65 if self.command_blocklist.contains(command) {
66 return false;
67 }
68 match &self.command_allowlist {
69 Some(allow) => allow.contains(command),
70 None => true,
71 }
72 }
73
74 #[must_use]
77 pub fn is_tool_enabled(&self, tool_or_action: &str) -> bool {
78 if self.disabled_tools.contains(tool_or_action) {
79 return false;
80 }
81 is_allowed_by_profile(self.profile, tool_or_action)
82 }
83
84 #[must_use]
89 pub fn is_invoke_allowed(&self, command: &str) -> bool {
90 if self.disabled_tools.contains("invoke_command") {
91 return false;
92 }
93 match self.profile {
94 PrivacyProfile::FullControl => true,
95 PrivacyProfile::Test => self
96 .command_allowlist
97 .as_ref()
98 .is_some_and(|al| al.contains(command)),
99 PrivacyProfile::Observe => false,
100 }
101 }
102
103 #[must_use]
105 pub fn redact_output(&self, output: &str) -> String {
106 if self.redaction_enabled {
107 self.redactor.redact(output)
108 } else {
109 output.to_string()
110 }
111 }
112}
113
114#[must_use]
121fn is_allowed_by_profile(profile: PrivacyProfile, tool_or_action: &str) -> bool {
122 match profile {
123 PrivacyProfile::FullControl => true,
124 PrivacyProfile::Test => matches!(
125 tool_or_action,
126 "dom_snapshot"
128 | "find_elements"
129 | "get_registry"
130 | "get_memory_stats"
131 | "get_plugin_info"
132 | "get_diagnostics"
133 | "detect_ghost_commands"
134 | "check_ipc_integrity"
135 | "resolve_command"
136 | "wait_for"
137 | "verify_state"
139 | "assert_semantic"
140 | "interact"
142 | "interact.click"
143 | "interact.double_click"
144 | "interact.hover"
145 | "interact.focus"
146 | "interact.scroll_into_view"
147 | "interact.select_option"
148 | "fill"
150 | "input"
151 | "input.fill"
152 | "type_text"
153 | "input.type_text"
154 | "input.press_key"
155 | "storage"
157 | "set_storage"
158 | "storage.set"
159 | "delete_storage"
160 | "storage.delete"
161 | "storage.get"
162 | "storage.get_cookies"
163 | "get_storage"
164 | "get_cookies"
165 | "recording"
167 | "recording.start"
168 | "recording.stop"
169 | "recording.checkpoint"
170 | "recording.list_checkpoints"
171 | "recording.get_events"
172 | "recording.events_between"
173 | "recording.get_replay"
174 | "recording.export"
175 | "recording.import"
176 | "logs"
178 | "logs.console"
179 | "logs.network"
180 | "logs.ipc"
181 | "logs.navigation"
182 | "logs.dialogs"
183 | "logs.events"
184 | "logs.slow_ipc"
185 | "inspect"
187 | "inspect.styles"
188 | "inspect.bounds"
189 | "inspect.highlight"
190 | "inspect.clear_highlights"
191 | "inspect.audit_a11y"
192 | "inspect.performance"
193 | "list_windows"
195 | "window"
196 | "window.get_state"
197 | "window.list"
198 | "get_window_state"
199 | "navigate.go_back"
201 | "navigate.get_history"
202 | "navigate.get_dialog_log"
203 ),
204 PrivacyProfile::Observe => matches!(
205 tool_or_action,
206 "dom_snapshot"
207 | "find_elements"
208 | "get_registry"
209 | "get_memory_stats"
210 | "get_plugin_info"
211 | "get_diagnostics"
212 | "detect_ghost_commands"
213 | "check_ipc_integrity"
214 | "resolve_command"
215 | "logs"
216 | "logs.console"
217 | "logs.network"
218 | "logs.ipc"
219 | "logs.navigation"
220 | "logs.dialogs"
221 | "logs.events"
222 | "logs.slow_ipc"
223 | "inspect"
224 | "inspect.styles"
225 | "inspect.bounds"
226 | "inspect.highlight"
227 | "inspect.clear_highlights"
228 | "inspect.audit_a11y"
229 | "inspect.performance"
230 | "list_windows"
231 | "window"
232 | "window.get_state"
233 | "window.list"
234 | "get_window_state"
235 | "wait_for"
236 ),
237 }
238}
239
240#[must_use]
242pub fn observe_privacy_config() -> PrivacyConfig {
243 PrivacyConfig {
244 profile: PrivacyProfile::Observe,
245 command_allowlist: None,
246 command_blocklist: HashSet::new(),
247 disabled_tools: HashSet::new(),
248 redactor: Redactor::default(),
249 redaction_enabled: true,
250 }
251}
252
253#[must_use]
255pub fn test_privacy_config() -> PrivacyConfig {
256 PrivacyConfig {
257 profile: PrivacyProfile::Test,
258 command_allowlist: None,
259 command_blocklist: HashSet::new(),
260 disabled_tools: HashSet::new(),
261 redactor: Redactor::default(),
262 redaction_enabled: true,
263 }
264}
265
266#[must_use]
271pub fn strict_privacy_config() -> PrivacyConfig {
272 observe_privacy_config()
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
282 fn default_allows_all_commands() {
283 let config = PrivacyConfig::default();
284 assert!(config.is_command_allowed("get_settings"));
285 assert!(config.is_command_allowed("anything"));
286 }
287
288 #[test]
289 fn blocklist_blocks() {
290 let mut config = PrivacyConfig::default();
291 config.command_blocklist.insert("save_api_key".to_string());
292 assert!(!config.is_command_allowed("save_api_key"));
293 assert!(config.is_command_allowed("get_settings"));
294 }
295
296 #[test]
297 fn allowlist_restricts() {
298 let mut allow = HashSet::new();
299 allow.insert("get_settings".to_string());
300 allow.insert("get_monitoring_status".to_string());
301 let config = PrivacyConfig {
302 command_allowlist: Some(allow),
303 ..Default::default()
304 };
305 assert!(config.is_command_allowed("get_settings"));
306 assert!(!config.is_command_allowed("save_api_key"));
307 }
308
309 #[test]
310 fn blocklist_wins_over_allowlist() {
311 let mut allow = HashSet::new();
312 allow.insert("save_api_key".to_string());
313 let mut block = HashSet::new();
314 block.insert("save_api_key".to_string());
315 let config = PrivacyConfig {
316 command_allowlist: Some(allow),
317 command_blocklist: block,
318 ..Default::default()
319 };
320 assert!(!config.is_command_allowed("save_api_key"));
321 }
322
323 #[test]
326 fn full_control_allows_everything() {
327 let config = PrivacyConfig::default();
328 assert_eq!(config.profile, PrivacyProfile::FullControl);
329 assert!(config.is_tool_enabled("eval_js"));
330 assert!(config.is_tool_enabled("screenshot"));
331 assert!(config.is_tool_enabled("invoke_command"));
332 assert!(config.is_tool_enabled("interact"));
333 assert!(config.is_tool_enabled("interact.click"));
334 assert!(config.is_tool_enabled("input.fill"));
335 assert!(config.is_tool_enabled("window.manage"));
336 assert!(config.is_tool_enabled("navigate"));
337 assert!(config.is_tool_enabled("navigate.go_to"));
338 assert!(config.is_tool_enabled("css.inject"));
339 assert!(config.is_tool_enabled("recording"));
340 assert!(config.is_tool_enabled("storage.set"));
341 assert!(config.is_tool_enabled("set_dialog_response"));
342 }
343
344 #[test]
347 fn test_profile_allows_interactions() {
348 let config = test_privacy_config();
349 assert!(config.is_tool_enabled("interact"));
350 assert!(config.is_tool_enabled("interact.click"));
351 assert!(config.is_tool_enabled("interact.double_click"));
352 assert!(config.is_tool_enabled("interact.hover"));
353 assert!(config.is_tool_enabled("interact.focus"));
354 assert!(config.is_tool_enabled("interact.scroll_into_view"));
355 assert!(config.is_tool_enabled("interact.select_option"));
356 }
357
358 #[test]
359 fn test_profile_allows_input() {
360 let config = test_privacy_config();
361 assert!(config.is_tool_enabled("fill"));
362 assert!(config.is_tool_enabled("input.fill"));
363 assert!(config.is_tool_enabled("type_text"));
364 assert!(config.is_tool_enabled("input.type_text"));
365 assert!(config.is_tool_enabled("input.press_key"));
366 }
367
368 #[test]
369 fn test_profile_allows_storage_writes() {
370 let config = test_privacy_config();
371 assert!(config.is_tool_enabled("set_storage"));
372 assert!(config.is_tool_enabled("storage.set"));
373 assert!(config.is_tool_enabled("delete_storage"));
374 assert!(config.is_tool_enabled("storage.delete"));
375 }
376
377 #[test]
378 fn test_profile_allows_recording() {
379 let config = test_privacy_config();
380 assert!(config.is_tool_enabled("recording"));
381 assert!(config.is_tool_enabled("recording.start"));
382 assert!(config.is_tool_enabled("recording.stop"));
383 }
384
385 #[test]
386 fn test_profile_blocks_eval_and_screenshot() {
387 let config = test_privacy_config();
388 assert!(!config.is_tool_enabled("eval_js"));
389 assert!(!config.is_tool_enabled("screenshot"));
390 }
391
392 #[test]
393 fn test_profile_blocks_navigation() {
394 let config = test_privacy_config();
395 assert!(!config.is_tool_enabled("navigate"));
396 assert!(!config.is_tool_enabled("navigate.go_to"));
397 assert!(!config.is_tool_enabled("set_dialog_response"));
398 assert!(!config.is_tool_enabled("navigate.set_dialog_response"));
399 }
400
401 #[test]
402 fn test_profile_blocks_window_mutations() {
403 let config = test_privacy_config();
404 assert!(!config.is_tool_enabled("window.manage"));
405 assert!(!config.is_tool_enabled("window.resize"));
406 assert!(!config.is_tool_enabled("window.move_to"));
407 assert!(!config.is_tool_enabled("window.set_title"));
408 }
409
410 #[test]
411 fn test_profile_blocks_css_injection() {
412 let config = test_privacy_config();
413 assert!(!config.is_tool_enabled("inject_css"));
414 assert!(!config.is_tool_enabled("css.inject"));
415 assert!(!config.is_tool_enabled("css.remove"));
416 }
417
418 #[test]
419 fn test_profile_blocks_invoke_command() {
420 let config = test_privacy_config();
421 assert!(!config.is_tool_enabled("invoke_command"));
422 }
423
424 #[test]
425 fn test_profile_allows_read_only_tools() {
426 let config = test_privacy_config();
427 assert!(config.is_tool_enabled("dom_snapshot"));
428 assert!(config.is_tool_enabled("find_elements"));
429 assert!(config.is_tool_enabled("verify_state"));
430 assert!(config.is_tool_enabled("detect_ghost_commands"));
431 assert!(config.is_tool_enabled("check_ipc_integrity"));
432 assert!(config.is_tool_enabled("get_registry"));
433 assert!(config.is_tool_enabled("get_memory_stats"));
434 assert!(config.is_tool_enabled("get_plugin_info"));
435 assert!(config.is_tool_enabled("resolve_command"));
436 assert!(config.is_tool_enabled("wait_for"));
437 assert!(config.is_tool_enabled("assert_semantic"));
438 }
439
440 #[test]
443 fn observe_blocks_all_interactions() {
444 let config = observe_privacy_config();
445 assert!(!config.is_tool_enabled("interact"));
446 assert!(!config.is_tool_enabled("interact.click"));
447 assert!(!config.is_tool_enabled("interact.double_click"));
448 assert!(!config.is_tool_enabled("interact.hover"));
449 assert!(!config.is_tool_enabled("interact.focus"));
450 assert!(!config.is_tool_enabled("interact.scroll_into_view"));
451 assert!(!config.is_tool_enabled("interact.select_option"));
452 }
453
454 #[test]
455 fn observe_blocks_all_input() {
456 let config = observe_privacy_config();
457 assert!(!config.is_tool_enabled("fill"));
458 assert!(!config.is_tool_enabled("input.fill"));
459 assert!(!config.is_tool_enabled("type_text"));
460 assert!(!config.is_tool_enabled("input.type_text"));
461 assert!(!config.is_tool_enabled("input.press_key"));
462 }
463
464 #[test]
465 fn observe_blocks_storage_writes() {
466 let config = observe_privacy_config();
467 assert!(!config.is_tool_enabled("set_storage"));
468 assert!(!config.is_tool_enabled("storage.set"));
469 assert!(!config.is_tool_enabled("delete_storage"));
470 assert!(!config.is_tool_enabled("storage.delete"));
471 }
472
473 #[test]
474 fn observe_blocks_recording() {
475 let config = observe_privacy_config();
476 assert!(!config.is_tool_enabled("recording"));
477 assert!(!config.is_tool_enabled("recording.start"));
478 assert!(!config.is_tool_enabled("recording.stop"));
479 }
480
481 #[test]
482 fn observe_blocks_dangerous_tools() {
483 let config = observe_privacy_config();
484 assert!(!config.is_tool_enabled("eval_js"));
485 assert!(!config.is_tool_enabled("screenshot"));
486 assert!(!config.is_tool_enabled("invoke_command"));
487 assert!(!config.is_tool_enabled("navigate"));
488 assert!(!config.is_tool_enabled("navigate.go_to"));
489 assert!(!config.is_tool_enabled("inject_css"));
490 assert!(!config.is_tool_enabled("css.inject"));
491 assert!(!config.is_tool_enabled("css.remove"));
492 assert!(!config.is_tool_enabled("window.manage"));
493 assert!(!config.is_tool_enabled("window.resize"));
494 assert!(!config.is_tool_enabled("window.move_to"));
495 assert!(!config.is_tool_enabled("window.set_title"));
496 }
497
498 #[test]
499 fn observe_allows_read_only_tools() {
500 let config = observe_privacy_config();
501 assert!(config.is_tool_enabled("dom_snapshot"));
502 assert!(config.is_tool_enabled("find_elements"));
503 assert!(config.is_tool_enabled("detect_ghost_commands"));
504 assert!(config.is_tool_enabled("check_ipc_integrity"));
505 assert!(config.is_tool_enabled("get_registry"));
506 assert!(config.is_tool_enabled("get_memory_stats"));
507 assert!(config.is_tool_enabled("get_plugin_info"));
508 assert!(config.is_tool_enabled("get_diagnostics"));
509 assert!(config.is_tool_enabled("resolve_command"));
510 assert!(config.is_tool_enabled("wait_for"));
511 assert!(config.is_tool_enabled("window")); }
513
514 #[test]
515 fn observe_blocks_eval_dependent_tools() {
516 let config = observe_privacy_config();
517 assert!(!config.is_tool_enabled("verify_state"));
520 assert!(!config.is_tool_enabled("assert_semantic"));
521 }
522
523 #[test]
524 fn observe_allows_read_actions_on_compound_tools() {
525 let config = observe_privacy_config();
526 assert!(config.is_tool_enabled("window.get_state"));
528 assert!(config.is_tool_enabled("window.list"));
529 assert!(config.is_tool_enabled("logs"));
531 assert!(config.is_tool_enabled("logs.console"));
532 assert!(config.is_tool_enabled("logs.network"));
533 assert!(config.is_tool_enabled("logs.ipc"));
534 assert!(config.is_tool_enabled("logs.navigation"));
535 assert!(config.is_tool_enabled("logs.dialogs"));
536 assert!(config.is_tool_enabled("logs.events"));
537 assert!(config.is_tool_enabled("logs.slow_ipc"));
538 assert!(config.is_tool_enabled("inspect"));
540 assert!(config.is_tool_enabled("inspect.styles"));
541 assert!(config.is_tool_enabled("inspect.bounds"));
542 assert!(config.is_tool_enabled("inspect.highlight"));
543 assert!(config.is_tool_enabled("inspect.clear_highlights"));
544 assert!(config.is_tool_enabled("inspect.audit_a11y"));
545 assert!(config.is_tool_enabled("inspect.performance"));
546 }
547
548 #[test]
549 fn observe_blocks_unlisted_tools() {
550 let config = observe_privacy_config();
551 assert!(!config.is_tool_enabled("navigate.get_history"));
553 assert!(!config.is_tool_enabled("navigate.get_dialog_log"));
554 assert!(!config.is_tool_enabled("css.get_styles"));
555 assert!(!config.is_tool_enabled("css.get_computed"));
556 assert!(!config.is_tool_enabled("storage.get"));
557 assert!(!config.is_tool_enabled("storage.get_cookies"));
558 assert!(!config.is_tool_enabled("navigate.go_back"));
559 }
560
561 #[test]
562 fn observe_enables_redaction() {
563 let config = observe_privacy_config();
564 assert!(config.redaction_enabled);
565 }
566
567 #[test]
570 fn disabled_tools_override_full_control() {
571 let mut disabled = HashSet::new();
572 disabled.insert("eval_js".to_string());
573 let config = PrivacyConfig {
574 profile: PrivacyProfile::FullControl,
575 disabled_tools: disabled,
576 ..Default::default()
577 };
578 assert!(!config.is_tool_enabled("eval_js"));
579 assert!(config.is_tool_enabled("screenshot"));
580 }
581
582 #[test]
583 fn disabled_tools_stack_with_profile() {
584 let mut disabled = HashSet::new();
585 disabled.insert("dom_snapshot".to_string());
586 let mut config = test_privacy_config();
587 config.disabled_tools = disabled;
588 assert!(!config.is_tool_enabled("dom_snapshot"));
590 assert!(!config.is_tool_enabled("eval_js"));
592 }
593
594 #[test]
597 fn invoke_allowed_in_full_control() {
598 let config = PrivacyConfig::default();
599 assert!(config.is_invoke_allowed("any_command"));
600 }
601
602 #[test]
603 fn invoke_blocked_in_observe() {
604 let config = observe_privacy_config();
605 assert!(!config.is_invoke_allowed("any_command"));
606 }
607
608 #[test]
609 fn invoke_allowed_in_test_with_allowlist() {
610 let mut allow = HashSet::new();
611 allow.insert("greet".to_string());
612 let mut config = test_privacy_config();
613 config.command_allowlist = Some(allow);
614 assert!(config.is_invoke_allowed("greet"));
615 assert!(!config.is_invoke_allowed("delete_user"));
616 }
617
618 #[test]
619 fn invoke_blocked_in_test_without_allowlist() {
620 let config = test_privacy_config();
621 assert!(!config.is_invoke_allowed("greet"));
622 }
623
624 #[test]
627 fn strict_privacy_is_observe_profile() {
628 let config = strict_privacy_config();
629 assert_eq!(config.profile, PrivacyProfile::Observe);
630 assert!(config.redaction_enabled);
631 }
632
633 #[test]
636 fn strict_mode_disables_dangerous_tools() {
637 let config = strict_privacy_config();
638 assert!(!config.is_tool_enabled("eval_js"));
639 assert!(!config.is_tool_enabled("screenshot"));
640 assert!(!config.is_tool_enabled("inject_css"));
641 assert!(!config.is_tool_enabled("navigate"));
642 assert!(!config.is_tool_enabled("invoke_command"));
643 assert!(config.is_tool_enabled("dom_snapshot"));
644 assert!(config.is_tool_enabled("get_memory_stats"));
645 assert!(config.redaction_enabled);
646 }
647
648 #[test]
649 fn strict_mode_blocks_window_mutations() {
650 let config = strict_privacy_config();
651 assert!(!config.is_tool_enabled("window.manage"));
652 assert!(!config.is_tool_enabled("window.resize"));
653 assert!(!config.is_tool_enabled("window.move_to"));
654 assert!(!config.is_tool_enabled("window.set_title"));
655 assert!(config.is_tool_enabled("window"));
656 }
657
658 #[test]
659 fn default_allows_all_actions() {
660 let config = PrivacyConfig::default();
661 assert!(config.is_tool_enabled("invoke_command"));
662 assert!(config.is_tool_enabled("window.manage"));
663 assert!(config.is_tool_enabled("window.resize"));
664 assert!(config.is_tool_enabled("window.move_to"));
665 assert!(config.is_tool_enabled("window.set_title"));
666 }
667
668 #[test]
671 fn redaction_when_enabled() {
672 let config = PrivacyConfig {
673 redaction_enabled: true,
674 ..Default::default()
675 };
676 let output = config.redact_output("key is sk-abc123def456ghi789jkl012mno");
677 assert!(output.contains("[REDACTED]"));
678 }
679
680 #[test]
681 fn no_redaction_when_disabled() {
682 let config = PrivacyConfig::default();
683 let input = "key is sk-abc123def456ghi789jkl012mno";
684 assert_eq!(config.redact_output(input), input);
685 }
686
687 #[test]
690 fn profile_display() {
691 assert_eq!(PrivacyProfile::Observe.to_string(), "observe");
692 assert_eq!(PrivacyProfile::Test.to_string(), "test");
693 assert_eq!(PrivacyProfile::FullControl.to_string(), "full_control");
694 }
695}