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