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 "eval_js"
127 | "screenshot"
128 | "invoke_command"
129 | "navigate"
130 | "navigate.go_to"
131 | "set_dialog_response"
132 | "navigate.set_dialog_response"
133 | "inject_css"
134 | "css.inject"
135 | "css.remove"
136 | "window.manage"
137 | "window.resize"
138 | "window.move_to"
139 | "window.set_title"
140 ),
141 PrivacyProfile::Observe => matches!(
142 tool_or_action,
143 "dom_snapshot"
144 | "find_elements"
145 | "get_registry"
146 | "get_memory_stats"
147 | "get_plugin_info"
148 | "detect_ghost_commands"
149 | "check_ipc_integrity"
150 | "resolve_command"
151 | "logs"
152 | "logs.console"
153 | "logs.network"
154 | "logs.ipc"
155 | "logs.navigation"
156 | "logs.dialogs"
157 | "logs.events"
158 | "logs.slow_ipc"
159 | "inspect"
160 | "inspect.styles"
161 | "inspect.bounds"
162 | "inspect.highlight"
163 | "inspect.clear_highlights"
164 | "inspect.audit_a11y"
165 | "inspect.performance"
166 | "list_windows"
167 | "window"
168 | "window.get_state"
169 | "window.list"
170 | "get_window_state"
171 | "wait_for"
172 ),
173 }
174}
175
176#[must_use]
178pub fn observe_privacy_config() -> PrivacyConfig {
179 PrivacyConfig {
180 profile: PrivacyProfile::Observe,
181 command_allowlist: None,
182 command_blocklist: HashSet::new(),
183 disabled_tools: HashSet::new(),
184 redactor: Redactor::default(),
185 redaction_enabled: true,
186 }
187}
188
189#[must_use]
191pub fn test_privacy_config() -> PrivacyConfig {
192 PrivacyConfig {
193 profile: PrivacyProfile::Test,
194 command_allowlist: None,
195 command_blocklist: HashSet::new(),
196 disabled_tools: HashSet::new(),
197 redactor: Redactor::default(),
198 redaction_enabled: true,
199 }
200}
201
202#[must_use]
207pub fn strict_privacy_config() -> PrivacyConfig {
208 observe_privacy_config()
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
218 fn default_allows_all_commands() {
219 let config = PrivacyConfig::default();
220 assert!(config.is_command_allowed("get_settings"));
221 assert!(config.is_command_allowed("anything"));
222 }
223
224 #[test]
225 fn blocklist_blocks() {
226 let mut config = PrivacyConfig::default();
227 config.command_blocklist.insert("save_api_key".to_string());
228 assert!(!config.is_command_allowed("save_api_key"));
229 assert!(config.is_command_allowed("get_settings"));
230 }
231
232 #[test]
233 fn allowlist_restricts() {
234 let mut allow = HashSet::new();
235 allow.insert("get_settings".to_string());
236 allow.insert("get_monitoring_status".to_string());
237 let config = PrivacyConfig {
238 command_allowlist: Some(allow),
239 ..Default::default()
240 };
241 assert!(config.is_command_allowed("get_settings"));
242 assert!(!config.is_command_allowed("save_api_key"));
243 }
244
245 #[test]
246 fn blocklist_wins_over_allowlist() {
247 let mut allow = HashSet::new();
248 allow.insert("save_api_key".to_string());
249 let mut block = HashSet::new();
250 block.insert("save_api_key".to_string());
251 let config = PrivacyConfig {
252 command_allowlist: Some(allow),
253 command_blocklist: block,
254 ..Default::default()
255 };
256 assert!(!config.is_command_allowed("save_api_key"));
257 }
258
259 #[test]
262 fn full_control_allows_everything() {
263 let config = PrivacyConfig::default();
264 assert_eq!(config.profile, PrivacyProfile::FullControl);
265 assert!(config.is_tool_enabled("eval_js"));
266 assert!(config.is_tool_enabled("screenshot"));
267 assert!(config.is_tool_enabled("invoke_command"));
268 assert!(config.is_tool_enabled("interact"));
269 assert!(config.is_tool_enabled("interact.click"));
270 assert!(config.is_tool_enabled("input.fill"));
271 assert!(config.is_tool_enabled("window.manage"));
272 assert!(config.is_tool_enabled("navigate"));
273 assert!(config.is_tool_enabled("navigate.go_to"));
274 assert!(config.is_tool_enabled("css.inject"));
275 assert!(config.is_tool_enabled("recording"));
276 assert!(config.is_tool_enabled("storage.set"));
277 assert!(config.is_tool_enabled("set_dialog_response"));
278 }
279
280 #[test]
283 fn test_profile_allows_interactions() {
284 let config = test_privacy_config();
285 assert!(config.is_tool_enabled("interact"));
286 assert!(config.is_tool_enabled("interact.click"));
287 assert!(config.is_tool_enabled("interact.double_click"));
288 assert!(config.is_tool_enabled("interact.hover"));
289 assert!(config.is_tool_enabled("interact.focus"));
290 assert!(config.is_tool_enabled("interact.scroll_into_view"));
291 assert!(config.is_tool_enabled("interact.select_option"));
292 }
293
294 #[test]
295 fn test_profile_allows_input() {
296 let config = test_privacy_config();
297 assert!(config.is_tool_enabled("fill"));
298 assert!(config.is_tool_enabled("input.fill"));
299 assert!(config.is_tool_enabled("type_text"));
300 assert!(config.is_tool_enabled("input.type_text"));
301 assert!(config.is_tool_enabled("input.press_key"));
302 }
303
304 #[test]
305 fn test_profile_allows_storage_writes() {
306 let config = test_privacy_config();
307 assert!(config.is_tool_enabled("set_storage"));
308 assert!(config.is_tool_enabled("storage.set"));
309 assert!(config.is_tool_enabled("delete_storage"));
310 assert!(config.is_tool_enabled("storage.delete"));
311 }
312
313 #[test]
314 fn test_profile_allows_recording() {
315 let config = test_privacy_config();
316 assert!(config.is_tool_enabled("recording"));
317 assert!(config.is_tool_enabled("recording.start"));
318 assert!(config.is_tool_enabled("recording.stop"));
319 }
320
321 #[test]
322 fn test_profile_blocks_eval_and_screenshot() {
323 let config = test_privacy_config();
324 assert!(!config.is_tool_enabled("eval_js"));
325 assert!(!config.is_tool_enabled("screenshot"));
326 }
327
328 #[test]
329 fn test_profile_blocks_navigation() {
330 let config = test_privacy_config();
331 assert!(!config.is_tool_enabled("navigate"));
332 assert!(!config.is_tool_enabled("navigate.go_to"));
333 assert!(!config.is_tool_enabled("set_dialog_response"));
334 assert!(!config.is_tool_enabled("navigate.set_dialog_response"));
335 }
336
337 #[test]
338 fn test_profile_blocks_window_mutations() {
339 let config = test_privacy_config();
340 assert!(!config.is_tool_enabled("window.manage"));
341 assert!(!config.is_tool_enabled("window.resize"));
342 assert!(!config.is_tool_enabled("window.move_to"));
343 assert!(!config.is_tool_enabled("window.set_title"));
344 }
345
346 #[test]
347 fn test_profile_blocks_css_injection() {
348 let config = test_privacy_config();
349 assert!(!config.is_tool_enabled("inject_css"));
350 assert!(!config.is_tool_enabled("css.inject"));
351 assert!(!config.is_tool_enabled("css.remove"));
352 }
353
354 #[test]
355 fn test_profile_blocks_invoke_command() {
356 let config = test_privacy_config();
357 assert!(!config.is_tool_enabled("invoke_command"));
358 }
359
360 #[test]
361 fn test_profile_allows_read_only_tools() {
362 let config = test_privacy_config();
363 assert!(config.is_tool_enabled("dom_snapshot"));
364 assert!(config.is_tool_enabled("find_elements"));
365 assert!(config.is_tool_enabled("verify_state"));
366 assert!(config.is_tool_enabled("detect_ghost_commands"));
367 assert!(config.is_tool_enabled("check_ipc_integrity"));
368 assert!(config.is_tool_enabled("get_registry"));
369 assert!(config.is_tool_enabled("get_memory_stats"));
370 assert!(config.is_tool_enabled("get_plugin_info"));
371 assert!(config.is_tool_enabled("resolve_command"));
372 assert!(config.is_tool_enabled("wait_for"));
373 assert!(config.is_tool_enabled("assert_semantic"));
374 }
375
376 #[test]
379 fn observe_blocks_all_interactions() {
380 let config = observe_privacy_config();
381 assert!(!config.is_tool_enabled("interact"));
382 assert!(!config.is_tool_enabled("interact.click"));
383 assert!(!config.is_tool_enabled("interact.double_click"));
384 assert!(!config.is_tool_enabled("interact.hover"));
385 assert!(!config.is_tool_enabled("interact.focus"));
386 assert!(!config.is_tool_enabled("interact.scroll_into_view"));
387 assert!(!config.is_tool_enabled("interact.select_option"));
388 }
389
390 #[test]
391 fn observe_blocks_all_input() {
392 let config = observe_privacy_config();
393 assert!(!config.is_tool_enabled("fill"));
394 assert!(!config.is_tool_enabled("input.fill"));
395 assert!(!config.is_tool_enabled("type_text"));
396 assert!(!config.is_tool_enabled("input.type_text"));
397 assert!(!config.is_tool_enabled("input.press_key"));
398 }
399
400 #[test]
401 fn observe_blocks_storage_writes() {
402 let config = observe_privacy_config();
403 assert!(!config.is_tool_enabled("set_storage"));
404 assert!(!config.is_tool_enabled("storage.set"));
405 assert!(!config.is_tool_enabled("delete_storage"));
406 assert!(!config.is_tool_enabled("storage.delete"));
407 }
408
409 #[test]
410 fn observe_blocks_recording() {
411 let config = observe_privacy_config();
412 assert!(!config.is_tool_enabled("recording"));
413 assert!(!config.is_tool_enabled("recording.start"));
414 assert!(!config.is_tool_enabled("recording.stop"));
415 }
416
417 #[test]
418 fn observe_blocks_dangerous_tools() {
419 let config = observe_privacy_config();
420 assert!(!config.is_tool_enabled("eval_js"));
421 assert!(!config.is_tool_enabled("screenshot"));
422 assert!(!config.is_tool_enabled("invoke_command"));
423 assert!(!config.is_tool_enabled("navigate"));
424 assert!(!config.is_tool_enabled("navigate.go_to"));
425 assert!(!config.is_tool_enabled("inject_css"));
426 assert!(!config.is_tool_enabled("css.inject"));
427 assert!(!config.is_tool_enabled("css.remove"));
428 assert!(!config.is_tool_enabled("window.manage"));
429 assert!(!config.is_tool_enabled("window.resize"));
430 assert!(!config.is_tool_enabled("window.move_to"));
431 assert!(!config.is_tool_enabled("window.set_title"));
432 }
433
434 #[test]
435 fn observe_allows_read_only_tools() {
436 let config = observe_privacy_config();
437 assert!(config.is_tool_enabled("dom_snapshot"));
438 assert!(config.is_tool_enabled("find_elements"));
439 assert!(config.is_tool_enabled("detect_ghost_commands"));
440 assert!(config.is_tool_enabled("check_ipc_integrity"));
441 assert!(config.is_tool_enabled("get_registry"));
442 assert!(config.is_tool_enabled("get_memory_stats"));
443 assert!(config.is_tool_enabled("get_plugin_info"));
444 assert!(config.is_tool_enabled("resolve_command"));
445 assert!(config.is_tool_enabled("wait_for"));
446 assert!(config.is_tool_enabled("window")); }
448
449 #[test]
450 fn observe_blocks_eval_dependent_tools() {
451 let config = observe_privacy_config();
452 assert!(!config.is_tool_enabled("verify_state"));
455 assert!(!config.is_tool_enabled("assert_semantic"));
456 }
457
458 #[test]
459 fn observe_allows_read_actions_on_compound_tools() {
460 let config = observe_privacy_config();
461 assert!(config.is_tool_enabled("window.get_state"));
463 assert!(config.is_tool_enabled("window.list"));
464 assert!(config.is_tool_enabled("logs"));
466 assert!(config.is_tool_enabled("logs.console"));
467 assert!(config.is_tool_enabled("logs.network"));
468 assert!(config.is_tool_enabled("logs.ipc"));
469 assert!(config.is_tool_enabled("logs.navigation"));
470 assert!(config.is_tool_enabled("logs.dialogs"));
471 assert!(config.is_tool_enabled("logs.events"));
472 assert!(config.is_tool_enabled("logs.slow_ipc"));
473 assert!(config.is_tool_enabled("inspect"));
475 assert!(config.is_tool_enabled("inspect.styles"));
476 assert!(config.is_tool_enabled("inspect.bounds"));
477 assert!(config.is_tool_enabled("inspect.highlight"));
478 assert!(config.is_tool_enabled("inspect.clear_highlights"));
479 assert!(config.is_tool_enabled("inspect.audit_a11y"));
480 assert!(config.is_tool_enabled("inspect.performance"));
481 }
482
483 #[test]
484 fn observe_blocks_unlisted_tools() {
485 let config = observe_privacy_config();
486 assert!(!config.is_tool_enabled("navigate.get_history"));
488 assert!(!config.is_tool_enabled("navigate.get_dialog_log"));
489 assert!(!config.is_tool_enabled("css.get_styles"));
490 assert!(!config.is_tool_enabled("css.get_computed"));
491 assert!(!config.is_tool_enabled("storage.get"));
492 assert!(!config.is_tool_enabled("storage.get_cookies"));
493 assert!(!config.is_tool_enabled("navigate.go_back"));
494 }
495
496 #[test]
497 fn observe_enables_redaction() {
498 let config = observe_privacy_config();
499 assert!(config.redaction_enabled);
500 }
501
502 #[test]
505 fn disabled_tools_override_full_control() {
506 let mut disabled = HashSet::new();
507 disabled.insert("eval_js".to_string());
508 let config = PrivacyConfig {
509 profile: PrivacyProfile::FullControl,
510 disabled_tools: disabled,
511 ..Default::default()
512 };
513 assert!(!config.is_tool_enabled("eval_js"));
514 assert!(config.is_tool_enabled("screenshot"));
515 }
516
517 #[test]
518 fn disabled_tools_stack_with_profile() {
519 let mut disabled = HashSet::new();
520 disabled.insert("dom_snapshot".to_string());
521 let mut config = test_privacy_config();
522 config.disabled_tools = disabled;
523 assert!(!config.is_tool_enabled("dom_snapshot"));
525 assert!(!config.is_tool_enabled("eval_js"));
527 }
528
529 #[test]
532 fn invoke_allowed_in_full_control() {
533 let config = PrivacyConfig::default();
534 assert!(config.is_invoke_allowed("any_command"));
535 }
536
537 #[test]
538 fn invoke_blocked_in_observe() {
539 let config = observe_privacy_config();
540 assert!(!config.is_invoke_allowed("any_command"));
541 }
542
543 #[test]
544 fn invoke_allowed_in_test_with_allowlist() {
545 let mut allow = HashSet::new();
546 allow.insert("greet".to_string());
547 let mut config = test_privacy_config();
548 config.command_allowlist = Some(allow);
549 assert!(config.is_invoke_allowed("greet"));
550 assert!(!config.is_invoke_allowed("delete_user"));
551 }
552
553 #[test]
554 fn invoke_blocked_in_test_without_allowlist() {
555 let config = test_privacy_config();
556 assert!(!config.is_invoke_allowed("greet"));
557 }
558
559 #[test]
562 fn strict_privacy_is_observe_profile() {
563 let config = strict_privacy_config();
564 assert_eq!(config.profile, PrivacyProfile::Observe);
565 assert!(config.redaction_enabled);
566 }
567
568 #[test]
571 fn strict_mode_disables_dangerous_tools() {
572 let config = strict_privacy_config();
573 assert!(!config.is_tool_enabled("eval_js"));
574 assert!(!config.is_tool_enabled("screenshot"));
575 assert!(!config.is_tool_enabled("inject_css"));
576 assert!(!config.is_tool_enabled("navigate"));
577 assert!(!config.is_tool_enabled("invoke_command"));
578 assert!(config.is_tool_enabled("dom_snapshot"));
579 assert!(config.is_tool_enabled("get_memory_stats"));
580 assert!(config.redaction_enabled);
581 }
582
583 #[test]
584 fn strict_mode_blocks_window_mutations() {
585 let config = strict_privacy_config();
586 assert!(!config.is_tool_enabled("window.manage"));
587 assert!(!config.is_tool_enabled("window.resize"));
588 assert!(!config.is_tool_enabled("window.move_to"));
589 assert!(!config.is_tool_enabled("window.set_title"));
590 assert!(config.is_tool_enabled("window"));
591 }
592
593 #[test]
594 fn default_allows_all_actions() {
595 let config = PrivacyConfig::default();
596 assert!(config.is_tool_enabled("invoke_command"));
597 assert!(config.is_tool_enabled("window.manage"));
598 assert!(config.is_tool_enabled("window.resize"));
599 assert!(config.is_tool_enabled("window.move_to"));
600 assert!(config.is_tool_enabled("window.set_title"));
601 }
602
603 #[test]
606 fn redaction_when_enabled() {
607 let config = PrivacyConfig {
608 redaction_enabled: true,
609 ..Default::default()
610 };
611 let output = config.redact_output("key is sk-abc123def456ghi789jkl012mno");
612 assert!(output.contains("[REDACTED]"));
613 }
614
615 #[test]
616 fn no_redaction_when_disabled() {
617 let config = PrivacyConfig::default();
618 let input = "key is sk-abc123def456ghi789jkl012mno";
619 assert_eq!(config.redact_output(input), input);
620 }
621
622 #[test]
625 fn profile_display() {
626 assert_eq!(PrivacyProfile::Observe.to_string(), "observe");
627 assert_eq!(PrivacyProfile::Test.to_string(), "test");
628 assert_eq!(PrivacyProfile::FullControl.to_string(), "full_control");
629 }
630}