1use futures::future::BoxFuture;
12use serde::{Deserialize, Serialize};
13use std::collections::{HashMap, HashSet};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub enum ToolPermission {
20 Read,
22 Write,
24 Network,
26 Execute,
28 Sensitive,
30}
31
32impl std::fmt::Display for ToolPermission {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 match self {
35 ToolPermission::Read => write!(f, "read"),
36 ToolPermission::Write => write!(f, "write"),
37 ToolPermission::Network => write!(f, "network"),
38 ToolPermission::Execute => write!(f, "execute"),
39 ToolPermission::Sensitive => write!(f, "sensitive"),
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
57#[serde(rename_all = "lowercase")]
58pub enum PermissionMode {
59 #[default]
61 Default,
62 Plan,
64 AcceptEdits,
66 BypassPermissions,
68 Auto,
70 Bubble,
72 DontAsk,
77}
78
79impl PermissionMode {
80 pub fn allows_write(&self) -> bool {
82 match self {
83 PermissionMode::BypassPermissions => true,
84 PermissionMode::AcceptEdits => true,
85 PermissionMode::DontAsk => true, PermissionMode::Plan => false,
87 _ => false,
88 }
89 }
90
91 pub fn requires_interaction(&self) -> bool {
93 match self {
94 PermissionMode::BypassPermissions => false,
95 PermissionMode::Auto => false,
96 PermissionMode::DontAsk => false, PermissionMode::AcceptEdits => false, _ => true,
99 }
100 }
101
102 pub fn uses_classifier(&self) -> bool {
104 matches!(self, PermissionMode::Auto)
105 }
106}
107
108impl std::fmt::Display for PermissionMode {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 match self {
111 PermissionMode::Default => write!(f, "default"),
112 PermissionMode::Plan => write!(f, "plan"),
113 PermissionMode::AcceptEdits => write!(f, "acceptEdits"),
114 PermissionMode::BypassPermissions => write!(f, "bypassPermissions"),
115 PermissionMode::Auto => write!(f, "auto"),
116 PermissionMode::Bubble => write!(f, "bubble"),
117 PermissionMode::DontAsk => write!(f, "dontAsk"),
118 }
119 }
120}
121
122#[derive(Debug, Clone, PartialEq)]
126pub enum PermissionDecision {
127 Allow,
129 Deny {
131 reason: String,
133 },
134 RequireApproval,
136 Ask {
138 suggestions: Vec<String>,
140 },
141}
142
143impl PermissionDecision {
144 pub fn is_allowed(&self) -> bool {
146 matches!(self, PermissionDecision::Allow)
147 }
148
149 pub fn is_denied(&self) -> bool {
151 matches!(self, PermissionDecision::Deny { .. })
152 }
153
154 pub fn requires_approval(&self) -> bool {
156 matches!(
157 self,
158 PermissionDecision::RequireApproval | PermissionDecision::Ask { .. }
159 )
160 }
161}
162
163#[derive(
172 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
173)]
174#[serde(rename_all = "lowercase")]
175pub enum RuleSource {
176 #[default]
178 Default = 0,
179 LocalSettings = 1,
181 ProjectSettings = 2,
183 UserSettings = 3,
185 Managed = 4,
187 CliArg = 5,
189 Session = 6,
191}
192
193impl std::fmt::Display for RuleSource {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 match self {
196 RuleSource::Default => write!(f, "default"),
197 RuleSource::LocalSettings => write!(f, "localSettings"),
198 RuleSource::ProjectSettings => write!(f, "projectSettings"),
199 RuleSource::UserSettings => write!(f, "userSettings"),
200 RuleSource::Managed => write!(f, "managed"),
201 RuleSource::CliArg => write!(f, "cliArg"),
202 RuleSource::Session => write!(f, "session"),
203 }
204 }
205}
206
207#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
211#[serde(tag = "type", rename_all = "lowercase")]
212pub enum RuleMatcher {
213 Tool { name: String },
215 Pattern { pattern: String },
217 Permission { permission: ToolPermission },
219 All,
221}
222
223impl RuleMatcher {
224 pub fn matches_matcher_str(&self, matcher_str: &str) -> bool {
230 match self {
231 RuleMatcher::Tool { name } => name == matcher_str,
232 RuleMatcher::Pattern { pattern } => pattern == matcher_str,
233 RuleMatcher::Permission { .. } => false,
234 RuleMatcher::All => matcher_str == "*" || matcher_str == "all",
235 }
236 }
237
238 pub fn matches(&self, tool_name: &str, permissions: &[ToolPermission]) -> bool {
240 match self {
241 RuleMatcher::Tool { name } => tool_name == name,
242 RuleMatcher::Pattern { pattern } => {
243 if pattern == "*" {
244 return true;
245 }
246 if tool_name == pattern {
248 return true;
249 }
250 #[cfg(feature = "permission")]
253 {
254 if let Ok(glob) = globset::Glob::new(pattern) {
255 let matcher = glob.compile_matcher();
256 if matcher.is_match(tool_name) {
257 return true;
258 }
259 }
260 }
261 if pattern.ends_with("*)") {
264 let prefix = &pattern[..pattern.len() - 2];
265 if tool_name.starts_with(prefix) {
266 return true;
267 }
268 }
269 if tool_name.starts_with(pattern)
271 && tool_name.len() > pattern.len()
272 && tool_name.as_bytes()[pattern.len()] == b'('
273 {
274 return true;
275 }
276 false
277 }
278 RuleMatcher::Permission { permission } => permissions.contains(permission),
279 RuleMatcher::All => true,
280 }
281 }
282}
283
284impl std::fmt::Display for RuleMatcher {
285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286 match self {
287 RuleMatcher::Tool { name } => write!(f, "tool:{}", name),
288 RuleMatcher::Pattern { pattern } => write!(f, "pattern:{}", pattern),
289 RuleMatcher::Permission { permission } => write!(f, "permission:{}", permission),
290 RuleMatcher::All => write!(f, "all"),
291 }
292 }
293}
294
295#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
299#[serde(tag = "type", rename_all = "lowercase")]
300pub enum RuleBehavior {
301 Allow,
303 Deny { reason: String },
305 Ask { suggestions: Vec<String> },
307}
308
309impl RuleBehavior {
310 pub fn to_decision(&self) -> PermissionDecision {
312 match self {
313 RuleBehavior::Allow => PermissionDecision::Allow,
314 RuleBehavior::Deny { reason } => PermissionDecision::Deny {
315 reason: reason.clone(),
316 },
317 RuleBehavior::Ask { suggestions } => PermissionDecision::Ask {
318 suggestions: suggestions.clone(),
319 },
320 }
321 }
322}
323
324#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
328pub struct PermissionRule {
329 pub matcher: RuleMatcher,
331 pub behavior: RuleBehavior,
333 pub source: RuleSource,
335 #[serde(default)]
337 pub description: Option<String>,
338}
339
340impl PermissionRule {
341 pub fn allow(matcher: RuleMatcher, source: RuleSource) -> Self {
343 Self {
344 matcher,
345 behavior: RuleBehavior::Allow,
346 source,
347 description: None,
348 }
349 }
350
351 pub fn deny(matcher: RuleMatcher, reason: String, source: RuleSource) -> Self {
353 Self {
354 matcher,
355 behavior: RuleBehavior::Deny { reason },
356 source,
357 description: None,
358 }
359 }
360
361 pub fn ask(matcher: RuleMatcher, suggestions: Vec<String>, source: RuleSource) -> Self {
363 Self {
364 matcher,
365 behavior: RuleBehavior::Ask { suggestions },
366 source,
367 description: None,
368 }
369 }
370
371 pub fn matches(&self, tool_name: &str, permissions: &[ToolPermission]) -> bool {
373 self.matcher.matches(tool_name, permissions)
374 }
375}
376
377#[derive(Debug, Clone, Default)]
386pub struct RuleRegistry {
387 rules: Vec<PermissionRule>,
388 tool_index: HashMap<String, Vec<usize>>,
390}
391
392impl RuleRegistry {
393 pub fn new() -> Self {
395 Self::default()
396 }
397
398 pub fn add_rule(&mut self, rule: PermissionRule) {
400 let pos = self
402 .rules
403 .iter()
404 .position(|r| r.source < rule.source)
405 .unwrap_or(self.rules.len());
406
407 self.rules.insert(pos, rule);
408
409 self.rebuild_tool_index();
411 }
412
413 pub fn add_rules(&mut self, rules: Vec<PermissionRule>) {
415 for rule in rules {
416 self.add_rule(rule);
417 }
418 }
419
420 pub fn check(&self, tool_name: &str, permissions: &[ToolPermission]) -> Option<RuleBehavior> {
430 for rule in &self.rules {
432 if matches!(rule.behavior, RuleBehavior::Deny { .. })
433 && rule.matches(tool_name, permissions)
434 {
435 return Some(rule.behavior.clone());
436 }
437 }
438 for rule in &self.rules {
440 if matches!(rule.behavior, RuleBehavior::Ask { .. })
441 && rule.matches(tool_name, permissions)
442 {
443 return Some(rule.behavior.clone());
444 }
445 }
446 for rule in &self.rules {
448 if matches!(rule.behavior, RuleBehavior::Allow) && rule.matches(tool_name, permissions)
449 {
450 return Some(rule.behavior.clone());
451 }
452 }
453 None
454 }
455
456 pub fn rules_by_source(&self, source: RuleSource) -> Vec<&PermissionRule> {
458 self.rules.iter().filter(|r| r.source == source).collect()
459 }
460
461 pub fn remove_by_source(&mut self, source: RuleSource) {
463 self.rules.retain(|r| r.source != source);
464 self.rebuild_tool_index();
465 }
466
467 pub fn remove_by_matcher(&mut self, matcher_str: &str) -> usize {
472 let before = self.rules.len();
473 self.rules
474 .retain(|r| !r.matcher.matches_matcher_str(matcher_str));
475 let removed = before - self.rules.len();
476 if removed > 0 {
477 self.rebuild_tool_index();
478 }
479 removed
480 }
481
482 fn rebuild_tool_index(&mut self) {
484 self.tool_index.clear();
485 for (i, rule) in self.rules.iter().enumerate() {
486 if let RuleMatcher::Tool { name } = &rule.matcher {
487 self.tool_index.entry(name.clone()).or_default().push(i);
488 }
489 }
490 }
491
492 pub fn clear(&mut self) {
494 self.rules.clear();
495 self.tool_index.clear();
496 }
497
498 pub fn len(&self) -> usize {
500 self.rules.len()
501 }
502
503 pub fn is_empty(&self) -> bool {
505 self.rules.is_empty()
506 }
507
508 pub fn all_rules(&self) -> &[PermissionRule] {
510 &self.rules
511 }
512}
513
514pub trait PermissionPolicy: Send + Sync {
518 fn check<'a>(
519 &'a self,
520 tool_name: &'a str,
521 permissions: &'a [ToolPermission],
522 ) -> BoxFuture<'a, PermissionDecision>;
523}
524
525pub struct DefaultPermissionPolicy {
527 granted: HashSet<ToolPermission>,
528 approval_required: HashSet<ToolPermission>,
529}
530
531impl Default for DefaultPermissionPolicy {
532 fn default() -> Self {
533 Self::new()
534 }
535}
536
537impl DefaultPermissionPolicy {
538 pub fn new() -> Self {
539 let mut approval_required = HashSet::new();
540 approval_required.insert(ToolPermission::Execute);
541 approval_required.insert(ToolPermission::Sensitive);
542
543 Self {
544 granted: HashSet::new(),
545 approval_required,
546 }
547 }
548
549 pub fn grant(mut self, perm: ToolPermission) -> Self {
550 self.granted.insert(perm);
551 self.approval_required.remove(&perm);
552 self
553 }
554
555 pub fn require_approval(mut self, perm: ToolPermission) -> Self {
556 self.approval_required.insert(perm);
557 self.granted.remove(&perm);
558 self
559 }
560
561 pub fn grant_all(mut self) -> Self {
562 self.granted.insert(ToolPermission::Read);
563 self.granted.insert(ToolPermission::Write);
564 self.granted.insert(ToolPermission::Network);
565 self.granted.insert(ToolPermission::Execute);
566 self.granted.insert(ToolPermission::Sensitive);
567 self.approval_required.clear();
568 self
569 }
570}
571
572impl PermissionPolicy for DefaultPermissionPolicy {
573 fn check<'a>(
574 &'a self,
575 _tool_name: &'a str,
576 permissions: &'a [ToolPermission],
577 ) -> BoxFuture<'a, PermissionDecision> {
578 Box::pin(async move {
579 if permissions.is_empty() {
580 return PermissionDecision::Allow;
581 }
582
583 let mut need_approval = Vec::new();
584 let mut denied = Vec::new();
585
586 for perm in permissions {
587 if self.granted.contains(perm) {
588 continue;
589 }
590 if self.approval_required.contains(perm) {
591 need_approval.push(*perm);
592 } else {
593 denied.push(*perm);
594 }
595 }
596
597 if !denied.is_empty() {
598 let names: Vec<String> = denied.iter().map(|p| p.to_string()).collect();
599 return PermissionDecision::Deny {
600 reason: format!("Unauthorized permissions: {}", names.join(", ")),
601 };
602 }
603
604 if !need_approval.is_empty() {
605 return PermissionDecision::RequireApproval;
606 }
607
608 PermissionDecision::Allow
609 })
610 }
611}
612
613#[cfg(test)]
616mod tests {
617 use super::*;
618
619 #[test]
620 fn test_permission_mode_default() {
621 let mode = PermissionMode::default();
622 assert_eq!(mode, PermissionMode::Default);
623 assert!(mode.requires_interaction());
624 assert!(!mode.uses_classifier());
625 }
626
627 #[test]
628 fn test_permission_mode_bypass() {
629 let mode = PermissionMode::BypassPermissions;
630 assert!(mode.allows_write());
631 assert!(!mode.requires_interaction());
632 }
633
634 #[test]
635 fn test_permission_mode_plan() {
636 let mode = PermissionMode::Plan;
637 assert!(!mode.allows_write());
638 assert!(mode.requires_interaction());
639 }
640
641 #[test]
642 fn test_permission_mode_auto() {
643 let mode = PermissionMode::Auto;
644 assert!(!mode.requires_interaction());
645 assert!(mode.uses_classifier());
646 }
647
648 #[test]
649 fn test_rule_source_ordering() {
650 assert!(RuleSource::Session > RuleSource::CliArg);
651 assert!(RuleSource::CliArg > RuleSource::UserSettings);
652 assert!(RuleSource::UserSettings > RuleSource::ProjectSettings);
653 assert!(RuleSource::ProjectSettings > RuleSource::LocalSettings);
654 assert!(RuleSource::LocalSettings > RuleSource::Default);
655 }
656
657 #[test]
658 fn test_rule_matcher_tool() {
659 let matcher = RuleMatcher::Tool {
660 name: "Bash".to_string(),
661 };
662 assert!(matcher.matches("Bash", &[]));
663 assert!(!matcher.matches("Read", &[]));
664 }
665
666 #[test]
667 fn test_rule_matcher_pattern() {
668 let matcher = RuleMatcher::Pattern {
669 pattern: "Bash".to_string(),
670 };
671 assert!(matcher.matches("Bash", &[]));
672 assert!(matcher.matches("Bash(git:*)", &[]));
673 assert!(!matcher.matches("BashExtra", &[]));
674 }
675
676 #[test]
677 fn test_rule_matcher_wildcard() {
678 let matcher = RuleMatcher::Pattern {
679 pattern: "*".to_string(),
680 };
681 assert!(matcher.matches("Bash", &[]));
682 assert!(matcher.matches("Read", &[]));
683 assert!(matcher.matches("Write", &[]));
684 }
685
686 #[test]
687 fn test_rule_matcher_permission() {
688 let matcher = RuleMatcher::Permission {
689 permission: ToolPermission::Execute,
690 };
691 assert!(matcher.matches("shell", &[ToolPermission::Execute]));
692 assert!(!matcher.matches("read", &[ToolPermission::Read]));
693 }
694
695 #[test]
696 fn test_permission_rule_create() {
697 let rule = PermissionRule::allow(
698 RuleMatcher::Tool {
699 name: "Read".to_string(),
700 },
701 RuleSource::UserSettings,
702 );
703 assert_eq!(rule.behavior, RuleBehavior::Allow);
704 assert_eq!(rule.source, RuleSource::UserSettings);
705 }
706
707 #[test]
708 fn test_rule_registry_add() {
709 let mut registry = RuleRegistry::new();
710
711 registry.add_rule(PermissionRule::deny(
713 RuleMatcher::All,
714 "default deny".to_string(),
715 RuleSource::Default,
716 ));
717
718 registry.add_rule(PermissionRule::allow(
720 RuleMatcher::Tool {
721 name: "Read".to_string(),
722 },
723 RuleSource::UserSettings,
724 ));
725
726 assert_eq!(registry.rules[0].source, RuleSource::UserSettings);
728 assert_eq!(registry.rules[1].source, RuleSource::Default);
729 }
730
731 #[test]
732 fn test_rule_registry_check() {
733 let mut registry = RuleRegistry::new();
734
735 registry.add_rule(PermissionRule::deny(
737 RuleMatcher::All,
738 "default deny".to_string(),
739 RuleSource::Default,
740 ));
741
742 registry.add_rule(PermissionRule::allow(
744 RuleMatcher::Tool {
745 name: "Read".to_string(),
746 },
747 RuleSource::UserSettings,
748 ));
749
750 let result = registry.check("Read", &[]);
753 assert_eq!(
754 result,
755 Some(RuleBehavior::Deny {
756 reason: "default deny".to_string()
757 })
758 );
759
760 let result = registry.check("Bash", &[]);
762 assert!(matches!(result, Some(RuleBehavior::Deny { .. })));
763 }
764
765 #[test]
766 fn test_rule_registry_allow_without_deny() {
767 let mut registry = RuleRegistry::new();
768
769 registry.add_rule(PermissionRule::allow(
771 RuleMatcher::Tool {
772 name: "Read".to_string(),
773 },
774 RuleSource::UserSettings,
775 ));
776
777 let result = registry.check("Read", &[]);
779 assert_eq!(result, Some(RuleBehavior::Allow));
780
781 let result = registry.check("Bash", &[]);
783 assert_eq!(result, None);
784 }
785
786 #[test]
787 fn test_rule_registry_deny_first_ordering() {
788 let mut registry = RuleRegistry::new();
789
790 registry.add_rule(PermissionRule::allow(
792 RuleMatcher::Pattern {
793 pattern: "Bash".to_string(),
794 },
795 RuleSource::UserSettings,
796 ));
797 registry.add_rule(PermissionRule::deny(
799 RuleMatcher::Pattern {
800 pattern: "Bash(rm:*)".to_string(),
801 },
802 "dangerous".to_string(),
803 RuleSource::Default,
804 ));
805
806 let result = registry.check("Bash(rm:rf)", &[]);
808 assert!(matches!(result, Some(RuleBehavior::Deny { .. })));
809
810 let result = registry.check("Bash(ls)", &[]);
812 assert_eq!(result, Some(RuleBehavior::Allow));
813 }
814
815 #[test]
816 fn test_rule_registry_ask_between_deny_and_allow() {
817 let mut registry = RuleRegistry::new();
818
819 registry.add_rule(PermissionRule::allow(
820 RuleMatcher::Pattern {
821 pattern: "Bash".to_string(),
822 },
823 RuleSource::UserSettings,
824 ));
825 registry.add_rule(PermissionRule::ask(
826 RuleMatcher::Pattern {
827 pattern: "Bash(rm:*)".to_string(),
828 },
829 vec!["Confirm".to_string()],
830 RuleSource::Default,
831 ));
832
833 let result = registry.check("Bash(rm:rf)", &[]);
835 assert!(matches!(result, Some(RuleBehavior::Ask { .. })));
836
837 let result = registry.check("Bash(git:status)", &[]);
839 assert_eq!(result, Some(RuleBehavior::Allow));
840 }
841
842 #[test]
843 fn test_permission_decision_is_allowed() {
844 assert!(PermissionDecision::Allow.is_allowed());
845 assert!(
846 !PermissionDecision::Deny {
847 reason: "test".to_string()
848 }
849 .is_allowed()
850 );
851 }
852
853 #[test]
854 fn test_permission_decision_requires_approval() {
855 assert!(PermissionDecision::RequireApproval.requires_approval());
856 assert!(
857 PermissionDecision::Ask {
858 suggestions: vec!["yes".to_string()]
859 }
860 .requires_approval()
861 );
862 assert!(!PermissionDecision::Allow.requires_approval());
863 }
864
865 #[tokio::test]
866 async fn test_empty_permissions_allowed() {
867 let policy = DefaultPermissionPolicy::new();
868 let decision = policy.check("tool", &[]).await;
869 assert!(matches!(decision, PermissionDecision::Allow));
870 }
871
872 #[tokio::test]
873 async fn test_granted_permission() {
874 let policy = DefaultPermissionPolicy::new().grant(ToolPermission::Read);
875 let decision = policy.check("tool", &[ToolPermission::Read]).await;
876 assert!(matches!(decision, PermissionDecision::Allow));
877 }
878
879 #[tokio::test]
880 async fn test_execute_requires_approval() {
881 let policy = DefaultPermissionPolicy::new();
882 let decision = policy.check("tool", &[ToolPermission::Execute]).await;
883 assert!(matches!(decision, PermissionDecision::RequireApproval));
884 }
885
886 #[tokio::test]
887 async fn test_ungranted_denied() {
888 let policy = DefaultPermissionPolicy::new();
889 let decision = policy.check("tool", &[ToolPermission::Write]).await;
890 assert!(matches!(decision, PermissionDecision::Deny { .. }));
891 }
892
893 #[tokio::test]
894 async fn test_grant_all() {
895 let policy = DefaultPermissionPolicy::new().grant_all();
896 let decision = policy
897 .check(
898 "tool",
899 &[
900 ToolPermission::Read,
901 ToolPermission::Write,
902 ToolPermission::Execute,
903 ToolPermission::Sensitive,
904 ],
905 )
906 .await;
907 assert!(matches!(decision, PermissionDecision::Allow));
908 }
909}