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, PartialEq, Eq)]
53pub struct PermissionRule {
54 pub rule: String,
56 #[serde(skip)]
58 tool_name: Option<String>,
59 #[serde(skip)]
61 arg_pattern: Option<String>,
62}
63
64impl<'de> Deserialize<'de> for PermissionRule {
65 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
66 where
67 D: serde::Deserializer<'de>,
68 {
69 #[derive(Deserialize)]
71 #[serde(untagged)]
72 enum RuleRepr {
73 Plain(String),
74 Struct { rule: String },
75 }
76
77 let rule_str = match RuleRepr::deserialize(deserializer)? {
78 RuleRepr::Plain(s) => s,
79 RuleRepr::Struct { rule } => rule,
80 };
81 Ok(PermissionRule::new(&rule_str))
83 }
84}
85
86impl PermissionRule {
87 pub fn new(rule: &str) -> Self {
89 let (tool_name, arg_pattern) = Self::parse_rule(rule);
90 Self {
91 rule: rule.to_string(),
92 tool_name,
93 arg_pattern,
94 }
95 }
96
97 fn parse_rule(rule: &str) -> (Option<String>, Option<String>) {
99 if let Some(paren_start) = rule.find('(') {
101 if rule.ends_with(')') {
102 let tool_name = rule[..paren_start].to_string();
103 let pattern = rule[paren_start + 1..rule.len() - 1].to_string();
104 return (Some(tool_name), Some(pattern));
105 }
106 }
107 (Some(rule.to_string()), None)
109 }
110
111 pub fn matches(&self, tool_name: &str, args: &serde_json::Value) -> bool {
113 let rule_tool = match &self.tool_name {
115 Some(t) => t,
116 None => return false,
117 };
118
119 if !self.matches_tool_name(rule_tool, tool_name) {
120 return false;
121 }
122
123 let pattern = match &self.arg_pattern {
125 Some(p) => p,
126 None => return true,
127 };
128
129 self.matches_args(pattern, tool_name, args)
131 }
132
133 fn matches_tool_name(&self, rule_tool: &str, actual_tool: &str) -> bool {
135 if rule_tool.contains('*') || rule_tool.contains('?') {
139 return self.glob_match(rule_tool, actual_tool);
140 }
141
142 if rule_tool.starts_with("mcp__") && actual_tool.starts_with("mcp__") {
144 if actual_tool.starts_with(rule_tool) {
146 return true;
147 }
148 }
149 rule_tool.eq_ignore_ascii_case(actual_tool)
150 }
151
152 fn matches_args(&self, pattern: &str, tool_name: &str, args: &serde_json::Value) -> bool {
154 if pattern == "*" {
156 return true;
157 }
158
159 let arg_string = self.build_arg_string(tool_name, args);
161
162 self.glob_match(pattern, &arg_string)
164 }
165
166 fn build_arg_string(&self, tool_name: &str, args: &serde_json::Value) -> String {
168 match tool_name.to_lowercase().as_str() {
169 "bash" => {
170 args.get("command")
172 .and_then(|v| v.as_str())
173 .unwrap_or("")
174 .to_string()
175 }
176 "read" | "write" | "edit" => {
177 args.get("file_path")
179 .and_then(|v| v.as_str())
180 .unwrap_or("")
181 .to_string()
182 }
183 "glob" => {
184 args.get("pattern")
186 .and_then(|v| v.as_str())
187 .unwrap_or("")
188 .to_string()
189 }
190 "grep" => {
191 let pattern = args.get("pattern").and_then(|v| v.as_str()).unwrap_or("");
193 let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("");
194 format!("{} {}", pattern, path)
195 }
196 "ls" => {
197 args.get("path")
199 .and_then(|v| v.as_str())
200 .unwrap_or("")
201 .to_string()
202 }
203 _ => {
204 serde_json::to_string(args).unwrap_or_default()
206 }
207 }
208 }
209
210 fn glob_match(&self, pattern: &str, text: &str) -> bool {
217 if let Some(prefix) = pattern.strip_suffix(":*") {
219 return text.starts_with(prefix);
220 }
221
222 let text = text.replace('\\', "/");
224
225 let regex_pattern = Self::glob_to_regex(pattern);
227 if let Ok(re) = regex::Regex::new(®ex_pattern) {
228 re.is_match(&text)
229 } else {
230 text.starts_with(pattern)
232 }
233 }
234
235 fn glob_to_regex(pattern: &str) -> String {
237 let mut regex = String::from("^");
238 let chars: Vec<char> = pattern.chars().collect();
239 let mut i = 0;
240
241 while i < chars.len() {
242 let c = chars[i];
243 match c {
244 '*' => {
245 if i + 1 < chars.len() && chars[i + 1] == '*' {
247 if i + 2 < chars.len() && chars[i + 2] == '/' {
250 regex.push_str(".*");
251 i += 3;
252 } else {
253 regex.push_str(".*");
254 i += 2;
255 }
256 } else {
257 regex.push_str("[^/\\\\]*");
259 i += 1;
260 }
261 }
262 '?' => {
263 regex.push_str("[^/\\\\]");
265 i += 1;
266 }
267 '.' | '+' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\' => {
268 regex.push('\\');
270 regex.push(c);
271 i += 1;
272 }
273 _ => {
274 regex.push(c);
275 i += 1;
276 }
277 }
278 }
279
280 regex.push('$');
281 regex
282 }
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct PermissionPolicy {
294 #[serde(default)]
296 pub deny: Vec<PermissionRule>,
297
298 #[serde(default)]
300 pub allow: Vec<PermissionRule>,
301
302 #[serde(default)]
304 pub ask: Vec<PermissionRule>,
305
306 #[serde(default = "default_decision")]
308 pub default_decision: PermissionDecision,
309
310 #[serde(default = "default_enabled")]
312 pub enabled: bool,
313}
314
315fn default_decision() -> PermissionDecision {
316 PermissionDecision::Ask
317}
318
319fn default_enabled() -> bool {
320 true
321}
322
323impl Default for PermissionPolicy {
324 fn default() -> Self {
325 Self {
326 deny: Vec::new(),
327 allow: Vec::new(),
328 ask: Vec::new(),
329 default_decision: PermissionDecision::Ask,
330 enabled: true,
331 }
332 }
333}
334
335impl PermissionPolicy {
336 pub fn new() -> Self {
338 Self::default()
339 }
340
341 pub fn strict() -> Self {
343 Self {
344 deny: Vec::new(),
345 allow: Vec::new(),
346 ask: Vec::new(),
347 default_decision: PermissionDecision::Ask,
348 enabled: true,
349 }
350 }
351
352 pub fn deny(mut self, rule: &str) -> Self {
354 self.deny.push(PermissionRule::new(rule));
355 self
356 }
357
358 pub fn allow(mut self, rule: &str) -> Self {
360 self.allow.push(PermissionRule::new(rule));
361 self
362 }
363
364 pub fn ask(mut self, rule: &str) -> Self {
366 self.ask.push(PermissionRule::new(rule));
367 self
368 }
369
370 pub fn deny_all(mut self, rules: &[&str]) -> Self {
372 for rule in rules {
373 self.deny.push(PermissionRule::new(rule));
374 }
375 self
376 }
377
378 pub fn allow_all(mut self, rules: &[&str]) -> Self {
380 for rule in rules {
381 self.allow.push(PermissionRule::new(rule));
382 }
383 self
384 }
385
386 pub fn ask_all(mut self, rules: &[&str]) -> Self {
388 for rule in rules {
389 self.ask.push(PermissionRule::new(rule));
390 }
391 self
392 }
393
394 pub fn check(&self, tool_name: &str, args: &serde_json::Value) -> PermissionDecision {
402 if !self.enabled {
403 return PermissionDecision::Allow;
404 }
405
406 for rule in &self.deny {
408 if rule.matches(tool_name, args) {
409 return PermissionDecision::Deny;
410 }
411 }
412
413 for rule in &self.allow {
415 if rule.matches(tool_name, args) {
416 return PermissionDecision::Allow;
417 }
418 }
419
420 for rule in &self.ask {
422 if rule.matches(tool_name, args) {
423 return PermissionDecision::Ask;
424 }
425 }
426
427 self.default_decision
429 }
430
431 pub fn is_allowed(&self, tool_name: &str, args: &serde_json::Value) -> bool {
433 matches!(self.check(tool_name, args), PermissionDecision::Allow)
434 }
435
436 pub fn is_denied(&self, tool_name: &str, args: &serde_json::Value) -> bool {
438 matches!(self.check(tool_name, args), PermissionDecision::Deny)
439 }
440
441 pub fn requires_confirmation(&self, tool_name: &str, args: &serde_json::Value) -> bool {
443 matches!(self.check(tool_name, args), PermissionDecision::Ask)
444 }
445
446 pub fn get_matching_rules(&self, tool_name: &str, args: &serde_json::Value) -> MatchingRules {
448 let mut result = MatchingRules::default();
449
450 for rule in &self.deny {
451 if rule.matches(tool_name, args) {
452 result.deny.push(rule.rule.clone());
453 }
454 }
455
456 for rule in &self.allow {
457 if rule.matches(tool_name, args) {
458 result.allow.push(rule.rule.clone());
459 }
460 }
461
462 for rule in &self.ask {
463 if rule.matches(tool_name, args) {
464 result.ask.push(rule.rule.clone());
465 }
466 }
467
468 result
469 }
470}
471
472impl PermissionChecker for PermissionPolicy {
473 fn check(&self, tool_name: &str, args: &serde_json::Value) -> PermissionDecision {
474 self.check(tool_name, args)
475 }
476}
477
478#[derive(Debug, Default, Clone)]
480pub struct MatchingRules {
481 pub deny: Vec<String>,
482 pub allow: Vec<String>,
483 pub ask: Vec<String>,
484}
485
486impl MatchingRules {
487 pub fn is_empty(&self) -> bool {
488 self.deny.is_empty() && self.allow.is_empty() && self.ask.is_empty()
489 }
490}
491
492#[derive(Debug)]
494pub struct PermissionManager {
495 global_policy: PermissionPolicy,
497 session_policies: HashMap<String, PermissionPolicy>,
499}
500
501impl Default for PermissionManager {
502 fn default() -> Self {
503 Self::new()
504 }
505}
506
507impl PermissionManager {
508 pub fn new() -> Self {
510 Self {
511 global_policy: PermissionPolicy::default(),
512 session_policies: HashMap::new(),
513 }
514 }
515
516 pub fn with_global_policy(policy: PermissionPolicy) -> Self {
518 Self {
519 global_policy: policy,
520 session_policies: HashMap::new(),
521 }
522 }
523
524 pub fn set_global_policy(&mut self, policy: PermissionPolicy) {
526 self.global_policy = policy;
527 }
528
529 pub fn global_policy(&self) -> &PermissionPolicy {
531 &self.global_policy
532 }
533
534 pub fn set_session_policy(&mut self, session_id: &str, policy: PermissionPolicy) {
536 self.session_policies.insert(session_id.to_string(), policy);
537 }
538
539 pub fn remove_session_policy(&mut self, session_id: &str) {
541 self.session_policies.remove(session_id);
542 }
543
544 pub fn get_effective_policy(&self, session_id: &str) -> &PermissionPolicy {
549 self.session_policies
550 .get(session_id)
551 .unwrap_or(&self.global_policy)
552 }
553
554 pub fn check(
556 &self,
557 session_id: &str,
558 tool_name: &str,
559 args: &serde_json::Value,
560 ) -> PermissionDecision {
561 let policy = self.get_effective_policy(session_id);
563
564 for rule in &policy.deny {
566 if rule.matches(tool_name, args) {
567 return PermissionDecision::Deny;
568 }
569 }
570
571 if !self.session_policies.contains_key(session_id) {
573 } else {
575 for rule in &self.global_policy.deny {
576 if rule.matches(tool_name, args) {
577 return PermissionDecision::Deny;
578 }
579 }
580 }
581
582 for rule in &policy.allow {
584 if rule.matches(tool_name, args) {
585 return PermissionDecision::Allow;
586 }
587 }
588
589 for rule in &policy.ask {
591 if rule.matches(tool_name, args) {
592 return PermissionDecision::Ask;
593 }
594 }
595
596 policy.default_decision
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604 use serde_json::json;
605
606 #[test]
611 fn test_rule_parse_simple() {
612 let rule = PermissionRule::new("Bash");
613 assert_eq!(rule.tool_name, Some("Bash".to_string()));
614 assert_eq!(rule.arg_pattern, None);
615 }
616
617 #[test]
618 fn test_rule_parse_with_pattern() {
619 let rule = PermissionRule::new("Bash(cargo:*)");
620 assert_eq!(rule.tool_name, Some("Bash".to_string()));
621 assert_eq!(rule.arg_pattern, Some("cargo:*".to_string()));
622 }
623
624 #[test]
625 fn test_rule_parse_wildcard() {
626 let rule = PermissionRule::new("Grep(*)");
627 assert_eq!(rule.tool_name, Some("Grep".to_string()));
628 assert_eq!(rule.arg_pattern, Some("*".to_string()));
629 }
630
631 #[test]
632 fn test_rule_match_tool_only() {
633 let rule = PermissionRule::new("Bash");
634 assert!(rule.matches("Bash", &json!({"command": "ls -la"})));
635 assert!(rule.matches("bash", &json!({"command": "echo hello"})));
636 assert!(!rule.matches("Read", &json!({})));
637 }
638
639 #[test]
640 fn test_rule_match_wildcard() {
641 let rule = PermissionRule::new("Grep(*)");
642 assert!(rule.matches("Grep", &json!({"pattern": "foo", "path": "/tmp"})));
643 assert!(rule.matches("grep", &json!({"pattern": "bar"})));
644 }
645
646 #[test]
647 fn test_rule_match_prefix_wildcard() {
648 let rule = PermissionRule::new("Bash(cargo:*)");
649 assert!(rule.matches("Bash", &json!({"command": "cargo build"})));
650 assert!(rule.matches("Bash", &json!({"command": "cargo test --lib"})));
651 assert!(rule.matches("Bash", &json!({"command": "cargo"})));
652 assert!(!rule.matches("Bash", &json!({"command": "npm install"})));
653 }
654
655 #[test]
656 fn test_rule_match_npm_commands() {
657 let rule = PermissionRule::new("Bash(npm run:*)");
658 assert!(rule.matches("Bash", &json!({"command": "npm run test"})));
659 assert!(rule.matches("Bash", &json!({"command": "npm run build"})));
660 assert!(!rule.matches("Bash", &json!({"command": "npm install"})));
661 }
662
663 #[test]
664 fn test_rule_match_file_path() {
665 let rule = PermissionRule::new("Read(src/*.rs)");
666 assert!(rule.matches("Read", &json!({"file_path": "src/main.rs"})));
667 assert!(rule.matches("Read", &json!({"file_path": "src/lib.rs"})));
668 assert!(!rule.matches("Read", &json!({"file_path": "src/foo/bar.rs"})));
669 }
670
671 #[test]
672 fn test_rule_match_recursive_glob() {
673 let rule = PermissionRule::new("Read(src/**/*.rs)");
674 assert!(rule.matches("Read", &json!({"file_path": "src/main.rs"})));
675 assert!(rule.matches("Read", &json!({"file_path": "src/foo/bar.rs"})));
676 assert!(rule.matches("Read", &json!({"file_path": "src/a/b/c.rs"})));
677 }
678
679 #[test]
680 fn test_rule_match_mcp_tool() {
681 let rule = PermissionRule::new("mcp__pencil");
682 assert!(rule.matches("mcp__pencil", &json!({})));
683 assert!(rule.matches("mcp__pencil__batch_design", &json!({})));
684 assert!(rule.matches("mcp__pencil__batch_get", &json!({})));
685 assert!(!rule.matches("mcp__other", &json!({})));
686 }
687
688 #[test]
689 fn test_rule_match_mcp_tool_wildcard() {
690 let rule = PermissionRule::new("mcp__longvt__*");
694 assert!(rule.matches("mcp__longvt__search", &json!({})));
695 assert!(rule.matches("mcp__longvt__create_memory", &json!({})));
696 assert!(rule.matches("mcp__longvt__delete", &json!({})));
697 assert!(!rule.matches("mcp__pencil__batch_design", &json!({})));
698 assert!(!rule.matches("mcp__other__tool", &json!({})));
699
700 let rule_all = PermissionRule::new("mcp__*");
702 assert!(rule_all.matches("mcp__longvt__search", &json!({})));
703 assert!(rule_all.matches("mcp__pencil__draw", &json!({})));
704 assert!(!rule_all.matches("bash", &json!({})));
705 }
706
707 #[test]
708 fn test_rule_case_insensitive() {
709 let rule = PermissionRule::new("BASH(cargo:*)");
710 assert!(rule.matches("Bash", &json!({"command": "cargo build"})));
711 assert!(rule.matches("bash", &json!({"command": "cargo test"})));
712 assert!(rule.matches("BASH", &json!({"command": "cargo check"})));
713 }
714
715 #[test]
720 fn test_policy_default() {
721 let policy = PermissionPolicy::default();
722 assert!(policy.enabled);
723 assert_eq!(policy.default_decision, PermissionDecision::Ask);
724 assert!(policy.allow.is_empty());
725 assert!(policy.deny.is_empty());
726 assert!(policy.ask.is_empty());
727 }
728
729 #[test]
730 fn test_policy_explicit_allow_default() {
731 let policy = PermissionPolicy {
732 default_decision: PermissionDecision::Allow,
733 ..PermissionPolicy::default()
734 };
735 assert_eq!(policy.default_decision, PermissionDecision::Allow);
736 }
737
738 #[test]
739 fn test_policy_strict() {
740 let policy = PermissionPolicy::strict();
741 assert_eq!(policy.default_decision, PermissionDecision::Ask);
742 }
743
744 #[test]
745 fn test_policy_builder() {
746 let policy = PermissionPolicy::new()
747 .allow("Bash(cargo:*)")
748 .allow("Grep(*)")
749 .deny("Bash(rm -rf:*)")
750 .ask("Write(*)");
751
752 assert_eq!(policy.allow.len(), 2);
753 assert_eq!(policy.deny.len(), 1);
754 assert_eq!(policy.ask.len(), 1);
755 }
756
757 #[test]
758 fn test_policy_check_allow() {
759 let policy = PermissionPolicy::new().allow("Bash(cargo:*)");
760
761 let decision = policy.check("Bash", &json!({"command": "cargo build"}));
762 assert_eq!(decision, PermissionDecision::Allow);
763 }
764
765 #[test]
766 fn test_policy_check_deny() {
767 let policy = PermissionPolicy::new().deny("Bash(rm -rf:*)");
768
769 let decision = policy.check("Bash", &json!({"command": "rm -rf /"}));
770 assert_eq!(decision, PermissionDecision::Deny);
771 }
772
773 #[test]
774 fn test_policy_check_ask() {
775 let policy = PermissionPolicy::new().ask("Write(*)");
776
777 let decision = policy.check("Write", &json!({"file_path": "/tmp/test.txt"}));
778 assert_eq!(decision, PermissionDecision::Ask);
779 }
780
781 #[test]
782 fn test_policy_check_default() {
783 let policy = PermissionPolicy::new();
784
785 let decision = policy.check("Unknown", &json!({}));
786 assert_eq!(decision, PermissionDecision::Ask);
787 }
788
789 #[test]
790 fn test_policy_deny_wins_over_allow() {
791 let policy = PermissionPolicy::new().allow("Bash(*)").deny("Bash(rm:*)");
792
793 let decision = policy.check("Bash", &json!({"command": "rm -rf /tmp"}));
795 assert_eq!(decision, PermissionDecision::Deny);
796
797 let decision = policy.check("Bash", &json!({"command": "ls -la"}));
799 assert_eq!(decision, PermissionDecision::Allow);
800 }
801
802 #[test]
803 fn test_policy_allow_wins_over_ask() {
804 let policy = PermissionPolicy::new()
805 .allow("Bash(cargo:*)")
806 .ask("Bash(*)");
807
808 let decision = policy.check("Bash", &json!({"command": "cargo build"}));
810 assert_eq!(decision, PermissionDecision::Allow);
811
812 let decision = policy.check("Bash", &json!({"command": "npm install"}));
814 assert_eq!(decision, PermissionDecision::Ask);
815 }
816
817 #[test]
818 fn test_policy_disabled() {
819 let mut policy = PermissionPolicy::new().deny("Bash(rm:*)").ask("Bash(*)");
820 policy.enabled = false;
821
822 let decision = policy.check("Bash", &json!({"command": "rm -rf /"}));
824 assert_eq!(decision, PermissionDecision::Allow);
825 }
826
827 #[test]
828 fn test_policy_is_allowed() {
829 let policy = PermissionPolicy::new().allow("Bash(cargo:*)");
830
831 assert!(policy.is_allowed("Bash", &json!({"command": "cargo build"})));
832 assert!(!policy.is_allowed("Bash", &json!({"command": "npm install"})));
833 }
834
835 #[test]
836 fn test_policy_is_denied() {
837 let policy = PermissionPolicy::new().deny("Bash(rm:*)");
838
839 assert!(policy.is_denied("Bash", &json!({"command": "rm -rf /"})));
840 assert!(!policy.is_denied("Bash", &json!({"command": "ls -la"})));
841 }
842
843 #[test]
844 fn test_policy_requires_confirmation() {
845 let mut policy = PermissionPolicy::new().allow("Read(*)").ask("Write(*)");
847 policy.default_decision = PermissionDecision::Deny; assert!(policy.requires_confirmation("Write", &json!({"file_path": "/tmp/test"})));
850 assert!(!policy.requires_confirmation("Read", &json!({"file_path": "/tmp/test"})));
851 }
852
853 #[test]
854 fn test_policy_matching_rules() {
855 let policy = PermissionPolicy::new()
856 .allow("Bash(cargo:*)")
857 .deny("Bash(cargo fmt:*)")
858 .ask("Bash(*)");
859
860 let matching = policy.get_matching_rules("Bash", &json!({"command": "cargo fmt"}));
861 assert_eq!(matching.deny.len(), 1);
862 assert_eq!(matching.allow.len(), 1);
863 assert_eq!(matching.ask.len(), 1);
864 }
865
866 #[test]
867 fn test_policy_allow_all() {
868 let policy =
869 PermissionPolicy::new().allow_all(&["Bash(cargo:*)", "Bash(npm:*)", "Grep(*)"]);
870
871 assert_eq!(policy.allow.len(), 3);
872 assert!(policy.is_allowed("Bash", &json!({"command": "cargo build"})));
873 assert!(policy.is_allowed("Bash", &json!({"command": "npm run test"})));
874 assert!(policy.is_allowed("Grep", &json!({"pattern": "foo"})));
875 }
876
877 #[test]
882 fn test_rule_deserialize_plain_string() {
883 let rule: PermissionRule = serde_yaml::from_str("read").unwrap();
885 assert_eq!(rule.rule, "read");
886 assert!(rule.matches("read", &json!({})));
887 assert!(!rule.matches("write", &json!({})));
888 }
889
890 #[test]
891 fn test_rule_deserialize_plain_string_with_pattern() {
892 let rule: PermissionRule = serde_yaml::from_str("\"Bash(cargo:*)\"").unwrap();
894 assert_eq!(rule.rule, "Bash(cargo:*)");
895 assert!(rule.matches("Bash", &json!({"command": "cargo build"})));
896 }
897
898 #[test]
899 fn test_rule_deserialize_struct_form() {
900 let rule: PermissionRule = serde_yaml::from_str("rule: read").unwrap();
902 assert_eq!(rule.rule, "read");
903 assert!(rule.matches("read", &json!({})));
904 }
905
906 #[test]
907 fn test_rule_deserialize_in_policy() {
908 let yaml = r#"
910allow:
911 - read
912 - "Bash(cargo:*)"
913 - rule: grep
914deny:
915 - write
916"#;
917 let policy: PermissionPolicy = serde_yaml::from_str(yaml).unwrap();
918 assert_eq!(policy.allow.len(), 3);
919 assert_eq!(policy.deny.len(), 1);
920 assert!(policy.is_allowed("read", &json!({})));
921 assert!(policy.is_allowed("Bash", &json!({"command": "cargo build"})));
922 assert!(policy.is_allowed("grep", &json!({})));
923 assert!(policy.is_denied("write", &json!({})));
924 }
925
926 #[test]
931 fn test_manager_default() {
932 let manager = PermissionManager::new();
933 assert_eq!(
934 manager.global_policy().default_decision,
935 PermissionDecision::Ask
936 );
937 }
938
939 #[test]
940 fn test_manager_with_global_policy() {
941 let policy = PermissionPolicy {
942 default_decision: PermissionDecision::Allow,
943 ..PermissionPolicy::default()
944 };
945 let manager = PermissionManager::with_global_policy(policy);
946 assert_eq!(
947 manager.global_policy().default_decision,
948 PermissionDecision::Allow
949 );
950 }
951
952 #[test]
953 fn test_manager_session_policy() {
954 let mut manager = PermissionManager::new();
955
956 let session_policy = PermissionPolicy::new().allow("Bash(cargo:*)");
957 manager.set_session_policy("session-1", session_policy);
958
959 let decision = manager.check("session-1", "Bash", &json!({"command": "cargo build"}));
961 assert_eq!(decision, PermissionDecision::Allow);
962
963 let decision = manager.check("session-2", "Bash", &json!({"command": "cargo build"}));
965 assert_eq!(decision, PermissionDecision::Ask);
966 }
967
968 #[test]
969 fn test_manager_remove_session_policy() {
970 let mut manager = PermissionManager::new();
971
972 let session_policy = PermissionPolicy {
973 default_decision: PermissionDecision::Allow,
974 ..PermissionPolicy::default()
975 };
976 manager.set_session_policy("session-1", session_policy);
977
978 let decision = manager.check("session-1", "Bash", &json!({"command": "anything"}));
980 assert_eq!(decision, PermissionDecision::Allow);
981
982 manager.remove_session_policy("session-1");
983
984 let decision = manager.check("session-1", "Bash", &json!({"command": "anything"}));
986 assert_eq!(decision, PermissionDecision::Ask);
987 }
988
989 #[test]
990 fn test_manager_global_deny_overrides_session_allow() {
991 let mut manager =
992 PermissionManager::with_global_policy(PermissionPolicy::new().deny("Bash(rm:*)"));
993
994 let session_policy = PermissionPolicy::new().allow("Bash(*)");
995 manager.set_session_policy("session-1", session_policy);
996
997 let decision = manager.check("session-1", "Bash", &json!({"command": "rm -rf /"}));
999 assert_eq!(decision, PermissionDecision::Deny);
1000
1001 let decision = manager.check("session-1", "Bash", &json!({"command": "ls -la"}));
1003 assert_eq!(decision, PermissionDecision::Allow);
1004 }
1005
1006 #[test]
1011 fn test_realistic_dev_policy() {
1012 let policy = PermissionPolicy::new()
1013 .allow_all(&[
1015 "Bash(cargo:*)",
1016 "Bash(npm:*)",
1017 "Bash(pnpm:*)",
1018 "Bash(just:*)",
1019 "Bash(git status:*)",
1020 "Bash(git diff:*)",
1021 "Bash(echo:*)",
1022 "Grep(*)",
1023 "Glob(*)",
1024 "Ls(*)",
1025 ])
1026 .deny_all(&["Bash(rm -rf:*)", "Bash(sudo:*)", "Bash(curl | sh:*)"])
1028 .ask_all(&["Write(*)", "Edit(*)"]);
1030
1031 assert!(policy.is_allowed("Bash", &json!({"command": "cargo build"})));
1033 assert!(policy.is_allowed("Bash", &json!({"command": "npm run test"})));
1034 assert!(policy.is_allowed("Grep", &json!({"pattern": "TODO"})));
1035
1036 assert!(policy.is_denied("Bash", &json!({"command": "rm -rf /"})));
1038 assert!(policy.is_denied("Bash", &json!({"command": "sudo apt install"})));
1039
1040 assert!(policy.requires_confirmation("Write", &json!({"file_path": "/tmp/test.rs"})));
1042 assert!(policy.requires_confirmation("Edit", &json!({"file_path": "src/main.rs"})));
1043 }
1044
1045 #[test]
1046 fn test_mcp_tool_permissions() {
1047 let policy = PermissionPolicy::new()
1048 .allow("mcp__pencil")
1049 .deny("mcp__dangerous");
1050
1051 assert!(policy.is_allowed("mcp__pencil__batch_design", &json!({})));
1052 assert!(policy.is_allowed("mcp__pencil__batch_get", &json!({})));
1053 assert!(policy.is_denied("mcp__dangerous__execute", &json!({})));
1054 }
1055
1056 #[test]
1057 fn test_allow_by_default_with_mcp_wildcard_deny() {
1058 let policy = PermissionPolicy {
1060 default_decision: PermissionDecision::Allow,
1061 ..PermissionPolicy::default()
1062 }
1063 .deny("mcp__longvt__*");
1064
1065 assert_eq!(
1067 policy.check("mcp__longvt__search", &json!({})),
1068 PermissionDecision::Deny
1069 );
1070 assert_eq!(
1071 policy.check("mcp__longvt__create_memory", &json!({})),
1072 PermissionDecision::Deny
1073 );
1074 assert_eq!(
1076 policy.check("mcp__pencil__draw", &json!({})),
1077 PermissionDecision::Allow
1078 );
1079 assert_eq!(
1080 policy.check("bash", &json!({"command": "ls"})),
1081 PermissionDecision::Allow
1082 );
1083 }
1084
1085 #[test]
1086 fn test_serialization() {
1087 let policy = PermissionPolicy::new()
1088 .allow("Bash(cargo:*)")
1089 .deny("Bash(rm:*)");
1090
1091 let json = serde_json::to_string(&policy).unwrap();
1092 let deserialized: PermissionPolicy = serde_json::from_str(&json).unwrap();
1093
1094 assert_eq!(deserialized.allow.len(), 1);
1095 assert_eq!(deserialized.deny.len(), 1);
1096 }
1097
1098 #[test]
1099 fn test_matching_rules_is_empty() {
1100 let rules = MatchingRules {
1101 deny: vec![],
1102 allow: vec![],
1103 ask: vec![],
1104 };
1105 assert!(rules.is_empty());
1106
1107 let rules = MatchingRules {
1108 deny: vec!["Bash".to_string()],
1109 allow: vec![],
1110 ask: vec![],
1111 };
1112 assert!(!rules.is_empty());
1113
1114 let rules = MatchingRules {
1115 deny: vec![],
1116 allow: vec!["Read".to_string()],
1117 ask: vec![],
1118 };
1119 assert!(!rules.is_empty());
1120
1121 let rules = MatchingRules {
1122 deny: vec![],
1123 allow: vec![],
1124 ask: vec!["Write".to_string()],
1125 };
1126 assert!(!rules.is_empty());
1127 }
1128
1129 #[test]
1130 fn test_permission_manager_default() {
1131 let pm = PermissionManager::default();
1132 let policy = pm.global_policy();
1133 assert!(policy.allow.is_empty());
1134 assert!(policy.deny.is_empty());
1135 assert!(policy.ask.is_empty());
1136 }
1137
1138 #[test]
1139 fn test_permission_manager_set_global_policy() {
1140 let mut pm = PermissionManager::new();
1141 let policy = PermissionPolicy::new().allow("Bash(*)");
1142 pm.set_global_policy(policy);
1143 assert_eq!(pm.global_policy().allow.len(), 1);
1144 }
1145
1146 #[test]
1147 fn test_permission_manager_session_policy() {
1148 let mut pm = PermissionManager::new();
1149 let policy = PermissionPolicy::new().deny("Bash(rm:*)");
1150 pm.set_session_policy("s1", policy);
1151
1152 let effective = pm.get_effective_policy("s1");
1153 assert_eq!(effective.deny.len(), 1);
1154
1155 let global = pm.get_effective_policy("s2");
1157 assert!(global.deny.is_empty());
1158 }
1159
1160 #[test]
1161 fn test_permission_manager_remove_session_policy() {
1162 let mut pm = PermissionManager::new();
1163 pm.set_session_policy("s1", PermissionPolicy::new().deny("Bash(*)"));
1164 assert_eq!(pm.get_effective_policy("s1").deny.len(), 1);
1165
1166 pm.remove_session_policy("s1");
1167 assert!(pm.get_effective_policy("s1").deny.is_empty());
1168 }
1169
1170 #[test]
1171 fn test_permission_manager_check_deny() {
1172 let mut pm = PermissionManager::new();
1173 pm.set_global_policy(PermissionPolicy::new().deny("Bash(rm:*)"));
1174
1175 let decision = pm.check("s1", "Bash", &json!({"command": "rm -rf /"}));
1176 assert_eq!(decision, PermissionDecision::Deny);
1177 }
1178
1179 #[test]
1180 fn test_permission_manager_check_allow() {
1181 let mut pm = PermissionManager::new();
1182 pm.set_global_policy(PermissionPolicy::new().allow("Bash(cargo:*)"));
1183
1184 let decision = pm.check("s1", "Bash", &json!({"command": "cargo build"}));
1185 assert_eq!(decision, PermissionDecision::Allow);
1186 }
1187
1188 #[test]
1189 fn test_permission_manager_check_session_override() {
1190 let mut pm = PermissionManager::new();
1191 pm.set_global_policy(PermissionPolicy::new().allow("Bash(*)"));
1192 pm.set_session_policy("s1", PermissionPolicy::new().deny("Bash(rm:*)"));
1193
1194 let decision = pm.check("s1", "Bash", &json!({"command": "rm -rf /"}));
1196 assert_eq!(decision, PermissionDecision::Deny);
1197
1198 let decision = pm.check("s2", "Bash", &json!({"command": "rm -rf /"}));
1200 assert_eq!(decision, PermissionDecision::Allow);
1201 }
1202
1203 #[test]
1204 fn test_permission_manager_with_global_policy() {
1205 let policy = PermissionPolicy::new().allow("Read(*)").deny("Write(*)");
1206 let pm = PermissionManager::with_global_policy(policy);
1207 assert_eq!(pm.global_policy().allow.len(), 1);
1208 assert_eq!(pm.global_policy().deny.len(), 1);
1209 }
1210}