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 permissive() -> Self {
343 Self {
344 deny: Vec::new(),
345 allow: Vec::new(),
346 ask: Vec::new(),
347 default_decision: PermissionDecision::Allow,
348 enabled: true,
349 }
350 }
351
352 pub fn strict() -> Self {
354 Self {
355 deny: Vec::new(),
356 allow: Vec::new(),
357 ask: Vec::new(),
358 default_decision: PermissionDecision::Ask,
359 enabled: true,
360 }
361 }
362
363 pub fn deny(mut self, rule: &str) -> Self {
365 self.deny.push(PermissionRule::new(rule));
366 self
367 }
368
369 pub fn allow(mut self, rule: &str) -> Self {
371 self.allow.push(PermissionRule::new(rule));
372 self
373 }
374
375 pub fn ask(mut self, rule: &str) -> Self {
377 self.ask.push(PermissionRule::new(rule));
378 self
379 }
380
381 pub fn deny_all(mut self, rules: &[&str]) -> Self {
383 for rule in rules {
384 self.deny.push(PermissionRule::new(rule));
385 }
386 self
387 }
388
389 pub fn allow_all(mut self, rules: &[&str]) -> Self {
391 for rule in rules {
392 self.allow.push(PermissionRule::new(rule));
393 }
394 self
395 }
396
397 pub fn ask_all(mut self, rules: &[&str]) -> Self {
399 for rule in rules {
400 self.ask.push(PermissionRule::new(rule));
401 }
402 self
403 }
404
405 pub fn check(&self, tool_name: &str, args: &serde_json::Value) -> PermissionDecision {
413 if !self.enabled {
414 return PermissionDecision::Allow;
415 }
416
417 for rule in &self.deny {
419 if rule.matches(tool_name, args) {
420 return PermissionDecision::Deny;
421 }
422 }
423
424 for rule in &self.allow {
426 if rule.matches(tool_name, args) {
427 return PermissionDecision::Allow;
428 }
429 }
430
431 for rule in &self.ask {
433 if rule.matches(tool_name, args) {
434 return PermissionDecision::Ask;
435 }
436 }
437
438 self.default_decision
440 }
441
442 pub fn is_allowed(&self, tool_name: &str, args: &serde_json::Value) -> bool {
444 matches!(self.check(tool_name, args), PermissionDecision::Allow)
445 }
446
447 pub fn is_denied(&self, tool_name: &str, args: &serde_json::Value) -> bool {
449 matches!(self.check(tool_name, args), PermissionDecision::Deny)
450 }
451
452 pub fn requires_confirmation(&self, tool_name: &str, args: &serde_json::Value) -> bool {
454 matches!(self.check(tool_name, args), PermissionDecision::Ask)
455 }
456
457 pub fn get_matching_rules(&self, tool_name: &str, args: &serde_json::Value) -> MatchingRules {
459 let mut result = MatchingRules::default();
460
461 for rule in &self.deny {
462 if rule.matches(tool_name, args) {
463 result.deny.push(rule.rule.clone());
464 }
465 }
466
467 for rule in &self.allow {
468 if rule.matches(tool_name, args) {
469 result.allow.push(rule.rule.clone());
470 }
471 }
472
473 for rule in &self.ask {
474 if rule.matches(tool_name, args) {
475 result.ask.push(rule.rule.clone());
476 }
477 }
478
479 result
480 }
481}
482
483impl PermissionChecker for PermissionPolicy {
484 fn check(&self, tool_name: &str, args: &serde_json::Value) -> PermissionDecision {
485 self.check(tool_name, args)
486 }
487}
488
489#[derive(Debug, Default, Clone)]
491pub struct MatchingRules {
492 pub deny: Vec<String>,
493 pub allow: Vec<String>,
494 pub ask: Vec<String>,
495}
496
497impl MatchingRules {
498 pub fn is_empty(&self) -> bool {
499 self.deny.is_empty() && self.allow.is_empty() && self.ask.is_empty()
500 }
501}
502
503#[derive(Debug)]
505pub struct PermissionManager {
506 global_policy: PermissionPolicy,
508 session_policies: HashMap<String, PermissionPolicy>,
510}
511
512impl Default for PermissionManager {
513 fn default() -> Self {
514 Self::new()
515 }
516}
517
518impl PermissionManager {
519 pub fn new() -> Self {
521 Self {
522 global_policy: PermissionPolicy::default(),
523 session_policies: HashMap::new(),
524 }
525 }
526
527 pub fn with_global_policy(policy: PermissionPolicy) -> Self {
529 Self {
530 global_policy: policy,
531 session_policies: HashMap::new(),
532 }
533 }
534
535 pub fn set_global_policy(&mut self, policy: PermissionPolicy) {
537 self.global_policy = policy;
538 }
539
540 pub fn global_policy(&self) -> &PermissionPolicy {
542 &self.global_policy
543 }
544
545 pub fn set_session_policy(&mut self, session_id: &str, policy: PermissionPolicy) {
547 self.session_policies.insert(session_id.to_string(), policy);
548 }
549
550 pub fn remove_session_policy(&mut self, session_id: &str) {
552 self.session_policies.remove(session_id);
553 }
554
555 pub fn get_effective_policy(&self, session_id: &str) -> &PermissionPolicy {
560 self.session_policies
561 .get(session_id)
562 .unwrap_or(&self.global_policy)
563 }
564
565 pub fn check(
567 &self,
568 session_id: &str,
569 tool_name: &str,
570 args: &serde_json::Value,
571 ) -> PermissionDecision {
572 let policy = self.get_effective_policy(session_id);
574
575 for rule in &policy.deny {
577 if rule.matches(tool_name, args) {
578 return PermissionDecision::Deny;
579 }
580 }
581
582 if !self.session_policies.contains_key(session_id) {
584 } else {
586 for rule in &self.global_policy.deny {
587 if rule.matches(tool_name, args) {
588 return PermissionDecision::Deny;
589 }
590 }
591 }
592
593 for rule in &policy.allow {
595 if rule.matches(tool_name, args) {
596 return PermissionDecision::Allow;
597 }
598 }
599
600 for rule in &policy.ask {
602 if rule.matches(tool_name, args) {
603 return PermissionDecision::Ask;
604 }
605 }
606
607 policy.default_decision
609 }
610}
611
612#[cfg(test)]
613mod tests {
614 use super::*;
615 use serde_json::json;
616
617 #[test]
622 fn test_rule_parse_simple() {
623 let rule = PermissionRule::new("Bash");
624 assert_eq!(rule.tool_name, Some("Bash".to_string()));
625 assert_eq!(rule.arg_pattern, None);
626 }
627
628 #[test]
629 fn test_rule_parse_with_pattern() {
630 let rule = PermissionRule::new("Bash(cargo:*)");
631 assert_eq!(rule.tool_name, Some("Bash".to_string()));
632 assert_eq!(rule.arg_pattern, Some("cargo:*".to_string()));
633 }
634
635 #[test]
636 fn test_rule_parse_wildcard() {
637 let rule = PermissionRule::new("Grep(*)");
638 assert_eq!(rule.tool_name, Some("Grep".to_string()));
639 assert_eq!(rule.arg_pattern, Some("*".to_string()));
640 }
641
642 #[test]
643 fn test_rule_match_tool_only() {
644 let rule = PermissionRule::new("Bash");
645 assert!(rule.matches("Bash", &json!({"command": "ls -la"})));
646 assert!(rule.matches("bash", &json!({"command": "echo hello"})));
647 assert!(!rule.matches("Read", &json!({})));
648 }
649
650 #[test]
651 fn test_rule_match_wildcard() {
652 let rule = PermissionRule::new("Grep(*)");
653 assert!(rule.matches("Grep", &json!({"pattern": "foo", "path": "/tmp"})));
654 assert!(rule.matches("grep", &json!({"pattern": "bar"})));
655 }
656
657 #[test]
658 fn test_rule_match_prefix_wildcard() {
659 let rule = PermissionRule::new("Bash(cargo:*)");
660 assert!(rule.matches("Bash", &json!({"command": "cargo build"})));
661 assert!(rule.matches("Bash", &json!({"command": "cargo test --lib"})));
662 assert!(rule.matches("Bash", &json!({"command": "cargo"})));
663 assert!(!rule.matches("Bash", &json!({"command": "npm install"})));
664 }
665
666 #[test]
667 fn test_rule_match_npm_commands() {
668 let rule = PermissionRule::new("Bash(npm run:*)");
669 assert!(rule.matches("Bash", &json!({"command": "npm run test"})));
670 assert!(rule.matches("Bash", &json!({"command": "npm run build"})));
671 assert!(!rule.matches("Bash", &json!({"command": "npm install"})));
672 }
673
674 #[test]
675 fn test_rule_match_file_path() {
676 let rule = PermissionRule::new("Read(src/*.rs)");
677 assert!(rule.matches("Read", &json!({"file_path": "src/main.rs"})));
678 assert!(rule.matches("Read", &json!({"file_path": "src/lib.rs"})));
679 assert!(!rule.matches("Read", &json!({"file_path": "src/foo/bar.rs"})));
680 }
681
682 #[test]
683 fn test_rule_match_recursive_glob() {
684 let rule = PermissionRule::new("Read(src/**/*.rs)");
685 assert!(rule.matches("Read", &json!({"file_path": "src/main.rs"})));
686 assert!(rule.matches("Read", &json!({"file_path": "src/foo/bar.rs"})));
687 assert!(rule.matches("Read", &json!({"file_path": "src/a/b/c.rs"})));
688 }
689
690 #[test]
691 fn test_rule_match_mcp_tool() {
692 let rule = PermissionRule::new("mcp__pencil");
693 assert!(rule.matches("mcp__pencil", &json!({})));
694 assert!(rule.matches("mcp__pencil__batch_design", &json!({})));
695 assert!(rule.matches("mcp__pencil__batch_get", &json!({})));
696 assert!(!rule.matches("mcp__other", &json!({})));
697 }
698
699 #[test]
700 fn test_rule_match_mcp_tool_wildcard() {
701 let rule = PermissionRule::new("mcp__longvt__*");
705 assert!(rule.matches("mcp__longvt__search", &json!({})));
706 assert!(rule.matches("mcp__longvt__create_memory", &json!({})));
707 assert!(rule.matches("mcp__longvt__delete", &json!({})));
708 assert!(!rule.matches("mcp__pencil__batch_design", &json!({})));
709 assert!(!rule.matches("mcp__other__tool", &json!({})));
710
711 let rule_all = PermissionRule::new("mcp__*");
713 assert!(rule_all.matches("mcp__longvt__search", &json!({})));
714 assert!(rule_all.matches("mcp__pencil__draw", &json!({})));
715 assert!(!rule_all.matches("bash", &json!({})));
716 }
717
718 #[test]
719 fn test_rule_case_insensitive() {
720 let rule = PermissionRule::new("BASH(cargo:*)");
721 assert!(rule.matches("Bash", &json!({"command": "cargo build"})));
722 assert!(rule.matches("bash", &json!({"command": "cargo test"})));
723 assert!(rule.matches("BASH", &json!({"command": "cargo check"})));
724 }
725
726 #[test]
731 fn test_policy_default() {
732 let policy = PermissionPolicy::default();
733 assert!(policy.enabled);
734 assert_eq!(policy.default_decision, PermissionDecision::Ask);
735 assert!(policy.allow.is_empty());
736 assert!(policy.deny.is_empty());
737 assert!(policy.ask.is_empty());
738 }
739
740 #[test]
741 fn test_policy_permissive() {
742 let policy = PermissionPolicy::permissive();
743 assert_eq!(policy.default_decision, PermissionDecision::Allow);
744 }
745
746 #[test]
747 fn test_policy_strict() {
748 let policy = PermissionPolicy::strict();
749 assert_eq!(policy.default_decision, PermissionDecision::Ask);
750 }
751
752 #[test]
753 fn test_policy_builder() {
754 let policy = PermissionPolicy::new()
755 .allow("Bash(cargo:*)")
756 .allow("Grep(*)")
757 .deny("Bash(rm -rf:*)")
758 .ask("Write(*)");
759
760 assert_eq!(policy.allow.len(), 2);
761 assert_eq!(policy.deny.len(), 1);
762 assert_eq!(policy.ask.len(), 1);
763 }
764
765 #[test]
766 fn test_policy_check_allow() {
767 let policy = PermissionPolicy::new().allow("Bash(cargo:*)");
768
769 let decision = policy.check("Bash", &json!({"command": "cargo build"}));
770 assert_eq!(decision, PermissionDecision::Allow);
771 }
772
773 #[test]
774 fn test_policy_check_deny() {
775 let policy = PermissionPolicy::new().deny("Bash(rm -rf:*)");
776
777 let decision = policy.check("Bash", &json!({"command": "rm -rf /"}));
778 assert_eq!(decision, PermissionDecision::Deny);
779 }
780
781 #[test]
782 fn test_policy_check_ask() {
783 let policy = PermissionPolicy::new().ask("Write(*)");
784
785 let decision = policy.check("Write", &json!({"file_path": "/tmp/test.txt"}));
786 assert_eq!(decision, PermissionDecision::Ask);
787 }
788
789 #[test]
790 fn test_policy_check_default() {
791 let policy = PermissionPolicy::new();
792
793 let decision = policy.check("Unknown", &json!({}));
794 assert_eq!(decision, PermissionDecision::Ask);
795 }
796
797 #[test]
798 fn test_policy_deny_wins_over_allow() {
799 let policy = PermissionPolicy::new().allow("Bash(*)").deny("Bash(rm:*)");
800
801 let decision = policy.check("Bash", &json!({"command": "rm -rf /tmp"}));
803 assert_eq!(decision, PermissionDecision::Deny);
804
805 let decision = policy.check("Bash", &json!({"command": "ls -la"}));
807 assert_eq!(decision, PermissionDecision::Allow);
808 }
809
810 #[test]
811 fn test_policy_allow_wins_over_ask() {
812 let policy = PermissionPolicy::new()
813 .allow("Bash(cargo:*)")
814 .ask("Bash(*)");
815
816 let decision = policy.check("Bash", &json!({"command": "cargo build"}));
818 assert_eq!(decision, PermissionDecision::Allow);
819
820 let decision = policy.check("Bash", &json!({"command": "npm install"}));
822 assert_eq!(decision, PermissionDecision::Ask);
823 }
824
825 #[test]
826 fn test_policy_disabled() {
827 let mut policy = PermissionPolicy::new().deny("Bash(rm:*)").ask("Bash(*)");
828 policy.enabled = false;
829
830 let decision = policy.check("Bash", &json!({"command": "rm -rf /"}));
832 assert_eq!(decision, PermissionDecision::Allow);
833 }
834
835 #[test]
836 fn test_policy_is_allowed() {
837 let policy = PermissionPolicy::new().allow("Bash(cargo:*)");
838
839 assert!(policy.is_allowed("Bash", &json!({"command": "cargo build"})));
840 assert!(!policy.is_allowed("Bash", &json!({"command": "npm install"})));
841 }
842
843 #[test]
844 fn test_policy_is_denied() {
845 let policy = PermissionPolicy::new().deny("Bash(rm:*)");
846
847 assert!(policy.is_denied("Bash", &json!({"command": "rm -rf /"})));
848 assert!(!policy.is_denied("Bash", &json!({"command": "ls -la"})));
849 }
850
851 #[test]
852 fn test_policy_requires_confirmation() {
853 let mut policy = PermissionPolicy::new().allow("Read(*)").ask("Write(*)");
855 policy.default_decision = PermissionDecision::Deny; assert!(policy.requires_confirmation("Write", &json!({"file_path": "/tmp/test"})));
858 assert!(!policy.requires_confirmation("Read", &json!({"file_path": "/tmp/test"})));
859 }
860
861 #[test]
862 fn test_policy_matching_rules() {
863 let policy = PermissionPolicy::new()
864 .allow("Bash(cargo:*)")
865 .deny("Bash(cargo fmt:*)")
866 .ask("Bash(*)");
867
868 let matching = policy.get_matching_rules("Bash", &json!({"command": "cargo fmt"}));
869 assert_eq!(matching.deny.len(), 1);
870 assert_eq!(matching.allow.len(), 1);
871 assert_eq!(matching.ask.len(), 1);
872 }
873
874 #[test]
875 fn test_policy_allow_all() {
876 let policy =
877 PermissionPolicy::new().allow_all(&["Bash(cargo:*)", "Bash(npm:*)", "Grep(*)"]);
878
879 assert_eq!(policy.allow.len(), 3);
880 assert!(policy.is_allowed("Bash", &json!({"command": "cargo build"})));
881 assert!(policy.is_allowed("Bash", &json!({"command": "npm run test"})));
882 assert!(policy.is_allowed("Grep", &json!({"pattern": "foo"})));
883 }
884
885 #[test]
890 fn test_rule_deserialize_plain_string() {
891 let rule: PermissionRule = serde_yaml::from_str("read").unwrap();
893 assert_eq!(rule.rule, "read");
894 assert!(rule.matches("read", &json!({})));
895 assert!(!rule.matches("write", &json!({})));
896 }
897
898 #[test]
899 fn test_rule_deserialize_plain_string_with_pattern() {
900 let rule: PermissionRule = serde_yaml::from_str("\"Bash(cargo:*)\"").unwrap();
902 assert_eq!(rule.rule, "Bash(cargo:*)");
903 assert!(rule.matches("Bash", &json!({"command": "cargo build"})));
904 }
905
906 #[test]
907 fn test_rule_deserialize_struct_form() {
908 let rule: PermissionRule = serde_yaml::from_str("rule: read").unwrap();
910 assert_eq!(rule.rule, "read");
911 assert!(rule.matches("read", &json!({})));
912 }
913
914 #[test]
915 fn test_rule_deserialize_in_policy() {
916 let yaml = r#"
918allow:
919 - read
920 - "Bash(cargo:*)"
921 - rule: grep
922deny:
923 - write
924"#;
925 let policy: PermissionPolicy = serde_yaml::from_str(yaml).unwrap();
926 assert_eq!(policy.allow.len(), 3);
927 assert_eq!(policy.deny.len(), 1);
928 assert!(policy.is_allowed("read", &json!({})));
929 assert!(policy.is_allowed("Bash", &json!({"command": "cargo build"})));
930 assert!(policy.is_allowed("grep", &json!({})));
931 assert!(policy.is_denied("write", &json!({})));
932 }
933
934 #[test]
939 fn test_manager_default() {
940 let manager = PermissionManager::new();
941 assert_eq!(
942 manager.global_policy().default_decision,
943 PermissionDecision::Ask
944 );
945 }
946
947 #[test]
948 fn test_manager_with_global_policy() {
949 let policy = PermissionPolicy::permissive();
950 let manager = PermissionManager::with_global_policy(policy);
951 assert_eq!(
952 manager.global_policy().default_decision,
953 PermissionDecision::Allow
954 );
955 }
956
957 #[test]
958 fn test_manager_session_policy() {
959 let mut manager = PermissionManager::new();
960
961 let session_policy = PermissionPolicy::new().allow("Bash(cargo:*)");
962 manager.set_session_policy("session-1", session_policy);
963
964 let decision = manager.check("session-1", "Bash", &json!({"command": "cargo build"}));
966 assert_eq!(decision, PermissionDecision::Allow);
967
968 let decision = manager.check("session-2", "Bash", &json!({"command": "cargo build"}));
970 assert_eq!(decision, PermissionDecision::Ask);
971 }
972
973 #[test]
974 fn test_manager_remove_session_policy() {
975 let mut manager = PermissionManager::new();
976
977 let session_policy = PermissionPolicy::permissive();
978 manager.set_session_policy("session-1", session_policy);
979
980 let decision = manager.check("session-1", "Bash", &json!({"command": "anything"}));
982 assert_eq!(decision, PermissionDecision::Allow);
983
984 manager.remove_session_policy("session-1");
985
986 let decision = manager.check("session-1", "Bash", &json!({"command": "anything"}));
988 assert_eq!(decision, PermissionDecision::Ask);
989 }
990
991 #[test]
992 fn test_manager_global_deny_overrides_session_allow() {
993 let mut manager =
994 PermissionManager::with_global_policy(PermissionPolicy::new().deny("Bash(rm:*)"));
995
996 let session_policy = PermissionPolicy::new().allow("Bash(*)");
997 manager.set_session_policy("session-1", session_policy);
998
999 let decision = manager.check("session-1", "Bash", &json!({"command": "rm -rf /"}));
1001 assert_eq!(decision, PermissionDecision::Deny);
1002
1003 let decision = manager.check("session-1", "Bash", &json!({"command": "ls -la"}));
1005 assert_eq!(decision, PermissionDecision::Allow);
1006 }
1007
1008 #[test]
1013 fn test_realistic_dev_policy() {
1014 let policy = PermissionPolicy::new()
1015 .allow_all(&[
1017 "Bash(cargo:*)",
1018 "Bash(npm:*)",
1019 "Bash(pnpm:*)",
1020 "Bash(just:*)",
1021 "Bash(git status:*)",
1022 "Bash(git diff:*)",
1023 "Bash(echo:*)",
1024 "Grep(*)",
1025 "Glob(*)",
1026 "Ls(*)",
1027 ])
1028 .deny_all(&["Bash(rm -rf:*)", "Bash(sudo:*)", "Bash(curl | sh:*)"])
1030 .ask_all(&["Write(*)", "Edit(*)"]);
1032
1033 assert!(policy.is_allowed("Bash", &json!({"command": "cargo build"})));
1035 assert!(policy.is_allowed("Bash", &json!({"command": "npm run test"})));
1036 assert!(policy.is_allowed("Grep", &json!({"pattern": "TODO"})));
1037
1038 assert!(policy.is_denied("Bash", &json!({"command": "rm -rf /"})));
1040 assert!(policy.is_denied("Bash", &json!({"command": "sudo apt install"})));
1041
1042 assert!(policy.requires_confirmation("Write", &json!({"file_path": "/tmp/test.rs"})));
1044 assert!(policy.requires_confirmation("Edit", &json!({"file_path": "src/main.rs"})));
1045 }
1046
1047 #[test]
1048 fn test_mcp_tool_permissions() {
1049 let policy = PermissionPolicy::new()
1050 .allow("mcp__pencil")
1051 .deny("mcp__dangerous");
1052
1053 assert!(policy.is_allowed("mcp__pencil__batch_design", &json!({})));
1054 assert!(policy.is_allowed("mcp__pencil__batch_get", &json!({})));
1055 assert!(policy.is_denied("mcp__dangerous__execute", &json!({})));
1056 }
1057
1058 #[test]
1059 fn test_permissive_with_mcp_wildcard_deny() {
1060 let policy = PermissionPolicy::permissive().deny("mcp__longvt__*");
1063
1064 assert_eq!(
1066 policy.check("mcp__longvt__search", &json!({})),
1067 PermissionDecision::Deny
1068 );
1069 assert_eq!(
1070 policy.check("mcp__longvt__create_memory", &json!({})),
1071 PermissionDecision::Deny
1072 );
1073 assert_eq!(
1075 policy.check("mcp__pencil__draw", &json!({})),
1076 PermissionDecision::Allow
1077 );
1078 assert_eq!(
1079 policy.check("bash", &json!({"command": "ls"})),
1080 PermissionDecision::Allow
1081 );
1082 }
1083
1084 #[test]
1085 fn test_serialization() {
1086 let policy = PermissionPolicy::new()
1087 .allow("Bash(cargo:*)")
1088 .deny("Bash(rm:*)");
1089
1090 let json = serde_json::to_string(&policy).unwrap();
1091 let deserialized: PermissionPolicy = serde_json::from_str(&json).unwrap();
1092
1093 assert_eq!(deserialized.allow.len(), 1);
1094 assert_eq!(deserialized.deny.len(), 1);
1095 }
1096
1097 #[test]
1098 fn test_matching_rules_is_empty() {
1099 let rules = MatchingRules {
1100 deny: vec![],
1101 allow: vec![],
1102 ask: vec![],
1103 };
1104 assert!(rules.is_empty());
1105
1106 let rules = MatchingRules {
1107 deny: vec!["Bash".to_string()],
1108 allow: vec![],
1109 ask: vec![],
1110 };
1111 assert!(!rules.is_empty());
1112
1113 let rules = MatchingRules {
1114 deny: vec![],
1115 allow: vec!["Read".to_string()],
1116 ask: vec![],
1117 };
1118 assert!(!rules.is_empty());
1119
1120 let rules = MatchingRules {
1121 deny: vec![],
1122 allow: vec![],
1123 ask: vec!["Write".to_string()],
1124 };
1125 assert!(!rules.is_empty());
1126 }
1127
1128 #[test]
1129 fn test_permission_manager_default() {
1130 let pm = PermissionManager::default();
1131 let policy = pm.global_policy();
1132 assert!(policy.allow.is_empty());
1133 assert!(policy.deny.is_empty());
1134 assert!(policy.ask.is_empty());
1135 }
1136
1137 #[test]
1138 fn test_permission_manager_set_global_policy() {
1139 let mut pm = PermissionManager::new();
1140 let policy = PermissionPolicy::new().allow("Bash(*)");
1141 pm.set_global_policy(policy);
1142 assert_eq!(pm.global_policy().allow.len(), 1);
1143 }
1144
1145 #[test]
1146 fn test_permission_manager_session_policy() {
1147 let mut pm = PermissionManager::new();
1148 let policy = PermissionPolicy::new().deny("Bash(rm:*)");
1149 pm.set_session_policy("s1", policy);
1150
1151 let effective = pm.get_effective_policy("s1");
1152 assert_eq!(effective.deny.len(), 1);
1153
1154 let global = pm.get_effective_policy("s2");
1156 assert!(global.deny.is_empty());
1157 }
1158
1159 #[test]
1160 fn test_permission_manager_remove_session_policy() {
1161 let mut pm = PermissionManager::new();
1162 pm.set_session_policy("s1", PermissionPolicy::new().deny("Bash(*)"));
1163 assert_eq!(pm.get_effective_policy("s1").deny.len(), 1);
1164
1165 pm.remove_session_policy("s1");
1166 assert!(pm.get_effective_policy("s1").deny.is_empty());
1167 }
1168
1169 #[test]
1170 fn test_permission_manager_check_deny() {
1171 let mut pm = PermissionManager::new();
1172 pm.set_global_policy(PermissionPolicy::new().deny("Bash(rm:*)"));
1173
1174 let decision = pm.check("s1", "Bash", &json!({"command": "rm -rf /"}));
1175 assert_eq!(decision, PermissionDecision::Deny);
1176 }
1177
1178 #[test]
1179 fn test_permission_manager_check_allow() {
1180 let mut pm = PermissionManager::new();
1181 pm.set_global_policy(PermissionPolicy::new().allow("Bash(cargo:*)"));
1182
1183 let decision = pm.check("s1", "Bash", &json!({"command": "cargo build"}));
1184 assert_eq!(decision, PermissionDecision::Allow);
1185 }
1186
1187 #[test]
1188 fn test_permission_manager_check_session_override() {
1189 let mut pm = PermissionManager::new();
1190 pm.set_global_policy(PermissionPolicy::new().allow("Bash(*)"));
1191 pm.set_session_policy("s1", PermissionPolicy::new().deny("Bash(rm:*)"));
1192
1193 let decision = pm.check("s1", "Bash", &json!({"command": "rm -rf /"}));
1195 assert_eq!(decision, PermissionDecision::Deny);
1196
1197 let decision = pm.check("s2", "Bash", &json!({"command": "rm -rf /"}));
1199 assert_eq!(decision, PermissionDecision::Allow);
1200 }
1201
1202 #[test]
1203 fn test_permission_manager_with_global_policy() {
1204 let policy = PermissionPolicy::new().allow("Read(*)").deny("Write(*)");
1205 let pm = PermissionManager::with_global_policy(policy);
1206 assert_eq!(pm.global_policy().allow.len(), 1);
1207 assert_eq!(pm.global_policy().deny.len(), 1);
1208 }
1209}