1use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum PermissionDecision {
17 Allow,
19 Deny,
21 Ask,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
36pub struct PermissionRule {
37 pub rule: String,
39 #[serde(skip)]
41 tool_name: Option<String>,
42 #[serde(skip)]
44 arg_pattern: Option<String>,
45}
46
47impl PermissionRule {
48 pub fn new(rule: &str) -> Self {
50 let (tool_name, arg_pattern) = Self::parse_rule(rule);
51 Self {
52 rule: rule.to_string(),
53 tool_name,
54 arg_pattern,
55 }
56 }
57
58 fn parse_rule(rule: &str) -> (Option<String>, Option<String>) {
60 if let Some(paren_start) = rule.find('(') {
62 if rule.ends_with(')') {
63 let tool_name = rule[..paren_start].to_string();
64 let pattern = rule[paren_start + 1..rule.len() - 1].to_string();
65 return (Some(tool_name), Some(pattern));
66 }
67 }
68 (Some(rule.to_string()), None)
70 }
71
72 pub fn matches(&self, tool_name: &str, args: &serde_json::Value) -> bool {
74 let rule_tool = match &self.tool_name {
76 Some(t) => t,
77 None => return false,
78 };
79
80 if !self.matches_tool_name(rule_tool, tool_name) {
81 return false;
82 }
83
84 let pattern = match &self.arg_pattern {
86 Some(p) => p,
87 None => return true,
88 };
89
90 self.matches_args(pattern, tool_name, args)
92 }
93
94 fn matches_tool_name(&self, rule_tool: &str, actual_tool: &str) -> bool {
96 if rule_tool.starts_with("mcp__") && actual_tool.starts_with("mcp__") {
98 if actual_tool.starts_with(rule_tool) {
100 return true;
101 }
102 }
103 rule_tool.eq_ignore_ascii_case(actual_tool)
104 }
105
106 fn matches_args(&self, pattern: &str, tool_name: &str, args: &serde_json::Value) -> bool {
108 if pattern == "*" {
110 return true;
111 }
112
113 let arg_string = self.build_arg_string(tool_name, args);
115
116 self.glob_match(pattern, &arg_string)
118 }
119
120 fn build_arg_string(&self, tool_name: &str, args: &serde_json::Value) -> String {
122 match tool_name.to_lowercase().as_str() {
123 "bash" => {
124 args.get("command")
126 .and_then(|v| v.as_str())
127 .unwrap_or("")
128 .to_string()
129 }
130 "read" | "write" | "edit" => {
131 args.get("file_path")
133 .and_then(|v| v.as_str())
134 .unwrap_or("")
135 .to_string()
136 }
137 "glob" => {
138 args.get("pattern")
140 .and_then(|v| v.as_str())
141 .unwrap_or("")
142 .to_string()
143 }
144 "grep" => {
145 let pattern = args.get("pattern").and_then(|v| v.as_str()).unwrap_or("");
147 let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("");
148 format!("{} {}", pattern, path)
149 }
150 "ls" => {
151 args.get("path")
153 .and_then(|v| v.as_str())
154 .unwrap_or("")
155 .to_string()
156 }
157 _ => {
158 serde_json::to_string(args).unwrap_or_default()
160 }
161 }
162 }
163
164 fn glob_match(&self, pattern: &str, text: &str) -> bool {
171 if let Some(prefix) = pattern.strip_suffix(":*") {
173 return text.starts_with(prefix);
174 }
175
176 let regex_pattern = Self::glob_to_regex(pattern);
178 if let Ok(re) = regex::Regex::new(®ex_pattern) {
179 re.is_match(text)
180 } else {
181 text.starts_with(pattern)
183 }
184 }
185
186 fn glob_to_regex(pattern: &str) -> String {
188 let mut regex = String::from("^");
189 let chars: Vec<char> = pattern.chars().collect();
190 let mut i = 0;
191
192 while i < chars.len() {
193 let c = chars[i];
194 match c {
195 '*' => {
196 if i + 1 < chars.len() && chars[i + 1] == '*' {
198 if i + 2 < chars.len() && chars[i + 2] == '/' {
201 regex.push_str(".*");
202 i += 3;
203 } else {
204 regex.push_str(".*");
205 i += 2;
206 }
207 } else {
208 regex.push_str("[^/]*");
210 i += 1;
211 }
212 }
213 '?' => {
214 regex.push_str("[^/]");
216 i += 1;
217 }
218 '.' | '+' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\' => {
219 regex.push('\\');
221 regex.push(c);
222 i += 1;
223 }
224 _ => {
225 regex.push(c);
226 i += 1;
227 }
228 }
229 }
230
231 regex.push('$');
232 regex
233 }
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct PermissionPolicy {
245 #[serde(default)]
247 pub deny: Vec<PermissionRule>,
248
249 #[serde(default)]
251 pub allow: Vec<PermissionRule>,
252
253 #[serde(default)]
255 pub ask: Vec<PermissionRule>,
256
257 #[serde(default = "default_decision")]
259 pub default_decision: PermissionDecision,
260
261 #[serde(default = "default_enabled")]
263 pub enabled: bool,
264}
265
266fn default_decision() -> PermissionDecision {
267 PermissionDecision::Ask
268}
269
270fn default_enabled() -> bool {
271 true
272}
273
274impl Default for PermissionPolicy {
275 fn default() -> Self {
276 Self {
277 deny: Vec::new(),
278 allow: Vec::new(),
279 ask: Vec::new(),
280 default_decision: PermissionDecision::Ask,
281 enabled: true,
282 }
283 }
284}
285
286impl PermissionPolicy {
287 pub fn new() -> Self {
289 Self::default()
290 }
291
292 pub fn permissive() -> Self {
294 Self {
295 deny: Vec::new(),
296 allow: Vec::new(),
297 ask: Vec::new(),
298 default_decision: PermissionDecision::Allow,
299 enabled: true,
300 }
301 }
302
303 pub fn strict() -> Self {
305 Self {
306 deny: Vec::new(),
307 allow: Vec::new(),
308 ask: Vec::new(),
309 default_decision: PermissionDecision::Ask,
310 enabled: true,
311 }
312 }
313
314 pub fn deny(mut self, rule: &str) -> Self {
316 self.deny.push(PermissionRule::new(rule));
317 self
318 }
319
320 pub fn allow(mut self, rule: &str) -> Self {
322 self.allow.push(PermissionRule::new(rule));
323 self
324 }
325
326 pub fn ask(mut self, rule: &str) -> Self {
328 self.ask.push(PermissionRule::new(rule));
329 self
330 }
331
332 pub fn deny_all(mut self, rules: &[&str]) -> Self {
334 for rule in rules {
335 self.deny.push(PermissionRule::new(rule));
336 }
337 self
338 }
339
340 pub fn allow_all(mut self, rules: &[&str]) -> Self {
342 for rule in rules {
343 self.allow.push(PermissionRule::new(rule));
344 }
345 self
346 }
347
348 pub fn ask_all(mut self, rules: &[&str]) -> Self {
350 for rule in rules {
351 self.ask.push(PermissionRule::new(rule));
352 }
353 self
354 }
355
356 pub fn check(&self, tool_name: &str, args: &serde_json::Value) -> PermissionDecision {
364 if !self.enabled {
365 return PermissionDecision::Allow;
366 }
367
368 for rule in &self.deny {
370 if rule.matches(tool_name, args) {
371 return PermissionDecision::Deny;
372 }
373 }
374
375 for rule in &self.allow {
377 if rule.matches(tool_name, args) {
378 return PermissionDecision::Allow;
379 }
380 }
381
382 for rule in &self.ask {
384 if rule.matches(tool_name, args) {
385 return PermissionDecision::Ask;
386 }
387 }
388
389 self.default_decision
391 }
392
393 pub fn is_allowed(&self, tool_name: &str, args: &serde_json::Value) -> bool {
395 matches!(self.check(tool_name, args), PermissionDecision::Allow)
396 }
397
398 pub fn is_denied(&self, tool_name: &str, args: &serde_json::Value) -> bool {
400 matches!(self.check(tool_name, args), PermissionDecision::Deny)
401 }
402
403 pub fn requires_confirmation(&self, tool_name: &str, args: &serde_json::Value) -> bool {
405 matches!(self.check(tool_name, args), PermissionDecision::Ask)
406 }
407
408 pub fn get_matching_rules(&self, tool_name: &str, args: &serde_json::Value) -> MatchingRules {
410 let mut result = MatchingRules::default();
411
412 for rule in &self.deny {
413 if rule.matches(tool_name, args) {
414 result.deny.push(rule.rule.clone());
415 }
416 }
417
418 for rule in &self.allow {
419 if rule.matches(tool_name, args) {
420 result.allow.push(rule.rule.clone());
421 }
422 }
423
424 for rule in &self.ask {
425 if rule.matches(tool_name, args) {
426 result.ask.push(rule.rule.clone());
427 }
428 }
429
430 result
431 }
432}
433
434#[derive(Debug, Default, Clone)]
436pub struct MatchingRules {
437 pub deny: Vec<String>,
438 pub allow: Vec<String>,
439 pub ask: Vec<String>,
440}
441
442impl MatchingRules {
443 pub fn is_empty(&self) -> bool {
444 self.deny.is_empty() && self.allow.is_empty() && self.ask.is_empty()
445 }
446}
447
448#[derive(Debug)]
450pub struct PermissionManager {
451 global_policy: PermissionPolicy,
453 session_policies: HashMap<String, PermissionPolicy>,
455}
456
457impl Default for PermissionManager {
458 fn default() -> Self {
459 Self::new()
460 }
461}
462
463impl PermissionManager {
464 pub fn new() -> Self {
466 Self {
467 global_policy: PermissionPolicy::default(),
468 session_policies: HashMap::new(),
469 }
470 }
471
472 pub fn with_global_policy(policy: PermissionPolicy) -> Self {
474 Self {
475 global_policy: policy,
476 session_policies: HashMap::new(),
477 }
478 }
479
480 pub fn set_global_policy(&mut self, policy: PermissionPolicy) {
482 self.global_policy = policy;
483 }
484
485 pub fn global_policy(&self) -> &PermissionPolicy {
487 &self.global_policy
488 }
489
490 pub fn set_session_policy(&mut self, session_id: &str, policy: PermissionPolicy) {
492 self.session_policies.insert(session_id.to_string(), policy);
493 }
494
495 pub fn remove_session_policy(&mut self, session_id: &str) {
497 self.session_policies.remove(session_id);
498 }
499
500 pub fn get_effective_policy(&self, session_id: &str) -> &PermissionPolicy {
505 self.session_policies
506 .get(session_id)
507 .unwrap_or(&self.global_policy)
508 }
509
510 pub fn check(
512 &self,
513 session_id: &str,
514 tool_name: &str,
515 args: &serde_json::Value,
516 ) -> PermissionDecision {
517 let policy = self.get_effective_policy(session_id);
519
520 for rule in &policy.deny {
522 if rule.matches(tool_name, args) {
523 return PermissionDecision::Deny;
524 }
525 }
526
527 if !self.session_policies.contains_key(session_id) {
529 } else {
531 for rule in &self.global_policy.deny {
532 if rule.matches(tool_name, args) {
533 return PermissionDecision::Deny;
534 }
535 }
536 }
537
538 for rule in &policy.allow {
540 if rule.matches(tool_name, args) {
541 return PermissionDecision::Allow;
542 }
543 }
544
545 for rule in &policy.ask {
547 if rule.matches(tool_name, args) {
548 return PermissionDecision::Ask;
549 }
550 }
551
552 policy.default_decision
554 }
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560 use serde_json::json;
561
562 #[test]
567 fn test_rule_parse_simple() {
568 let rule = PermissionRule::new("Bash");
569 assert_eq!(rule.tool_name, Some("Bash".to_string()));
570 assert_eq!(rule.arg_pattern, None);
571 }
572
573 #[test]
574 fn test_rule_parse_with_pattern() {
575 let rule = PermissionRule::new("Bash(cargo:*)");
576 assert_eq!(rule.tool_name, Some("Bash".to_string()));
577 assert_eq!(rule.arg_pattern, Some("cargo:*".to_string()));
578 }
579
580 #[test]
581 fn test_rule_parse_wildcard() {
582 let rule = PermissionRule::new("Grep(*)");
583 assert_eq!(rule.tool_name, Some("Grep".to_string()));
584 assert_eq!(rule.arg_pattern, Some("*".to_string()));
585 }
586
587 #[test]
588 fn test_rule_match_tool_only() {
589 let rule = PermissionRule::new("Bash");
590 assert!(rule.matches("Bash", &json!({"command": "ls -la"})));
591 assert!(rule.matches("bash", &json!({"command": "echo hello"})));
592 assert!(!rule.matches("Read", &json!({})));
593 }
594
595 #[test]
596 fn test_rule_match_wildcard() {
597 let rule = PermissionRule::new("Grep(*)");
598 assert!(rule.matches("Grep", &json!({"pattern": "foo", "path": "/tmp"})));
599 assert!(rule.matches("grep", &json!({"pattern": "bar"})));
600 }
601
602 #[test]
603 fn test_rule_match_prefix_wildcard() {
604 let rule = PermissionRule::new("Bash(cargo:*)");
605 assert!(rule.matches("Bash", &json!({"command": "cargo build"})));
606 assert!(rule.matches("Bash", &json!({"command": "cargo test --lib"})));
607 assert!(rule.matches("Bash", &json!({"command": "cargo"})));
608 assert!(!rule.matches("Bash", &json!({"command": "npm install"})));
609 }
610
611 #[test]
612 fn test_rule_match_npm_commands() {
613 let rule = PermissionRule::new("Bash(npm run:*)");
614 assert!(rule.matches("Bash", &json!({"command": "npm run test"})));
615 assert!(rule.matches("Bash", &json!({"command": "npm run build"})));
616 assert!(!rule.matches("Bash", &json!({"command": "npm install"})));
617 }
618
619 #[test]
620 fn test_rule_match_file_path() {
621 let rule = PermissionRule::new("Read(src/*.rs)");
622 assert!(rule.matches("Read", &json!({"file_path": "src/main.rs"})));
623 assert!(rule.matches("Read", &json!({"file_path": "src/lib.rs"})));
624 assert!(!rule.matches("Read", &json!({"file_path": "src/foo/bar.rs"})));
625 }
626
627 #[test]
628 fn test_rule_match_recursive_glob() {
629 let rule = PermissionRule::new("Read(src/**/*.rs)");
630 assert!(rule.matches("Read", &json!({"file_path": "src/main.rs"})));
631 assert!(rule.matches("Read", &json!({"file_path": "src/foo/bar.rs"})));
632 assert!(rule.matches("Read", &json!({"file_path": "src/a/b/c.rs"})));
633 }
634
635 #[test]
636 fn test_rule_match_mcp_tool() {
637 let rule = PermissionRule::new("mcp__pencil");
638 assert!(rule.matches("mcp__pencil", &json!({})));
639 assert!(rule.matches("mcp__pencil__batch_design", &json!({})));
640 assert!(rule.matches("mcp__pencil__batch_get", &json!({})));
641 assert!(!rule.matches("mcp__other", &json!({})));
642 }
643
644 #[test]
645 fn test_rule_case_insensitive() {
646 let rule = PermissionRule::new("BASH(cargo:*)");
647 assert!(rule.matches("Bash", &json!({"command": "cargo build"})));
648 assert!(rule.matches("bash", &json!({"command": "cargo test"})));
649 assert!(rule.matches("BASH", &json!({"command": "cargo check"})));
650 }
651
652 #[test]
657 fn test_policy_default() {
658 let policy = PermissionPolicy::default();
659 assert!(policy.enabled);
660 assert_eq!(policy.default_decision, PermissionDecision::Ask);
661 assert!(policy.allow.is_empty());
662 assert!(policy.deny.is_empty());
663 assert!(policy.ask.is_empty());
664 }
665
666 #[test]
667 fn test_policy_permissive() {
668 let policy = PermissionPolicy::permissive();
669 assert_eq!(policy.default_decision, PermissionDecision::Allow);
670 }
671
672 #[test]
673 fn test_policy_strict() {
674 let policy = PermissionPolicy::strict();
675 assert_eq!(policy.default_decision, PermissionDecision::Ask);
676 }
677
678 #[test]
679 fn test_policy_builder() {
680 let policy = PermissionPolicy::new()
681 .allow("Bash(cargo:*)")
682 .allow("Grep(*)")
683 .deny("Bash(rm -rf:*)")
684 .ask("Write(*)");
685
686 assert_eq!(policy.allow.len(), 2);
687 assert_eq!(policy.deny.len(), 1);
688 assert_eq!(policy.ask.len(), 1);
689 }
690
691 #[test]
692 fn test_policy_check_allow() {
693 let policy = PermissionPolicy::new().allow("Bash(cargo:*)");
694
695 let decision = policy.check("Bash", &json!({"command": "cargo build"}));
696 assert_eq!(decision, PermissionDecision::Allow);
697 }
698
699 #[test]
700 fn test_policy_check_deny() {
701 let policy = PermissionPolicy::new().deny("Bash(rm -rf:*)");
702
703 let decision = policy.check("Bash", &json!({"command": "rm -rf /"}));
704 assert_eq!(decision, PermissionDecision::Deny);
705 }
706
707 #[test]
708 fn test_policy_check_ask() {
709 let policy = PermissionPolicy::new().ask("Write(*)");
710
711 let decision = policy.check("Write", &json!({"file_path": "/tmp/test.txt"}));
712 assert_eq!(decision, PermissionDecision::Ask);
713 }
714
715 #[test]
716 fn test_policy_check_default() {
717 let policy = PermissionPolicy::new();
718
719 let decision = policy.check("Unknown", &json!({}));
720 assert_eq!(decision, PermissionDecision::Ask);
721 }
722
723 #[test]
724 fn test_policy_deny_wins_over_allow() {
725 let policy = PermissionPolicy::new().allow("Bash(*)").deny("Bash(rm:*)");
726
727 let decision = policy.check("Bash", &json!({"command": "rm -rf /tmp"}));
729 assert_eq!(decision, PermissionDecision::Deny);
730
731 let decision = policy.check("Bash", &json!({"command": "ls -la"}));
733 assert_eq!(decision, PermissionDecision::Allow);
734 }
735
736 #[test]
737 fn test_policy_allow_wins_over_ask() {
738 let policy = PermissionPolicy::new()
739 .allow("Bash(cargo:*)")
740 .ask("Bash(*)");
741
742 let decision = policy.check("Bash", &json!({"command": "cargo build"}));
744 assert_eq!(decision, PermissionDecision::Allow);
745
746 let decision = policy.check("Bash", &json!({"command": "npm install"}));
748 assert_eq!(decision, PermissionDecision::Ask);
749 }
750
751 #[test]
752 fn test_policy_disabled() {
753 let mut policy = PermissionPolicy::new().deny("Bash(rm:*)").ask("Bash(*)");
754 policy.enabled = false;
755
756 let decision = policy.check("Bash", &json!({"command": "rm -rf /"}));
758 assert_eq!(decision, PermissionDecision::Allow);
759 }
760
761 #[test]
762 fn test_policy_is_allowed() {
763 let policy = PermissionPolicy::new().allow("Bash(cargo:*)");
764
765 assert!(policy.is_allowed("Bash", &json!({"command": "cargo build"})));
766 assert!(!policy.is_allowed("Bash", &json!({"command": "npm install"})));
767 }
768
769 #[test]
770 fn test_policy_is_denied() {
771 let policy = PermissionPolicy::new().deny("Bash(rm:*)");
772
773 assert!(policy.is_denied("Bash", &json!({"command": "rm -rf /"})));
774 assert!(!policy.is_denied("Bash", &json!({"command": "ls -la"})));
775 }
776
777 #[test]
778 fn test_policy_requires_confirmation() {
779 let mut policy = PermissionPolicy::new().allow("Read(*)").ask("Write(*)");
781 policy.default_decision = PermissionDecision::Deny; assert!(policy.requires_confirmation("Write", &json!({"file_path": "/tmp/test"})));
784 assert!(!policy.requires_confirmation("Read", &json!({"file_path": "/tmp/test"})));
785 }
786
787 #[test]
788 fn test_policy_matching_rules() {
789 let policy = PermissionPolicy::new()
790 .allow("Bash(cargo:*)")
791 .deny("Bash(cargo fmt:*)")
792 .ask("Bash(*)");
793
794 let matching = policy.get_matching_rules("Bash", &json!({"command": "cargo fmt"}));
795 assert_eq!(matching.deny.len(), 1);
796 assert_eq!(matching.allow.len(), 1);
797 assert_eq!(matching.ask.len(), 1);
798 }
799
800 #[test]
801 fn test_policy_allow_all() {
802 let policy =
803 PermissionPolicy::new().allow_all(&["Bash(cargo:*)", "Bash(npm:*)", "Grep(*)"]);
804
805 assert_eq!(policy.allow.len(), 3);
806 assert!(policy.is_allowed("Bash", &json!({"command": "cargo build"})));
807 assert!(policy.is_allowed("Bash", &json!({"command": "npm run test"})));
808 assert!(policy.is_allowed("Grep", &json!({"pattern": "foo"})));
809 }
810
811 #[test]
816 fn test_manager_default() {
817 let manager = PermissionManager::new();
818 assert_eq!(
819 manager.global_policy().default_decision,
820 PermissionDecision::Ask
821 );
822 }
823
824 #[test]
825 fn test_manager_with_global_policy() {
826 let policy = PermissionPolicy::permissive();
827 let manager = PermissionManager::with_global_policy(policy);
828 assert_eq!(
829 manager.global_policy().default_decision,
830 PermissionDecision::Allow
831 );
832 }
833
834 #[test]
835 fn test_manager_session_policy() {
836 let mut manager = PermissionManager::new();
837
838 let session_policy = PermissionPolicy::new().allow("Bash(cargo:*)");
839 manager.set_session_policy("session-1", session_policy);
840
841 let decision = manager.check("session-1", "Bash", &json!({"command": "cargo build"}));
843 assert_eq!(decision, PermissionDecision::Allow);
844
845 let decision = manager.check("session-2", "Bash", &json!({"command": "cargo build"}));
847 assert_eq!(decision, PermissionDecision::Ask);
848 }
849
850 #[test]
851 fn test_manager_remove_session_policy() {
852 let mut manager = PermissionManager::new();
853
854 let session_policy = PermissionPolicy::permissive();
855 manager.set_session_policy("session-1", session_policy);
856
857 let decision = manager.check("session-1", "Bash", &json!({"command": "anything"}));
859 assert_eq!(decision, PermissionDecision::Allow);
860
861 manager.remove_session_policy("session-1");
862
863 let decision = manager.check("session-1", "Bash", &json!({"command": "anything"}));
865 assert_eq!(decision, PermissionDecision::Ask);
866 }
867
868 #[test]
869 fn test_manager_global_deny_overrides_session_allow() {
870 let mut manager =
871 PermissionManager::with_global_policy(PermissionPolicy::new().deny("Bash(rm:*)"));
872
873 let session_policy = PermissionPolicy::new().allow("Bash(*)");
874 manager.set_session_policy("session-1", session_policy);
875
876 let decision = manager.check("session-1", "Bash", &json!({"command": "rm -rf /"}));
878 assert_eq!(decision, PermissionDecision::Deny);
879
880 let decision = manager.check("session-1", "Bash", &json!({"command": "ls -la"}));
882 assert_eq!(decision, PermissionDecision::Allow);
883 }
884
885 #[test]
890 fn test_realistic_dev_policy() {
891 let policy = PermissionPolicy::new()
892 .allow_all(&[
894 "Bash(cargo:*)",
895 "Bash(npm:*)",
896 "Bash(pnpm:*)",
897 "Bash(just:*)",
898 "Bash(git status:*)",
899 "Bash(git diff:*)",
900 "Bash(echo:*)",
901 "Grep(*)",
902 "Glob(*)",
903 "Ls(*)",
904 ])
905 .deny_all(&["Bash(rm -rf:*)", "Bash(sudo:*)", "Bash(curl | sh:*)"])
907 .ask_all(&["Write(*)", "Edit(*)"]);
909
910 assert!(policy.is_allowed("Bash", &json!({"command": "cargo build"})));
912 assert!(policy.is_allowed("Bash", &json!({"command": "npm run test"})));
913 assert!(policy.is_allowed("Grep", &json!({"pattern": "TODO"})));
914
915 assert!(policy.is_denied("Bash", &json!({"command": "rm -rf /"})));
917 assert!(policy.is_denied("Bash", &json!({"command": "sudo apt install"})));
918
919 assert!(policy.requires_confirmation("Write", &json!({"file_path": "/tmp/test.rs"})));
921 assert!(policy.requires_confirmation("Edit", &json!({"file_path": "src/main.rs"})));
922 }
923
924 #[test]
925 fn test_mcp_tool_permissions() {
926 let policy = PermissionPolicy::new()
927 .allow("mcp__pencil")
928 .deny("mcp__dangerous");
929
930 assert!(policy.is_allowed("mcp__pencil__batch_design", &json!({})));
931 assert!(policy.is_allowed("mcp__pencil__batch_get", &json!({})));
932 assert!(policy.is_denied("mcp__dangerous__execute", &json!({})));
933 }
934
935 #[test]
936 fn test_serialization() {
937 let policy = PermissionPolicy::new()
938 .allow("Bash(cargo:*)")
939 .deny("Bash(rm:*)");
940
941 let json = serde_json::to_string(&policy).unwrap();
942 let deserialized: PermissionPolicy = serde_json::from_str(&json).unwrap();
943
944 assert_eq!(deserialized.allow.len(), 1);
945 assert_eq!(deserialized.deny.len(), 1);
946 }
947
948 #[test]
949 fn test_matching_rules_is_empty() {
950 let rules = MatchingRules {
951 deny: vec![],
952 allow: vec![],
953 ask: vec![],
954 };
955 assert!(rules.is_empty());
956
957 let rules = MatchingRules {
958 deny: vec!["Bash".to_string()],
959 allow: vec![],
960 ask: vec![],
961 };
962 assert!(!rules.is_empty());
963
964 let rules = MatchingRules {
965 deny: vec![],
966 allow: vec!["Read".to_string()],
967 ask: vec![],
968 };
969 assert!(!rules.is_empty());
970
971 let rules = MatchingRules {
972 deny: vec![],
973 allow: vec![],
974 ask: vec!["Write".to_string()],
975 };
976 assert!(!rules.is_empty());
977 }
978
979 #[test]
980 fn test_permission_manager_default() {
981 let pm = PermissionManager::default();
982 let policy = pm.global_policy();
983 assert!(policy.allow.is_empty());
984 assert!(policy.deny.is_empty());
985 assert!(policy.ask.is_empty());
986 }
987
988 #[test]
989 fn test_permission_manager_set_global_policy() {
990 let mut pm = PermissionManager::new();
991 let policy = PermissionPolicy::new().allow("Bash(*)");
992 pm.set_global_policy(policy);
993 assert_eq!(pm.global_policy().allow.len(), 1);
994 }
995
996 #[test]
997 fn test_permission_manager_session_policy() {
998 let mut pm = PermissionManager::new();
999 let policy = PermissionPolicy::new().deny("Bash(rm:*)");
1000 pm.set_session_policy("s1", policy);
1001
1002 let effective = pm.get_effective_policy("s1");
1003 assert_eq!(effective.deny.len(), 1);
1004
1005 let global = pm.get_effective_policy("s2");
1007 assert!(global.deny.is_empty());
1008 }
1009
1010 #[test]
1011 fn test_permission_manager_remove_session_policy() {
1012 let mut pm = PermissionManager::new();
1013 pm.set_session_policy("s1", PermissionPolicy::new().deny("Bash(*)"));
1014 assert_eq!(pm.get_effective_policy("s1").deny.len(), 1);
1015
1016 pm.remove_session_policy("s1");
1017 assert!(pm.get_effective_policy("s1").deny.is_empty());
1018 }
1019
1020 #[test]
1021 fn test_permission_manager_check_deny() {
1022 let mut pm = PermissionManager::new();
1023 pm.set_global_policy(PermissionPolicy::new().deny("Bash(rm:*)"));
1024
1025 let decision = pm.check("s1", "Bash", &json!({"command": "rm -rf /"}));
1026 assert_eq!(decision, PermissionDecision::Deny);
1027 }
1028
1029 #[test]
1030 fn test_permission_manager_check_allow() {
1031 let mut pm = PermissionManager::new();
1032 pm.set_global_policy(PermissionPolicy::new().allow("Bash(cargo:*)"));
1033
1034 let decision = pm.check("s1", "Bash", &json!({"command": "cargo build"}));
1035 assert_eq!(decision, PermissionDecision::Allow);
1036 }
1037
1038 #[test]
1039 fn test_permission_manager_check_session_override() {
1040 let mut pm = PermissionManager::new();
1041 pm.set_global_policy(PermissionPolicy::new().allow("Bash(*)"));
1042 pm.set_session_policy("s1", PermissionPolicy::new().deny("Bash(rm:*)"));
1043
1044 let decision = pm.check("s1", "Bash", &json!({"command": "rm -rf /"}));
1046 assert_eq!(decision, PermissionDecision::Deny);
1047
1048 let decision = pm.check("s2", "Bash", &json!({"command": "rm -rf /"}));
1050 assert_eq!(decision, PermissionDecision::Allow);
1051 }
1052
1053 #[test]
1054 fn test_permission_manager_with_global_policy() {
1055 let policy = PermissionPolicy::new().allow("Read(*)").deny("Write(*)");
1056 let pm = PermissionManager::with_global_policy(policy);
1057 assert_eq!(pm.global_policy().allow.len(), 1);
1058 assert_eq!(pm.global_policy().deny.len(), 1);
1059 }
1060}