Skip to main content

echo_core/tools/
permission.rs

1//! Tool permission model
2//!
3//! Provides a multi-level permission control system:
4//! - PermissionMode: permission mode (default/plan/auto/bypass etc.)
5//! - PermissionRule: rule system (allow/deny/ask)
6//! - RuleSource: rule source priority
7//! - RuleRegistry: rule registry
8//!
9//! Referenced from Claude Code's permission architecture design
10
11use futures::future::BoxFuture;
12use serde::{Deserialize, Serialize};
13use std::collections::{HashMap, HashSet};
14
15// ── Tool Permission Types ──────────────────────────────────────────────────────
16
17/// Tool permission types
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub enum ToolPermission {
20    /// Read files/directories permission
21    Read,
22    /// Write files/directories permission
23    Write,
24    /// Network access permission
25    Network,
26    /// Execute commands/code permission
27    Execute,
28    /// Sensitive operation permission (e.g. access keys, environment variables, etc.)
29    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// ── Permission Mode ────────────────────────────────────────────────────────────
45
46/// Permission mode - controls permission check behavior
47///
48/// Referenced from Claude Code's PermissionMode design:
49/// - Default: require user confirmation for dangerous operations
50/// - Plan: read-only mode
51/// - AcceptEdits: automatically accept edits
52/// - BypassPermissions: bypass all checks (can be disabled by bypass_disabled)
53/// - Auto: AI classifier auto-decides
54/// - Bubble: sub-agent permissions bubble up
55/// - DontAsk: silently reject tools not matching an allow rule (no user prompt)
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
57#[serde(rename_all = "lowercase")]
58pub enum PermissionMode {
59    /// Default mode: require user confirmation for dangerous operations
60    #[default]
61    Default,
62    /// Plan mode: read-only, disallow writes and executes
63    Plan,
64    /// Automatically accept file edit operations
65    AcceptEdits,
66    /// Bypass all permission checks (use with caution)
67    BypassPermissions,
68    /// AI classifier auto-decides (requires Classifier implementation)
69    Auto,
70    /// Sub-agent permissions bubble up to parent process
71    Bubble,
72    /// Silent mode: auto-allow rules that match, silently reject those that don't
73    ///
74    /// An intermediate mode between Default and BypassPermissions,
75    /// suitable for CI/CD and other unattended execution scenarios.
76    DontAsk,
77}
78
79impl PermissionMode {
80    /// 检查当前模式是否允许写入操作
81    pub fn allows_write(&self) -> bool {
82        match self {
83            PermissionMode::BypassPermissions => true,
84            PermissionMode::AcceptEdits => true,
85            PermissionMode::DontAsk => true, // allows write operations in rules
86            PermissionMode::Plan => false,
87            _ => false,
88        }
89    }
90
91    /// 检查当前模式是否需要用户交互
92    pub fn requires_interaction(&self) -> bool {
93        match self {
94            PermissionMode::BypassPermissions => false,
95            PermissionMode::Auto => false,
96            PermissionMode::DontAsk => false, // silently reject, no interaction
97            PermissionMode::AcceptEdits => false, // edits auto-accepted, others still need confirmation
98            _ => true,
99        }
100    }
101
102    /// 检查当前模式是否使用分类器
103    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// ── Permission Decision ────────────────────────────────────────────────────────
123
124/// Permission decision
125#[derive(Debug, Clone, PartialEq)]
126pub enum PermissionDecision {
127    /// Allow execution
128    Allow,
129    /// Deny execution
130    Deny {
131        /// Denial reason
132        reason: String,
133    },
134    /// Require user approval
135    RequireApproval,
136    /// Require user approval with suggestions
137    Ask {
138        /// Suggestion list
139        suggestions: Vec<String>,
140    },
141}
142
143impl PermissionDecision {
144    /// 检查是否为允许决策
145    pub fn is_allowed(&self) -> bool {
146        matches!(self, PermissionDecision::Allow)
147    }
148
149    /// 检查是否为拒绝决策
150    pub fn is_denied(&self) -> bool {
151        matches!(self, PermissionDecision::Deny { .. })
152    }
153
154    /// 检查是否需要用户审批
155    pub fn requires_approval(&self) -> bool {
156        matches!(
157            self,
158            PermissionDecision::RequireApproval | PermissionDecision::Ask { .. }
159        )
160    }
161}
162
163// ── Rule Source Priority ───────────────────────────────────────────────────────
164
165/// Rule source priority (higher value = higher priority)
166///
167/// Referenced from Claude Code's PERMISSION_RULE_SOURCES design.
168/// In deny-first evaluation, source priority only affects ordering among
169/// rules of the same type (deny/ask/allow); deny rules always take
170/// precedence over ask and allow rules.
171#[derive(
172    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
173)]
174#[serde(rename_all = "lowercase")]
175pub enum RuleSource {
176    /// Default rule (lowest priority)
177    #[default]
178    Default = 0,
179    /// Local settings (.echo/settings.local.json)
180    LocalSettings = 1,
181    /// Project settings (.echo/settings.json)
182    ProjectSettings = 2,
183    /// User settings (~/.echo/settings.json)
184    UserSettings = 3,
185    /// Admin policy (cannot be overridden by users, for enterprise deployment)
186    Managed = 4,
187    /// CLI argument
188    CliArg = 5,
189    /// Session temporary rule (highest priority)
190    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// ── Rule Matcher ───────────────────────────────────────────────────────────────
208
209/// Rule matcher - defines how a rule matches tool invocations
210#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
211#[serde(tag = "type", rename_all = "lowercase")]
212pub enum RuleMatcher {
213    /// Exact tool name match
214    Tool { name: String },
215    /// Wildcard pattern match (supports "Bash(git:*)" etc.)
216    Pattern { pattern: String },
217    /// Match by permission type
218    Permission { permission: ToolPermission },
219    /// Match all tools
220    All,
221}
222
223impl RuleMatcher {
224    /// Check whether a matcher string matches this matcher (for rule removal)
225    ///
226    /// Consistent with the `RuleMatcher::Pattern` construction semantics in
227    /// `parse_rule()`: uses the same matcher string for removal as for addition
228    /// to locate rules.
229    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    /// Check whether this matches the specified tool
239    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                // Exact match first
247                if tool_name == pattern {
248                    return true;
249                }
250                // Use glob matching for patterns like "Bash(rm:*)"
251                // Only return true on glob match; fall through to prefix check on non-match.
252                #[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                // Fallback: handle "prefix*)" patterns without globset.
262                // E.g., "Bash(rm:*)" matches "Bash(rm:rf)".
263                if pattern.ends_with("*)") {
264                    let prefix = &pattern[..pattern.len() - 2];
265                    if tool_name.starts_with(prefix) {
266                        return true;
267                    }
268                }
269                // Fallback: prefix match for "Bash" matching "Bash(git:*)"
270                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// ── Rule Behavior ──────────────────────────────────────────────────────────────
296
297/// Rule behavior - the action taken after a match
298#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
299#[serde(tag = "type", rename_all = "lowercase")]
300pub enum RuleBehavior {
301    /// Allow execution
302    Allow,
303    /// Deny execution
304    Deny { reason: String },
305    /// Require user confirmation
306    Ask { suggestions: Vec<String> },
307}
308
309impl RuleBehavior {
310    /// Convert to PermissionDecision
311    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// ── Permission Rule ────────────────────────────────────────────────────────────
325
326/// Permission rule - complete definition of a single rule
327#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
328pub struct PermissionRule {
329    /// Rule matcher
330    pub matcher: RuleMatcher,
331    /// Rule behavior
332    pub behavior: RuleBehavior,
333    /// Rule source
334    pub source: RuleSource,
335    /// Rule description (optional)
336    #[serde(default)]
337    pub description: Option<String>,
338}
339
340impl PermissionRule {
341    /// Create an allow rule
342    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    /// Create a deny rule
352    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    /// Create an ask rule
362    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    /// Check whether this matches the specified tool invocation
372    pub fn matches(&self, tool_name: &str, permissions: &[ToolPermission]) -> bool {
373        self.matcher.matches(tool_name, permissions)
374    }
375}
376
377// ── Rule Registry ──────────────────────────────────────────────────────────────
378
379/// Rule registry - manages all permission rules
380///
381/// Rules are sorted by source priority; higher priority rules match first.
382/// Matching order:
383/// 1. By source priority from high to low
384/// 2. Within the same source, by insertion order
385#[derive(Debug, Clone, Default)]
386pub struct RuleRegistry {
387    rules: Vec<PermissionRule>,
388    /// Index for fast exact tool name lookups: tool_name -> list of rule indices
389    tool_index: HashMap<String, Vec<usize>>,
390}
391
392impl RuleRegistry {
393    /// Create an empty rule registry
394    pub fn new() -> Self {
395        Self::default()
396    }
397
398    /// Add a rule (auto-sorted by priority)
399    pub fn add_rule(&mut self, rule: PermissionRule) {
400        // 按来源优先级插入排序
401        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        // Rebuild tool_index after insertion so all indices are correct
410        self.rebuild_tool_index();
411    }
412
413    /// Batch add rules
414    pub fn add_rules(&mut self, rules: Vec<PermissionRule>) {
415        for rule in rules {
416            self.add_rule(rule);
417        }
418    }
419
420    /// Check a tool invocation and return the matching rule behavior
421    ///
422    /// Evaluation order follows deny-first principle (referenced from Claude Code):
423    /// 1. Deny rules — any deny from any source takes precedence over all allows
424    /// 2. Ask rules — by source priority
425    /// 3. Allow rules — by source priority
426    ///
427    /// This ensures that a low-priority deny rule can never be overridden by a
428    /// high-priority allow rule.
429    pub fn check(&self, tool_name: &str, permissions: &[ToolPermission]) -> Option<RuleBehavior> {
430        // Pass 1: Deny — any deny anywhere wins (full scan)
431        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        // Pass 2: Ask — by source priority (rules are already sorted by source)
439        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        // Pass 3: Allow — by source priority
447        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    /// Get all rules from the specified source
457    pub fn rules_by_source(&self, source: RuleSource) -> Vec<&PermissionRule> {
458        self.rules.iter().filter(|r| r.source == source).collect()
459    }
460
461    /// Remove all rules from the specified source
462    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    /// Remove all rules matching the specified matcher string
468    ///
469    /// Returns the number of removed rules. Matches with the same semantics as
470    /// `parse_rule()`'s `AddRule`.
471    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    /// Rebuild tool_index (called after rule removal)
483    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    /// Clear all rules
493    pub fn clear(&mut self) {
494        self.rules.clear();
495        self.tool_index.clear();
496    }
497
498    /// Get the number of rules
499    pub fn len(&self) -> usize {
500        self.rules.len()
501    }
502
503    /// Check whether it is empty
504    pub fn is_empty(&self) -> bool {
505        self.rules.is_empty()
506    }
507
508    /// Get all rules
509    pub fn all_rules(&self) -> &[PermissionRule] {
510        &self.rules
511    }
512}
513
514// ── Permission Policy Trait ────────────────────────────────────────────────────
515
516/// Permission policy trait
517pub 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
525/// Default permission policy (retains backward compatibility)
526pub 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// ── Unit Tests ─────────────────────────────────────────────────────────────────
614
615#[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        // 添加低优先级规则
712        registry.add_rule(PermissionRule::deny(
713            RuleMatcher::All,
714            "default deny".to_string(),
715            RuleSource::Default,
716        ));
717
718        // 添加高优先级规则
719        registry.add_rule(PermissionRule::allow(
720            RuleMatcher::Tool {
721                name: "Read".to_string(),
722            },
723            RuleSource::UserSettings,
724        ));
725
726        // 高优先级规则应该在前面
727        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        // 默认拒绝所有
736        registry.add_rule(PermissionRule::deny(
737            RuleMatcher::All,
738            "default deny".to_string(),
739            RuleSource::Default,
740        ));
741
742        // 用户设置允许 Read
743        registry.add_rule(PermissionRule::allow(
744            RuleMatcher::Tool {
745                name: "Read".to_string(),
746            },
747            RuleSource::UserSettings,
748        ));
749
750        // deny-first: 即使有高优先级 Allow,Deny All 仍然匹配
751        // Read 匹配 Deny(All) → 被拒绝
752        let result = registry.check("Read", &[]);
753        assert_eq!(
754            result,
755            Some(RuleBehavior::Deny {
756                reason: "default deny".to_string()
757            })
758        );
759
760        // Bash 也匹配 Deny(All) → 被拒绝
761        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        // 只有 allow 规则
770        registry.add_rule(PermissionRule::allow(
771            RuleMatcher::Tool {
772                name: "Read".to_string(),
773            },
774            RuleSource::UserSettings,
775        ));
776
777        // Read 匹配 Allow → 允许
778        let result = registry.check("Read", &[]);
779        assert_eq!(result, Some(RuleBehavior::Allow));
780
781        // Bash 无匹配规则
782        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        // 高优先级 allow
791        registry.add_rule(PermissionRule::allow(
792            RuleMatcher::Pattern {
793                pattern: "Bash".to_string(),
794            },
795            RuleSource::UserSettings,
796        ));
797        // 低优先级 deny
798        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        // Bash(rm:rf) — deny 匹配,deny 优先于 allow
807        let result = registry.check("Bash(rm:rf)", &[]);
808        assert!(matches!(result, Some(RuleBehavior::Deny { .. })));
809
810        // Bash(ls) — 只有 allow 匹配
811        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        // Bash(rm:rf) — Ask 规则匹配,优先于 Allow
834        let result = registry.check("Bash(rm:rf)", &[]);
835        assert!(matches!(result, Some(RuleBehavior::Ask { .. })));
836
837        // Bash(git:status) — 只有 Allow 规则匹配
838        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}