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