Skip to main content

katu_core/
permission.rs

1//! # katu_core::permission
2//!
3//! ## 职责
4//! 定义权限系统的类型与规则引擎 — 控制工具执行的授权机制。
5//!
6//! ## 设计
7//! 参考三个参考实现的取舍:
8//! - **Claude-Code 的层次化规则** — 多来源规则 + 优先级(简化为 5 层)
9//! - **OpenCode 的通配符匹配** — 模式匹配规则引擎(采纳 last-match-wins 语义)
10//! - **Oh-My-Pi 的简洁性** — 会话级缓存 + 4 种用户回复
11//!
12//! ## 权限检查管线
13//! ```text
14//! ┌─────────────────────────────────────────────────────────────────────┐
15//! │                        Agent Loop                                    │
16//! │                                                                      │
17//! │  1. 规则引擎求值 (Ruleset::evaluate)                                  │
18//! │     Policy deny → 立即 Deny (不可覆盖)                               │
19//! │     其他 deny  → Deny                                                │
20//! │                                                                      │
21//! │  2. Tool.check_permissions() (工具级检查)                             │
22//! │     deny → Deny                                                      │
23//! │     ask  → 继续                                                      │
24//! │                                                                      │
25//! │  3. 安全检查 (SafetyCheck)                                           │
26//! │     敏感路径/危险操作 → Ask                                           │
27//! │                                                                      │
28//! │  4. 会话缓存 (SessionCache)                                          │
29//! │     always_allow → Allow                                             │
30//! │     always_deny  → Deny                                              │
31//! │                                                                      │
32//! │  5. 模式检查 + allow 规则                                             │
33//! │     bypass 模式 → Allow                                               │
34//! │     allow 规则  → Allow                                               │
35//! │                                                                      │
36//! │  6. 回退                                                              │
37//! │     → Ask (请求用户决策)                                              │
38//! └─────────────────────────────────────────────────────────────────────┘
39//! ```
40//!
41//! ## 对外接口
42//! - `PermissionBehavior` — 权限行为三态 (allow / deny / ask)
43//! - `PermissionMode` — 全局权限模式
44//! - `RuleSource` — 规则来源(5 层优先级)
45//! - `PermissionRule` — 单条权限规则
46//! - `Ruleset` — 规则集合 + 求值引擎
47//! - `PermissionRequest` — 权限请求
48//! - `PermissionDecision` — 权限决策
49//! - `PermissionReason` — 决策原因
50//! - `PermissionReply` — 用户回复
51//! - `SessionPermissionCache` — 会话级权限缓存
52//! - `PermissionUpdate` — 权限规则更新动作
53//!
54//! ## 与 Hook 系统的关系
55//! ```text
56//! PreToolUse Hook → HookPermission (allow/deny/ask)
57//!                         ↓
58//! Permission System → 聚合 Hook 决策 + 规则 + 工具检查 → 最终决策
59//! ```
60//!
61//! ## 调用者
62//! - `katu-agent` (future) — Agent loop 在工具执行前调用
63//! - Hook 系统 — HookPermission 作为输入之一
64
65use async_trait::async_trait;
66use serde::{Deserialize, Serialize};
67
68use crate::hook::HookPermission;
69use crate::types::ToolCallId;
70
71// ===========================================================================
72// PermissionBehavior
73// ===========================================================================
74
75/// 权限行为三态。
76///
77/// 这是权限系统中最基本的决策单元:
78/// - `Allow` — 允许执行
79/// - `Deny` — 拒绝执行
80/// - `Ask` — 需要用户确认
81///
82/// # 优先级
83/// 在规则冲突时:`Deny > Ask > Allow`。
84///
85/// # Examples
86///
87/// ```
88/// use katu_core::permission::PermissionBehavior;
89///
90/// let behavior = PermissionBehavior::Allow;
91/// assert!(behavior.is_allow());
92/// assert!(!behavior.is_deny());
93/// ```
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
95#[serde(rename_all = "snake_case")]
96pub enum PermissionBehavior {
97    /// 允许执行。
98    Allow,
99    /// 拒绝执行。
100    Deny,
101    /// 需要用户确认。
102    Ask,
103}
104
105impl PermissionBehavior {
106    pub fn is_allow(&self) -> bool {
107        matches!(self, Self::Allow)
108    }
109
110    pub fn is_deny(&self) -> bool {
111        matches!(self, Self::Deny)
112    }
113
114    pub fn is_ask(&self) -> bool {
115        matches!(self, Self::Ask)
116    }
117
118    /// 严格度数值 — 用于冲突解决。
119    ///
120    /// Deny(2) > Ask(1) > Allow(0)。
121    pub fn strictness(&self) -> u8 {
122        match self {
123            Self::Allow => 0,
124            Self::Ask => 1,
125            Self::Deny => 2,
126        }
127    }
128}
129
130// ===========================================================================
131// PermissionMode
132// ===========================================================================
133
134/// 全局权限模式 — 控制 Agent 的整体权限策略。
135///
136/// 模式决定了"未被规则覆盖"的操作如何处理。
137///
138/// # Examples
139///
140/// ```
141/// use katu_core::permission::PermissionMode;
142///
143/// let mode = PermissionMode::Default;
144/// assert!(!mode.is_bypass());
145/// assert!(PermissionMode::Bypass.is_bypass());
146/// ```
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
148#[serde(rename_all = "snake_case")]
149pub enum PermissionMode {
150    /// 默认模式 — 危险操作询问用户。
151    #[default]
152    Default,
153
154    /// 计划模式 — 只规划不执行,所有写操作 deny。
155    Plan,
156
157    /// 自动允许编辑 — 文件编辑类操作自动 allow,其他仍 ask。
158    AcceptEdits,
159
160    /// 绕过权限 — 跳过用户询问,直接 allow。
161    ///
162    /// # 安全限制
163    /// - Policy deny 规则**不可**被绕过
164    /// - 安全检查(敏感路径)**不可**被绕过
165    Bypass,
166
167    /// 不询问模式 — 需要 ask 的操作直接 deny(用于非交互环境)。
168    NonInteractive,
169}
170
171impl PermissionMode {
172    /// 是否为绕过模式。
173    pub fn is_bypass(&self) -> bool {
174        matches!(self, Self::Bypass)
175    }
176
177    /// 是否为非交互模式。
178    pub fn is_non_interactive(&self) -> bool {
179        matches!(self, Self::NonInteractive)
180    }
181
182    /// 是否为计划模式。
183    pub fn is_plan(&self) -> bool {
184        matches!(self, Self::Plan)
185    }
186
187    /// 对 ask 决策应用模式变换。
188    ///
189    /// - `Bypass` → ask 变为 allow
190    /// - `NonInteractive` → ask 变为 deny
191    /// - 其他 → 保持 ask
192    pub fn transform_ask(&self) -> PermissionBehavior {
193        match self {
194            Self::Bypass => PermissionBehavior::Allow,
195            Self::NonInteractive => PermissionBehavior::Deny,
196            _ => PermissionBehavior::Ask,
197        }
198    }
199}
200
201// ===========================================================================
202// RuleSource
203// ===========================================================================
204
205/// 规则来源 — 5 层优先级,高优先级来源的决策不可被低优先级覆盖。
206///
207/// ```text
208/// Policy (最高) → User → Project → Session → Default (最低)
209/// ```
210///
211/// # 设计参考
212/// - Claude-Code: 7 种来源
213/// - OpenCode: 无来源概念(flat ruleset)
214/// - katu: 5 层,兼顾灵活性与简洁性
215///
216/// # Examples
217///
218/// ```
219/// use katu_core::permission::RuleSource;
220///
221/// assert!(RuleSource::Policy.priority() > RuleSource::User.priority());
222/// assert!(RuleSource::User.priority() > RuleSource::Project.priority());
223/// ```
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
225#[serde(rename_all = "snake_case")]
226pub enum RuleSource {
227    /// 管理员策略 — 最高优先级,不可被用户覆盖。
228    ///
229    /// 来自企业管理平台或 `policySettings`。
230    Policy,
231
232    /// 用户全局设置 — 用户级偏好。
233    ///
234    /// 来自 `~/.config/katu/settings.toml`。
235    User,
236
237    /// 项目设置 — 项目级偏好。
238    ///
239    /// 来自 `.katu/settings.toml`(版本控制中共享)。
240    Project,
241
242    /// 会话 / 运行时 — 本次运行中动态添加的规则。
243    ///
244    /// 来自用户 "always allow" 选择或命令行参数。
245    Session,
246
247    /// 默认规则 — 最低优先级,内置兜底。
248    Default,
249}
250
251impl RuleSource {
252    /// 返回此来源的优先级数值(越大越高)。
253    pub fn priority(&self) -> u8 {
254        match self {
255            Self::Policy => 100,
256            Self::User => 80,
257            Self::Project => 60,
258            Self::Session => 40,
259            Self::Default => 0,
260        }
261    }
262
263    /// 此来源的规则是否不可被低优先级来源覆盖。
264    pub fn is_immutable(&self) -> bool {
265        matches!(self, Self::Policy)
266    }
267}
268
269impl PartialOrd for RuleSource {
270    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
271        Some(self.cmp(other))
272    }
273}
274
275impl Ord for RuleSource {
276    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
277        self.priority().cmp(&other.priority())
278    }
279}
280
281// ===========================================================================
282// PermissionRule
283// ===========================================================================
284
285/// 权限规则 — 基于模式匹配的授权条目。
286///
287/// 一条规则匹配 `(permission_key, content_pattern)` 二元组。
288///
289/// # 模式匹配语法
290/// - 精确匹配:`"bash"`, `"/etc/shadow"`
291/// - 通配符:`"read_*"`, `"*.rs"`, `"/home/*/project/*"`
292/// - 全匹配:`"*"`
293///
294/// # Examples
295///
296/// ```
297/// use katu_core::permission::{PermissionRule, PermissionBehavior, RuleSource};
298///
299/// // 允许所有 read 操作
300/// let rule = PermissionRule::new(RuleSource::User, PermissionBehavior::Allow, "read", "*");
301///
302/// // 拒绝 bash 执行 rm 命令
303/// let rule = PermissionRule::new(RuleSource::Project, PermissionBehavior::Deny, "bash", "rm *");
304///
305/// // 编辑 .rs 文件需要确认
306/// let rule = PermissionRule::new(RuleSource::User, PermissionBehavior::Ask, "edit", "*.rs");
307/// ```
308#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
309pub struct PermissionRule {
310    /// 规则来源。
311    pub source: RuleSource,
312
313    /// 权限行为。
314    pub behavior: PermissionBehavior,
315
316    /// 权限 key 的匹配模式(如 `"bash"`, `"edit"`, `"read_*"`)。
317    pub permission: String,
318
319    /// 内容的匹配模式(如 `"rm *"`, `"*.rs"`, `"/etc/*"`)。
320    pub pattern: String,
321}
322
323impl PermissionRule {
324    /// 创建新规则。
325    pub fn new(
326        source: RuleSource,
327        behavior: PermissionBehavior,
328        permission: impl Into<String>,
329        pattern: impl Into<String>,
330    ) -> Self {
331        Self {
332            source,
333            behavior,
334            permission: permission.into(),
335            pattern: pattern.into(),
336        }
337    }
338
339    /// 快捷构造:allow 规则。
340    pub fn allow(source: RuleSource, permission: impl Into<String>, pattern: impl Into<String>) -> Self {
341        Self::new(source, PermissionBehavior::Allow, permission, pattern)
342    }
343
344    /// 快捷构造:deny 规则。
345    pub fn deny(source: RuleSource, permission: impl Into<String>, pattern: impl Into<String>) -> Self {
346        Self::new(source, PermissionBehavior::Deny, permission, pattern)
347    }
348
349    /// 快捷构造:ask 规则。
350    pub fn ask(source: RuleSource, permission: impl Into<String>, pattern: impl Into<String>) -> Self {
351        Self::new(source, PermissionBehavior::Ask, permission, pattern)
352    }
353
354    /// 检查此规则是否匹配给定的 (permission_key, content)。
355    pub fn matches(&self, permission_key: &str, content: &str) -> bool {
356        wildcard_match(permission_key, &self.permission)
357            && wildcard_match(content, &self.pattern)
358    }
359}
360
361// ===========================================================================
362// Ruleset
363// ===========================================================================
364
365/// 规则集合 — 有序规则列表 + 求值引擎。
366///
367/// ## 求值语义
368/// 按来源优先级分层求值:
369/// 1. 从最高优先级来源开始
370/// 2. 在同一来源内,最后匹配的规则获胜(last-match-wins,参考 OpenCode)
371/// 3. 高优先级来源的结果不可被低优先级覆盖
372/// 4. 无匹配 → 返回 None(由调用者决定默认行为)
373///
374/// # Examples
375///
376/// ```
377/// use katu_core::permission::*;
378///
379/// let mut ruleset = Ruleset::new();
380///
381/// // 用户允许所有 read
382/// ruleset.add(PermissionRule::allow(RuleSource::User, "read", "*"));
383/// // 项目禁止读取 .env
384/// ruleset.add(PermissionRule::deny(RuleSource::Project, "read", "*.env"));
385///
386/// // 求值:User(allow read *) 优先于 Project(deny read *.env)
387/// let result = ruleset.evaluate("read", ".env");
388/// // User 优先级更高,返回 Allow
389/// assert_eq!(result, Some(PermissionBehavior::Allow));
390/// ```
391#[derive(Debug, Clone, Default, Serialize, Deserialize)]
392pub struct Ruleset {
393    rules: Vec<PermissionRule>,
394}
395
396impl Ruleset {
397    /// 创建空规则集。
398    pub fn new() -> Self {
399        Self { rules: Vec::new() }
400    }
401
402    /// 添加规则。
403    pub fn add(&mut self, rule: PermissionRule) {
404        self.rules.push(rule);
405    }
406
407    /// 批量添加规则。
408    pub fn extend(&mut self, rules: impl IntoIterator<Item = PermissionRule>) {
409        self.rules.extend(rules);
410    }
411
412    /// 移除指定来源的所有规则。
413    pub fn remove_source(&mut self, source: RuleSource) {
414        self.rules.retain(|r| r.source != source);
415    }
416
417    /// 规则数量。
418    pub fn len(&self) -> usize {
419        self.rules.len()
420    }
421
422    /// 是否为空。
423    pub fn is_empty(&self) -> bool {
424        self.rules.is_empty()
425    }
426
427    /// 获取所有规则的只读引用。
428    pub fn rules(&self) -> &[PermissionRule] {
429        &self.rules
430    }
431
432    /// 求值 — 对给定的 (permission_key, content) 查找匹配的规则。
433    ///
434    /// ## 算法
435    /// 1. 按来源优先级从高到低分层
436    /// 2. 每层内找最后一条匹配规则(last-match-wins)
437    /// 3. 最高优先级层的结果获胜
438    /// 4. 无匹配返回 None
439    ///
440    /// # Examples
441    ///
442    /// ```
443    /// use katu_core::permission::*;
444    ///
445    /// let mut ruleset = Ruleset::new();
446    /// ruleset.add(PermissionRule::deny(RuleSource::Policy, "bash", "rm *"));
447    /// ruleset.add(PermissionRule::allow(RuleSource::Session, "bash", "*"));
448    ///
449    /// // Policy deny 不可被 Session allow 覆盖
450    /// assert_eq!(ruleset.evaluate("bash", "rm -rf /"), Some(PermissionBehavior::Deny));
451    /// // 非 rm 命令被 Session allow 覆盖
452    /// assert_eq!(ruleset.evaluate("bash", "ls -la"), Some(PermissionBehavior::Allow));
453    /// ```
454    pub fn evaluate(&self, permission_key: &str, content: &str) -> Option<PermissionBehavior> {
455        // 按来源分层,从高优先级到低优先级
456        for source in &[
457            RuleSource::Policy,
458            RuleSource::User,
459            RuleSource::Project,
460            RuleSource::Session,
461            RuleSource::Default,
462        ] {
463            // 在此来源层内,找最后一条匹配的规则 (last-match-wins)
464            let last_match = self
465                .rules
466                .iter()
467                .filter(|r| r.source == *source)
468                .filter(|r| r.matches(permission_key, content))
469                .last();
470
471            if let Some(rule) = last_match {
472                return Some(rule.behavior);
473            }
474        }
475
476        None
477    }
478
479    /// 快速检查 — 是否存在针对某 permission_key 的 deny 规则。
480    ///
481    /// 用于热路径的快速短路(不做 content 匹配)。
482    pub fn has_deny_for(&self, permission_key: &str) -> bool {
483        self.rules.iter().any(|r| {
484            r.behavior.is_deny() && wildcard_match(permission_key, &r.permission)
485        })
486    }
487}
488
489// ===========================================================================
490// PermissionRequest
491// ===========================================================================
492
493/// 权限请求 — 工具执行前向权限系统提交的授权请求。
494///
495/// 由 Agent loop 构造,传入权限系统进行求值。
496///
497/// # Examples
498///
499/// ```
500/// use katu_core::permission::PermissionRequest;
501/// use katu_core::ToolCallId;
502/// use serde_json::json;
503///
504/// let req = PermissionRequest::new("bash", "rm -rf /tmp/cache")
505///     .with_tool_name("bash")
506///     .with_call_id(ToolCallId::new("call_1"))
507///     .with_metadata(json!({"working_dir": "/home/user"}));
508/// ```
509#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct PermissionRequest {
511    /// 权限 key(如 `"bash"`, `"edit"`, `"read"`)。
512    pub permission: String,
513
514    /// 需检查的内容模式列表。
515    ///
516    /// 例如 bash 命令的完整内容、文件路径等。
517    /// 所有 pattern 必须通过才能 allow。
518    pub patterns: Vec<String>,
519
520    /// 工具名称。
521    #[serde(default, skip_serializing_if = "Option::is_none")]
522    pub tool_name: Option<String>,
523
524    /// 关联的 tool call ID。
525    #[serde(default, skip_serializing_if = "Option::is_none")]
526    pub call_id: Option<ToolCallId>,
527
528    /// 额外元数据(用于 UI 展示和日志)。
529    #[serde(default)]
530    pub metadata: serde_json::Value,
531
532    /// 如果用户选择 "always allow",应持久化的模式列表。
533    ///
534    /// 可能与 `patterns` 不同 — 例如 bash 可能请求检查 `"rm -rf /tmp/cache"`,
535    /// 但 always-allow 应记录为 `"rm *"`(更宽泛的模式)。
536    #[serde(default, skip_serializing_if = "Vec::is_empty")]
537    pub always_allow_patterns: Vec<String>,
538}
539
540impl PermissionRequest {
541    /// 创建权限请求 — 单一 pattern。
542    pub fn new(permission: impl Into<String>, pattern: impl Into<String>) -> Self {
543        Self {
544            permission: permission.into(),
545            patterns: vec![pattern.into()],
546            tool_name: None,
547            call_id: None,
548            metadata: serde_json::Value::Null,
549            always_allow_patterns: Vec::new(),
550        }
551    }
552
553    /// 创建权限请求 — 多 pattern。
554    pub fn with_patterns(
555        permission: impl Into<String>,
556        patterns: impl IntoIterator<Item = impl Into<String>>,
557    ) -> Self {
558        Self {
559            permission: permission.into(),
560            patterns: patterns.into_iter().map(Into::into).collect(),
561            tool_name: None,
562            call_id: None,
563            metadata: serde_json::Value::Null,
564            always_allow_patterns: Vec::new(),
565        }
566    }
567
568    /// 设置工具名称(builder 模式)。
569    pub fn with_tool_name(mut self, name: impl Into<String>) -> Self {
570        self.tool_name = Some(name.into());
571        self
572    }
573
574    /// 设置 call ID(builder 模式)。
575    pub fn with_call_id(mut self, id: ToolCallId) -> Self {
576        self.call_id = Some(id);
577        self
578    }
579
580    /// 设置元数据(builder 模式)。
581    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
582        self.metadata = metadata;
583        self
584    }
585
586    /// 设置 always-allow 模式列表(builder 模式)。
587    pub fn with_always_allow(mut self, patterns: impl IntoIterator<Item = impl Into<String>>) -> Self {
588        self.always_allow_patterns = patterns.into_iter().map(Into::into).collect();
589        self
590    }
591}
592
593// ===========================================================================
594// PermissionDecision
595// ===========================================================================
596
597/// 权限决策 — 权限系统对请求的最终裁定。
598///
599/// # Examples
600///
601/// ```
602/// use katu_core::permission::{PermissionDecision, PermissionReason, RuleSource};
603///
604/// let decision = PermissionDecision::allow(PermissionReason::Rule {
605///     source: RuleSource::User,
606/// });
607/// assert!(decision.is_allow());
608///
609/// let decision = PermissionDecision::deny(
610///     PermissionReason::Rule { source: RuleSource::Policy },
611///     "Operation blocked by admin policy",
612/// );
613/// assert!(decision.is_deny());
614/// ```
615#[derive(Debug, Clone, Serialize, Deserialize)]
616#[serde(tag = "behavior", rename_all = "snake_case")]
617pub enum PermissionDecision {
618    /// 允许执行。
619    Allow {
620        reason: PermissionReason,
621        /// Hook 或工具修改后的输入(如有)。
622        #[serde(default, skip_serializing_if = "Option::is_none")]
623        updated_input: Option<serde_json::Value>,
624    },
625
626    /// 拒绝执行。
627    Deny {
628        reason: PermissionReason,
629        /// 展示给用户/LLM 的拒绝消息。
630        message: String,
631    },
632
633    /// 需要用户确认。
634    Ask {
635        /// 展示给用户的确认消息。
636        message: String,
637        /// 建议的权限更新(用户选择 always 时应用)。
638        #[serde(default, skip_serializing_if = "Vec::is_empty")]
639        suggestions: Vec<PermissionUpdate>,
640    },
641}
642
643impl PermissionDecision {
644    /// 创建 Allow 决策。
645    pub fn allow(reason: PermissionReason) -> Self {
646        Self::Allow {
647            reason,
648            updated_input: None,
649        }
650    }
651
652    /// 创建带修改输入的 Allow 决策。
653    pub fn allow_with_input(reason: PermissionReason, updated_input: serde_json::Value) -> Self {
654        Self::Allow {
655            reason,
656            updated_input: Some(updated_input),
657        }
658    }
659
660    /// 创建 Deny 决策。
661    pub fn deny(reason: PermissionReason, message: impl Into<String>) -> Self {
662        Self::Deny {
663            reason,
664            message: message.into(),
665        }
666    }
667
668    /// 创建 Ask 决策。
669    pub fn ask(message: impl Into<String>) -> Self {
670        Self::Ask {
671            message: message.into(),
672            suggestions: Vec::new(),
673        }
674    }
675
676    /// 创建带建议的 Ask 决策。
677    pub fn ask_with_suggestions(
678        message: impl Into<String>,
679        suggestions: Vec<PermissionUpdate>,
680    ) -> Self {
681        Self::Ask {
682            message: message.into(),
683            suggestions,
684        }
685    }
686
687    pub fn is_allow(&self) -> bool {
688        matches!(self, Self::Allow { .. })
689    }
690
691    pub fn is_deny(&self) -> bool {
692        matches!(self, Self::Deny { .. })
693    }
694
695    pub fn is_ask(&self) -> bool {
696        matches!(self, Self::Ask { .. })
697    }
698
699    /// 获取决策的行为枚举。
700    pub fn behavior(&self) -> PermissionBehavior {
701        match self {
702            Self::Allow { .. } => PermissionBehavior::Allow,
703            Self::Deny { .. } => PermissionBehavior::Deny,
704            Self::Ask { .. } => PermissionBehavior::Ask,
705        }
706    }
707}
708
709// ===========================================================================
710// PermissionReason
711// ===========================================================================
712
713/// 权限决策原因 — 说明为什么做出此决策。
714///
715/// 用于审计日志和 UI 展示。
716///
717/// # Examples
718///
719/// ```
720/// use katu_core::permission::{PermissionReason, RuleSource};
721///
722/// let reason = PermissionReason::Rule { source: RuleSource::Policy };
723/// assert!(matches!(reason, PermissionReason::Rule { .. }));
724/// ```
725#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
726#[serde(tag = "type", rename_all = "snake_case")]
727pub enum PermissionReason {
728    /// 匹配了某条规则。
729    Rule { source: RuleSource },
730    /// 权限模式决定(如 Bypass 自动 allow)。
731    Mode,
732    /// 工具级 check_permissions 返回。
733    ToolCheck,
734    /// Hook 系统决定。
735    Hook,
736    /// 会话缓存命中(用户之前选择了 always)。
737    SessionCache,
738    /// 安全检查触发(敏感路径等)。
739    SafetyCheck,
740    /// 用户直接决定(回复 allow/deny)。
741    UserDecision,
742}
743
744// ===========================================================================
745// PermissionResult
746// ===========================================================================
747
748/// 工具级权限检查结果 — `Tool::check_permissions()` 的返回值。
749///
750/// 与 `PermissionDecision` 不同,多了 `Passthrough` 变体表示
751/// "我不关心,交给框架规则引擎决定"。
752///
753/// # Examples
754///
755/// ```
756/// use katu_core::permission::PermissionResult;
757///
758/// // 工具不关心权限(大多数工具的默认行为)
759/// let result = PermissionResult::Passthrough;
760/// assert!(result.is_passthrough());
761///
762/// // 工具明确拒绝(如路径安全检查失败)
763/// let result = PermissionResult::deny("Cannot write to /etc/");
764/// assert!(result.is_deny());
765/// ```
766#[derive(Debug, Clone, Serialize, Deserialize)]
767#[serde(tag = "type", rename_all = "snake_case")]
768pub enum PermissionResult {
769    /// 允许执行。
770    Allow,
771    /// 拒绝执行。
772    Deny { message: String },
773    /// 需要用户确认。
774    Ask { message: String },
775    /// 不做决定 — 交由框架的规则引擎处理。
776    Passthrough,
777}
778
779impl PermissionResult {
780    /// 创建 Deny 结果。
781    pub fn deny(message: impl Into<String>) -> Self {
782        Self::Deny {
783            message: message.into(),
784        }
785    }
786
787    /// 创建 Ask 结果。
788    pub fn ask(message: impl Into<String>) -> Self {
789        Self::Ask {
790            message: message.into(),
791        }
792    }
793
794    pub fn is_allow(&self) -> bool {
795        matches!(self, Self::Allow)
796    }
797
798    pub fn is_deny(&self) -> bool {
799        matches!(self, Self::Deny { .. })
800    }
801
802    pub fn is_ask(&self) -> bool {
803        matches!(self, Self::Ask { .. })
804    }
805
806    pub fn is_passthrough(&self) -> bool {
807        matches!(self, Self::Passthrough)
808    }
809}
810
811// ===========================================================================
812// PermissionReply
813// ===========================================================================
814
815/// 用户回复 — 对 Ask 决策的用户响应。
816///
817/// # 设计参考
818/// - Oh-My-Pi: allow_once / allow_always / reject_once / reject_always (4 种)
819/// - OpenCode: once / always / reject (3 种)
820/// - katu: 4 种 + 可选反馈(参考 OpenCode 的 CorrectedError)
821///
822/// # Examples
823///
824/// ```
825/// use katu_core::permission::PermissionReply;
826///
827/// let reply = PermissionReply::AllowOnce;
828/// assert!(reply.is_allow());
829///
830/// let reply = PermissionReply::DenyWithFeedback {
831///     feedback: "Use a safer command instead".into(),
832/// };
833/// assert!(reply.is_deny());
834/// assert!(reply.has_feedback());
835/// ```
836#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
837#[serde(tag = "type", rename_all = "snake_case")]
838pub enum PermissionReply {
839    /// 本次允许(不记忆)。
840    AllowOnce,
841
842    /// 始终允许(写入会话缓存 / 持久化)。
843    AllowAlways,
844
845    /// 本次拒绝。
846    DenyOnce,
847
848    /// 始终拒绝(写入会话缓存)。
849    DenyAlways,
850
851    /// 拒绝 + 反馈(LLM 可据此修正策略)。
852    DenyWithFeedback { feedback: String },
853}
854
855impl PermissionReply {
856    pub fn is_allow(&self) -> bool {
857        matches!(self, Self::AllowOnce | Self::AllowAlways)
858    }
859
860    pub fn is_deny(&self) -> bool {
861        matches!(self, Self::DenyOnce | Self::DenyAlways | Self::DenyWithFeedback { .. })
862    }
863
864    pub fn is_always(&self) -> bool {
865        matches!(self, Self::AllowAlways | Self::DenyAlways)
866    }
867
868    /// 是否携带用户反馈。
869    pub fn has_feedback(&self) -> bool {
870        matches!(self, Self::DenyWithFeedback { .. })
871    }
872
873    /// 获取反馈内容(如有)。
874    pub fn feedback(&self) -> Option<&str> {
875        match self {
876            Self::DenyWithFeedback { feedback } => Some(feedback.as_str()),
877            _ => None,
878        }
879    }
880
881    /// 转换为行为枚举。
882    pub fn behavior(&self) -> PermissionBehavior {
883        if self.is_allow() {
884            PermissionBehavior::Allow
885        } else {
886            PermissionBehavior::Deny
887        }
888    }
889}
890
891// ===========================================================================
892// PermissionUpdate
893// ===========================================================================
894
895/// 权限规则更新动作 — 描述对规则集的变更。
896///
897/// 用于:
898/// 1. 用户选择 "always allow" 后持久化规则
899/// 2. Hook 建议的权限变更
900/// 3. 配置文件同步
901///
902/// # Examples
903///
904/// ```
905/// use katu_core::permission::*;
906///
907/// // 添加规则:always allow bash(ls *)
908/// let update = PermissionUpdate::add_rule(
909///     RuleSource::Session,
910///     PermissionRule::allow(RuleSource::Session, "bash", "ls *"),
911/// );
912/// ```
913#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
914#[serde(tag = "action", rename_all = "snake_case")]
915pub enum PermissionUpdate {
916    /// 添加规则。
917    AddRule {
918        destination: RuleSource,
919        rule: PermissionRule,
920    },
921    /// 移除匹配规则。
922    RemoveRules {
923        destination: RuleSource,
924        permission: String,
925        pattern: String,
926    },
927    /// 设置全局模式。
928    SetMode {
929        mode: PermissionMode,
930    },
931}
932
933impl PermissionUpdate {
934    /// 创建添加规则的更新。
935    pub fn add_rule(destination: RuleSource, rule: PermissionRule) -> Self {
936        Self::AddRule { destination, rule }
937    }
938
939    /// 创建移除规则的更新。
940    pub fn remove_rules(
941        destination: RuleSource,
942        permission: impl Into<String>,
943        pattern: impl Into<String>,
944    ) -> Self {
945        Self::RemoveRules {
946            destination,
947            permission: permission.into(),
948            pattern: pattern.into(),
949        }
950    }
951
952    /// 创建设置模式的更新。
953    pub fn set_mode(mode: PermissionMode) -> Self {
954        Self::SetMode { mode }
955    }
956}
957
958// ===========================================================================
959// SessionPermissionCache
960// ===========================================================================
961
962/// 会话级权限缓存 — 记录用户 "always" 决策。
963///
964/// ## 设计参考
965/// - Oh-My-Pi: `Map<string, "allow_always" | "reject_always">`
966/// - OpenCode: `approved: Ruleset` 累积规则
967/// - katu: `Ruleset` 子集,只包含 Session 来源的 allow/deny 规则
968///
969/// ## 线程安全
970/// 作为纯数据结构,线程安全由持有者负责(如 `Arc<RwLock<SessionPermissionCache>>`)。
971///
972/// # Examples
973///
974/// ```
975/// use katu_core::permission::*;
976///
977/// let mut cache = SessionPermissionCache::new();
978///
979/// // 用户选择 "always allow bash(git *)"
980/// cache.allow_always("bash", "git *");
981///
982/// // 下次检查时命中缓存
983/// assert_eq!(cache.check("bash", "git pull"), Some(PermissionBehavior::Allow));
984/// assert_eq!(cache.check("bash", "rm -rf /"), None); // 不在缓存中
985/// ```
986#[derive(Debug, Clone, Default, Serialize, Deserialize)]
987pub struct SessionPermissionCache {
988    rules: Vec<CacheEntry>,
989}
990
991/// 缓存条目。
992#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
993struct CacheEntry {
994    permission: String,
995    pattern: String,
996    behavior: PermissionBehavior,
997}
998
999impl SessionPermissionCache {
1000    /// 创建空缓存。
1001    pub fn new() -> Self {
1002        Self { rules: Vec::new() }
1003    }
1004
1005    /// 记录 "always allow"。
1006    pub fn allow_always(&mut self, permission: impl Into<String>, pattern: impl Into<String>) {
1007        self.rules.push(CacheEntry {
1008            permission: permission.into(),
1009            pattern: pattern.into(),
1010            behavior: PermissionBehavior::Allow,
1011        });
1012    }
1013
1014    /// 记录 "always deny"。
1015    pub fn deny_always(&mut self, permission: impl Into<String>, pattern: impl Into<String>) {
1016        self.rules.push(CacheEntry {
1017            permission: permission.into(),
1018            pattern: pattern.into(),
1019            behavior: PermissionBehavior::Deny,
1020        });
1021    }
1022
1023    /// 检查缓存 — 返回匹配的缓存决策。
1024    ///
1025    /// 使用 last-match-wins 语义。
1026    pub fn check(&self, permission_key: &str, content: &str) -> Option<PermissionBehavior> {
1027        self.rules
1028            .iter()
1029            .filter(|e| {
1030                wildcard_match(permission_key, &e.permission)
1031                    && wildcard_match(content, &e.pattern)
1032            })
1033            .last()
1034            .map(|e| e.behavior)
1035    }
1036
1037    /// 缓存条目数量。
1038    pub fn len(&self) -> usize {
1039        self.rules.len()
1040    }
1041
1042    /// 是否为空。
1043    pub fn is_empty(&self) -> bool {
1044        self.rules.is_empty()
1045    }
1046
1047    /// 清空缓存。
1048    pub fn clear(&mut self) {
1049        self.rules.clear();
1050    }
1051
1052    /// 转换为可持久化的 Ruleset(Session 来源)。
1053    pub fn to_ruleset(&self) -> Ruleset {
1054        let mut ruleset = Ruleset::new();
1055        for entry in &self.rules {
1056            ruleset.add(PermissionRule::new(
1057                RuleSource::Session,
1058                entry.behavior,
1059                &entry.permission,
1060                &entry.pattern,
1061            ));
1062        }
1063        ruleset
1064    }
1065}
1066
1067// ===========================================================================
1068// DenialTracker
1069// ===========================================================================
1070
1071/// 连续拒绝追踪器 — 防止 LLM 陷入反复被拒绝的循环。
1072///
1073/// 参考 Claude-Code 的 circuit breaker 机制:
1074/// 连续 N 次拒绝后,向 LLM 注入警告或强制中断。
1075///
1076/// # Examples
1077///
1078/// ```
1079/// use katu_core::permission::DenialTracker;
1080///
1081/// let mut tracker = DenialTracker::new(3); // 阈值 = 3
1082///
1083/// tracker.record_denial();
1084/// tracker.record_denial();
1085/// assert!(!tracker.is_tripped());
1086///
1087/// tracker.record_denial();
1088/// assert!(tracker.is_tripped()); // 连续 3 次,触发
1089///
1090/// tracker.record_allow();
1091/// assert!(!tracker.is_tripped()); // 一次 allow 重置
1092/// ```
1093#[derive(Debug, Clone)]
1094pub struct DenialTracker {
1095    /// 连续拒绝次数。
1096    consecutive_denials: u32,
1097    /// 触发阈值。
1098    threshold: u32,
1099    /// 总拒绝次数(不重置)。
1100    total_denials: u32,
1101}
1102
1103impl DenialTracker {
1104    /// 创建新的追踪器。
1105    pub fn new(threshold: u32) -> Self {
1106        Self {
1107            consecutive_denials: 0,
1108            threshold,
1109            total_denials: 0,
1110        }
1111    }
1112
1113    /// 记录一次拒绝。
1114    pub fn record_denial(&mut self) {
1115        self.consecutive_denials += 1;
1116        self.total_denials += 1;
1117    }
1118
1119    /// 记录一次允许(重置连续计数)。
1120    pub fn record_allow(&mut self) {
1121        self.consecutive_denials = 0;
1122    }
1123
1124    /// 是否已触发阈值。
1125    pub fn is_tripped(&self) -> bool {
1126        self.consecutive_denials >= self.threshold
1127    }
1128
1129    /// 当前连续拒绝次数。
1130    pub fn consecutive_count(&self) -> u32 {
1131        self.consecutive_denials
1132    }
1133
1134    /// 总拒绝次数。
1135    pub fn total_count(&self) -> u32 {
1136        self.total_denials
1137    }
1138
1139    /// 重置。
1140    pub fn reset(&mut self) {
1141        self.consecutive_denials = 0;
1142    }
1143}
1144
1145// ===========================================================================
1146// Hook 集成 — 类型桥接
1147// ===========================================================================
1148
1149/// `HookPermission` → `PermissionBehavior` 转换。
1150///
1151/// Hook 系统的权限决策映射到权限系统的行为三态。
1152///
1153/// # Examples
1154///
1155/// ```
1156/// use katu_core::hook::HookPermission;
1157/// use katu_core::permission::PermissionBehavior;
1158///
1159/// let behavior: PermissionBehavior = HookPermission::Allow.into();
1160/// assert_eq!(behavior, PermissionBehavior::Allow);
1161///
1162/// let behavior: PermissionBehavior = HookPermission::Deny { reason: Some("no".into()) }.into();
1163/// assert_eq!(behavior, PermissionBehavior::Deny);
1164/// ```
1165impl From<HookPermission> for PermissionBehavior {
1166    fn from(hook_perm: HookPermission) -> Self {
1167        match hook_perm {
1168            HookPermission::Allow => Self::Allow,
1169            HookPermission::Deny { .. } => Self::Deny,
1170            HookPermission::Ask { .. } => Self::Ask,
1171        }
1172    }
1173}
1174
1175impl From<&HookPermission> for PermissionBehavior {
1176    fn from(hook_perm: &HookPermission) -> Self {
1177        match hook_perm {
1178            HookPermission::Allow => Self::Allow,
1179            HookPermission::Deny { .. } => Self::Deny,
1180            HookPermission::Ask { .. } => Self::Ask,
1181        }
1182    }
1183}
1184
1185/// `PermissionBehavior` → `HookPermission` 转换。
1186impl From<PermissionBehavior> for HookPermission {
1187    fn from(behavior: PermissionBehavior) -> Self {
1188        match behavior {
1189            PermissionBehavior::Allow => Self::Allow,
1190            PermissionBehavior::Deny => Self::Deny { reason: None },
1191            PermissionBehavior::Ask => Self::Ask { message: None },
1192        }
1193    }
1194}
1195
1196// ===========================================================================
1197// PermissionEngine — 管线协调器 trait
1198// ===========================================================================
1199
1200/// 权限检查的完整输入上下文。
1201///
1202/// Agent loop 在工具执行前构造此结构,传入 `PermissionEngine::check()`。
1203#[derive(Debug, Clone)]
1204pub struct PermissionCheckInput {
1205    /// 权限请求。
1206    pub request: PermissionRequest,
1207
1208    /// Hook 系统的聚合决策(如果 PreToolUse hook 有返回)。
1209    ///
1210    /// `None` 表示没有 Hook 参与或所有 Hook 都 passthrough。
1211    pub hook_decision: Option<HookPermission>,
1212
1213    /// 工具级权限检查结果。
1214    ///
1215    /// `None` 表示工具未实现 `check_permissions`(等同于 Passthrough)。
1216    pub tool_check: Option<PermissionResult>,
1217
1218    /// 当前权限模式。
1219    pub mode: PermissionMode,
1220}
1221
1222impl PermissionCheckInput {
1223    /// 创建最小输入 — 只有请求和模式。
1224    pub fn new(request: PermissionRequest, mode: PermissionMode) -> Self {
1225        Self {
1226            request,
1227            hook_decision: None,
1228            tool_check: None,
1229            mode,
1230        }
1231    }
1232
1233    /// 设置 Hook 决策(builder 模式)。
1234    pub fn with_hook_decision(mut self, decision: HookPermission) -> Self {
1235        self.hook_decision = Some(decision);
1236        self
1237    }
1238
1239    /// 设置工具检查结果(builder 模式)。
1240    pub fn with_tool_check(mut self, result: PermissionResult) -> Self {
1241        self.tool_check = Some(result);
1242        self
1243    }
1244}
1245
1246/// 权限引擎 trait — 定义完整的权限检查管线契约。
1247///
1248/// `katu-agent` 实现此 trait,协调规则引擎 + Hook 决策 + 工具检查 + 用户交互。
1249///
1250/// ## 管线执行顺序
1251///
1252/// ```text
1253/// ┌──────────────────────────────────────────────────────────────────┐
1254/// │  PermissionEngine::check(input)                                   │
1255/// │                                                                   │
1256/// │  Phase 1: 不可覆盖层                                               │
1257/// │    ├─ Policy deny 规则 → 立即 Deny                                │
1258/// │    └─ Tool deny (check_permissions) → 立即 Deny                   │
1259/// │                                                                   │
1260/// │  Phase 2: Hook 决策                                               │
1261/// │    ├─ Hook deny → Deny (Hook deny 不可被规则 allow 覆盖)           │
1262/// │    └─ Hook ask → 记录 (后续可能被规则 allow 覆盖)                   │
1263/// │                                                                   │
1264/// │  Phase 3: 规则求值                                                 │
1265/// │    ├─ Rule deny → Deny                                            │
1266/// │    ├─ Rule allow → 但 Hook deny 仍然 deny                         │
1267/// │    └─ Rule ask → Ask                                              │
1268/// │                                                                   │
1269/// │  Phase 4: 缓存 + 模式                                             │
1270/// │    ├─ Session cache allow → Allow                                 │
1271/// │    ├─ Bypass mode → Allow (Policy deny 除外)                      │
1272/// │    └─ NonInteractive mode → Deny                                  │
1273/// │                                                                   │
1274/// │  Phase 5: 回退                                                    │
1275/// │    └─ → Ask                                                       │
1276/// └──────────────────────────────────────────────────────────────────┘
1277/// ```
1278///
1279/// ## 重要约束
1280/// - **Hook allow 不能绕过 Rule deny** — 规则是安全底线
1281/// - **Hook deny 不能被 Rule allow 覆盖** — Hook 拦截是显式安全决策
1282/// - **Policy deny 不可被任何方式覆盖** — 管理员策略是最终权威
1283///
1284/// # Examples
1285///
1286/// ```ignore
1287/// // katu-agent 中的实现示例(伪代码)
1288/// struct DefaultPermissionEngine {
1289///     ruleset: Ruleset,
1290///     cache: SessionPermissionCache,
1291///     denial_tracker: DenialTracker,
1292/// }
1293///
1294/// #[async_trait]
1295/// impl PermissionEngine for DefaultPermissionEngine {
1296///     async fn check(&self, input: PermissionCheckInput) -> PermissionDecision {
1297///         // Phase 1: Policy deny
1298///         if let Some(PermissionBehavior::Deny) = self.ruleset.evaluate_source(
1299///             RuleSource::Policy, &input.request.permission, &pattern
1300///         ) {
1301///             return PermissionDecision::deny(PermissionReason::Rule { source: RuleSource::Policy }, "Blocked by policy");
1302///         }
1303///
1304///         // Phase 2: Hook decision
1305///         if let Some(HookPermission::Deny { reason }) = &input.hook_decision {
1306///             return PermissionDecision::deny(PermissionReason::Hook, reason.as_deref().unwrap_or("Blocked by hook"));
1307///         }
1308///
1309///         // ... etc
1310///     }
1311/// }
1312/// ```
1313#[async_trait]
1314pub trait PermissionEngine: Send + Sync {
1315    /// 执行完整的权限检查管线。
1316    ///
1317    /// 返回 `PermissionDecision`:
1318    /// - `Allow` → Agent loop 继续执行工具
1319    /// - `Deny` → 将拒绝信息作为 ToolResult 反馈给 LLM
1320    /// - `Ask` → 调用 `prompt_user()` 获取用户回复
1321    async fn check(&self, input: PermissionCheckInput) -> PermissionDecision;
1322
1323    /// 向用户提示权限确认(交互式模式)。
1324    ///
1325    /// 由 Agent loop 在收到 `Ask` 决策后调用。
1326    /// 返回用户的回复,Agent loop 据此决定下一步:
1327    /// - `AllowOnce` → 继续执行
1328    /// - `AllowAlways` → 更新缓存/规则 + 继续执行
1329    /// - `DenyOnce` → 中止本次
1330    /// - `DenyAlways` → 更新缓存 + 中止
1331    /// - `DenyWithFeedback` → 中止 + 将 feedback 注入 LLM
1332    async fn prompt_user(&self, decision: &PermissionDecision) -> PermissionReply;
1333
1334    /// 应用用户回复 — 更新内部状态(缓存、规则等)。
1335    ///
1336    /// `request` 用于确定 always-allow 应持久化的 pattern。
1337    async fn apply_reply(
1338        &self,
1339        request: &PermissionRequest,
1340        reply: &PermissionReply,
1341    );
1342}
1343
1344/// 默认权限检查算法 — 纯函数版本,不涉及用户交互。
1345///
1346/// 适合在不实现完整 `PermissionEngine` trait 的场景下使用。
1347/// 只执行 Phase 1-4(规则 + Hook + 工具 + 缓存 + 模式),不处理 Ask 的用户交互。
1348///
1349/// # Returns
1350/// - `Allow` / `Deny` — 确定性决策
1351/// - `Ask` — 需要上层调用 `prompt_user()`
1352///
1353/// # Examples
1354///
1355/// ```
1356/// use katu_core::hook::HookPermission;
1357/// use katu_core::permission::*;
1358///
1359/// let mut ruleset = Ruleset::new();
1360/// ruleset.add(PermissionRule::deny(RuleSource::Policy, "bash", "rm *"));
1361///
1362/// let cache = SessionPermissionCache::new();
1363///
1364/// let request = PermissionRequest::new("bash", "rm -rf /");
1365/// let input = PermissionCheckInput::new(request, PermissionMode::Default);
1366///
1367/// let decision = evaluate_permission(&ruleset, &cache, &input);
1368/// assert!(decision.is_deny());
1369/// ```
1370pub fn evaluate_permission(
1371    ruleset: &Ruleset,
1372    cache: &SessionPermissionCache,
1373    input: &PermissionCheckInput,
1374) -> PermissionDecision {
1375    let permission_key = &input.request.permission;
1376
1377    // 对所有 pattern 进行检查(所有 pattern 必须 allow 才能 allow)
1378    // 任一 pattern deny → deny;任一 ask → ask(除非有 deny)
1379    let mut overall_behavior: Option<PermissionBehavior> = None;
1380
1381    for pattern in &input.request.patterns {
1382        let decision = evaluate_single(ruleset, cache, input, permission_key, pattern);
1383        match decision {
1384            PermissionBehavior::Deny => {
1385                // 任何 deny → 立即返回
1386                let reason = determine_deny_reason(ruleset, input, permission_key, pattern);
1387                return PermissionDecision::deny(
1388                    reason,
1389                    format!("Permission denied: {}({})", permission_key, pattern),
1390                );
1391            }
1392            PermissionBehavior::Ask => {
1393                if overall_behavior != Some(PermissionBehavior::Deny) {
1394                    overall_behavior = Some(PermissionBehavior::Ask);
1395                }
1396            }
1397            PermissionBehavior::Allow => {
1398                if overall_behavior.is_none() {
1399                    overall_behavior = Some(PermissionBehavior::Allow);
1400                }
1401            }
1402        }
1403    }
1404
1405    // 如果 patterns 为空,用 "*" 做检查
1406    if input.request.patterns.is_empty() {
1407        let decision = evaluate_single(ruleset, cache, input, permission_key, "*");
1408        overall_behavior = Some(decision);
1409        if decision.is_deny() {
1410            let reason = determine_deny_reason(ruleset, input, permission_key, "*");
1411            return PermissionDecision::deny(reason, format!("Permission denied: {}", permission_key));
1412        }
1413    }
1414
1415    match overall_behavior.unwrap_or(PermissionBehavior::Ask) {
1416        PermissionBehavior::Allow => {
1417            PermissionDecision::allow(PermissionReason::Rule { source: RuleSource::Session })
1418        }
1419        PermissionBehavior::Ask => {
1420            PermissionDecision::ask(format!(
1421                "Allow {} to execute {}?",
1422                input.request.tool_name.as_deref().unwrap_or(permission_key),
1423                input.request.patterns.first().map(|s| s.as_str()).unwrap_or("*"),
1424            ))
1425        }
1426        PermissionBehavior::Deny => {
1427            // 已在循环中 early-return 处理,逻辑上不应到达这里
1428            unreachable!("Deny should have been returned early in the loop")
1429        }
1430    }
1431}
1432
1433/// 单个 (permission_key, pattern) 的权限求值 — 完整管线。
1434fn evaluate_single(
1435    ruleset: &Ruleset,
1436    cache: &SessionPermissionCache,
1437    input: &PermissionCheckInput,
1438    permission_key: &str,
1439    pattern: &str,
1440) -> PermissionBehavior {
1441    // Phase 1: Policy deny(不可覆盖)
1442    let policy_rules: Vec<_> = ruleset
1443        .rules()
1444        .iter()
1445        .filter(|r| r.source == RuleSource::Policy && r.behavior.is_deny())
1446        .filter(|r| r.matches(permission_key, pattern))
1447        .collect();
1448    if !policy_rules.is_empty() {
1449        return PermissionBehavior::Deny;
1450    }
1451
1452    // Phase 1b: Tool-level deny
1453    if let Some(ref tool_result) = input.tool_check {
1454        match tool_result {
1455            PermissionResult::Deny { .. } => return PermissionBehavior::Deny,
1456            PermissionResult::Ask { .. } => { /* 继续,后面处理 */ }
1457            _ => {}
1458        }
1459    }
1460
1461    // Phase 2: Hook deny(不可被规则 allow 覆盖)
1462    if let Some(ref hook_decision) = input.hook_decision {
1463        if hook_decision.is_deny() {
1464            return PermissionBehavior::Deny;
1465        }
1466    }
1467
1468    // Phase 3: 规则求值
1469    if let Some(rule_behavior) = ruleset.evaluate(permission_key, pattern) {
1470        match rule_behavior {
1471            PermissionBehavior::Deny => return PermissionBehavior::Deny,
1472            PermissionBehavior::Allow => {
1473                // 规则 allow — 但 Hook ask 可能要求确认
1474                if let Some(ref hook_decision) = input.hook_decision {
1475                    if hook_decision.is_ask() {
1476                        return PermissionBehavior::Ask;
1477                    }
1478                }
1479                // 工具 ask 也需要确认
1480                if let Some(PermissionResult::Ask { .. }) = input.tool_check {
1481                    return PermissionBehavior::Ask;
1482                }
1483                return PermissionBehavior::Allow;
1484            }
1485            PermissionBehavior::Ask => { /* 继续到缓存+模式检查 */ }
1486        }
1487    }
1488
1489    // Phase 4: Session cache
1490    if let Some(cached) = cache.check(permission_key, pattern) {
1491        match cached {
1492            PermissionBehavior::Allow => return PermissionBehavior::Allow,
1493            PermissionBehavior::Deny => return PermissionBehavior::Deny,
1494            _ => {}
1495        }
1496    }
1497
1498    // Phase 4b: Hook allow(规则没有 deny 时,Hook allow 可以生效)
1499    if let Some(ref hook_decision) = input.hook_decision {
1500        if hook_decision.is_allow() {
1501            return PermissionBehavior::Allow;
1502        }
1503    }
1504
1505    // Phase 4c: Mode 变换
1506    match input.mode {
1507        PermissionMode::Bypass => return PermissionBehavior::Allow,
1508        PermissionMode::NonInteractive => return PermissionBehavior::Deny,
1509        PermissionMode::Plan => return PermissionBehavior::Deny,
1510        _ => {}
1511    }
1512
1513    // Phase 5: 回退
1514    PermissionBehavior::Ask
1515}
1516
1517/// 确定 deny 的具体原因。
1518fn determine_deny_reason(
1519    ruleset: &Ruleset,
1520    input: &PermissionCheckInput,
1521    permission_key: &str,
1522    pattern: &str,
1523) -> PermissionReason {
1524    // 检查是否是 Policy deny
1525    let is_policy = ruleset
1526        .rules()
1527        .iter()
1528        .any(|r| r.source == RuleSource::Policy && r.behavior.is_deny() && r.matches(permission_key, pattern));
1529    if is_policy {
1530        return PermissionReason::Rule { source: RuleSource::Policy };
1531    }
1532
1533    // 检查是否是 Hook deny
1534    if let Some(ref hook) = input.hook_decision {
1535        if hook.is_deny() {
1536            return PermissionReason::Hook;
1537        }
1538    }
1539
1540    // 检查是否是 Tool deny
1541    if let Some(PermissionResult::Deny { .. }) = &input.tool_check {
1542        return PermissionReason::ToolCheck;
1543    }
1544
1545    // 检查规则来源
1546    for source in &[RuleSource::User, RuleSource::Project, RuleSource::Session, RuleSource::Default] {
1547        let has_deny = ruleset
1548            .rules()
1549            .iter()
1550            .any(|r| r.source == *source && r.behavior.is_deny() && r.matches(permission_key, pattern));
1551        if has_deny {
1552            return PermissionReason::Rule { source: *source };
1553        }
1554    }
1555
1556    // Mode deny
1557    if input.mode.is_non_interactive() || input.mode.is_plan() {
1558        return PermissionReason::Mode;
1559    }
1560
1561    PermissionReason::Rule { source: RuleSource::Default }
1562}
1563
1564// ===========================================================================
1565// 通配符匹配
1566// ===========================================================================
1567
1568/// 通配符模式匹配。
1569///
1570/// 支持 `*` 作为通配符(匹配零个或多个字符)。
1571/// 不支持 `?`(单字符通配符)— 保持简单。
1572///
1573/// # Examples
1574///
1575/// ```
1576/// use katu_core::permission::wildcard_match;
1577///
1578/// assert!(wildcard_match("hello", "hello"));
1579/// assert!(wildcard_match("hello", "*"));
1580/// assert!(wildcard_match("hello world", "hello *"));
1581/// assert!(wildcard_match("foo.rs", "*.rs"));
1582/// assert!(wildcard_match("/etc/shadow", "/etc/*"));
1583/// assert!(!wildcard_match("foo.ts", "*.rs"));
1584/// ```
1585pub fn wildcard_match(value: &str, pattern: &str) -> bool {
1586    if pattern == "*" {
1587        return true;
1588    }
1589    if !pattern.contains('*') {
1590        return value == pattern;
1591    }
1592
1593    let parts: Vec<&str> = pattern.split('*').collect();
1594
1595    // 检查首段(必须是前缀)
1596    if !parts[0].is_empty() && !value.starts_with(parts[0]) {
1597        return false;
1598    }
1599
1600    // 检查尾段(必须是后缀)
1601    let last = parts[parts.len() - 1];
1602    if !last.is_empty() && !value.ends_with(last) {
1603        return false;
1604    }
1605
1606    // 逐段贪心匹配中间部分
1607    let mut pos = parts[0].len();
1608    for part in &parts[1..parts.len() - 1] {
1609        if part.is_empty() {
1610            continue;
1611        }
1612        match value[pos..].find(part) {
1613            Some(idx) => pos += idx + part.len(),
1614            None => return false,
1615        }
1616    }
1617
1618    // 确保尾段不与中间段重叠
1619    if !last.is_empty() {
1620        let tail_start = value.len() - last.len();
1621        if pos > tail_start {
1622            return false;
1623        }
1624    }
1625
1626    true
1627}
1628
1629// ===========================================================================
1630// Tests
1631// ===========================================================================
1632
1633#[cfg(test)]
1634mod tests {
1635    use super::*;
1636    use serde_json::json;
1637
1638    // -- PermissionBehavior --
1639
1640    #[test]
1641    fn test_behavior_variants() {
1642        assert!(PermissionBehavior::Allow.is_allow());
1643        assert!(PermissionBehavior::Deny.is_deny());
1644        assert!(PermissionBehavior::Ask.is_ask());
1645    }
1646
1647    #[test]
1648    fn test_behavior_strictness_order() {
1649        assert!(PermissionBehavior::Deny.strictness() > PermissionBehavior::Ask.strictness());
1650        assert!(PermissionBehavior::Ask.strictness() > PermissionBehavior::Allow.strictness());
1651    }
1652
1653    #[test]
1654    fn test_behavior_serde_roundtrip() {
1655        for b in [
1656            PermissionBehavior::Allow,
1657            PermissionBehavior::Deny,
1658            PermissionBehavior::Ask,
1659        ] {
1660            let json_str = serde_json::to_string(&b).unwrap();
1661            let restored: PermissionBehavior = serde_json::from_str(&json_str).unwrap();
1662            assert_eq!(b, restored);
1663        }
1664    }
1665
1666    // -- PermissionMode --
1667
1668    #[test]
1669    fn test_mode_default() {
1670        assert_eq!(PermissionMode::default(), PermissionMode::Default);
1671    }
1672
1673    #[test]
1674    fn test_mode_transform_ask() {
1675        assert_eq!(PermissionMode::Default.transform_ask(), PermissionBehavior::Ask);
1676        assert_eq!(PermissionMode::Bypass.transform_ask(), PermissionBehavior::Allow);
1677        assert_eq!(PermissionMode::NonInteractive.transform_ask(), PermissionBehavior::Deny);
1678        assert_eq!(PermissionMode::Plan.transform_ask(), PermissionBehavior::Ask);
1679        assert_eq!(PermissionMode::AcceptEdits.transform_ask(), PermissionBehavior::Ask);
1680    }
1681
1682    #[test]
1683    fn test_mode_serde_roundtrip() {
1684        for mode in [
1685            PermissionMode::Default,
1686            PermissionMode::Plan,
1687            PermissionMode::AcceptEdits,
1688            PermissionMode::Bypass,
1689            PermissionMode::NonInteractive,
1690        ] {
1691            let json_str = serde_json::to_string(&mode).unwrap();
1692            let restored: PermissionMode = serde_json::from_str(&json_str).unwrap();
1693            assert_eq!(mode, restored);
1694        }
1695    }
1696
1697    // -- RuleSource --
1698
1699    #[test]
1700    fn test_rule_source_priority_order() {
1701        assert!(RuleSource::Policy.priority() > RuleSource::User.priority());
1702        assert!(RuleSource::User.priority() > RuleSource::Project.priority());
1703        assert!(RuleSource::Project.priority() > RuleSource::Session.priority());
1704        assert!(RuleSource::Session.priority() > RuleSource::Default.priority());
1705    }
1706
1707    #[test]
1708    fn test_rule_source_ord() {
1709        let mut sources = vec![
1710            RuleSource::Session,
1711            RuleSource::Policy,
1712            RuleSource::Default,
1713            RuleSource::User,
1714            RuleSource::Project,
1715        ];
1716        sources.sort();
1717        assert_eq!(
1718            sources,
1719            vec![
1720                RuleSource::Default,
1721                RuleSource::Session,
1722                RuleSource::Project,
1723                RuleSource::User,
1724                RuleSource::Policy,
1725            ]
1726        );
1727    }
1728
1729    #[test]
1730    fn test_rule_source_immutable() {
1731        assert!(RuleSource::Policy.is_immutable());
1732        assert!(!RuleSource::User.is_immutable());
1733        assert!(!RuleSource::Session.is_immutable());
1734    }
1735
1736    // -- PermissionRule --
1737
1738    #[test]
1739    fn test_rule_new() {
1740        let rule = PermissionRule::new(
1741            RuleSource::User,
1742            PermissionBehavior::Allow,
1743            "read",
1744            "*",
1745        );
1746        assert_eq!(rule.source, RuleSource::User);
1747        assert_eq!(rule.behavior, PermissionBehavior::Allow);
1748        assert_eq!(rule.permission, "read");
1749        assert_eq!(rule.pattern, "*");
1750    }
1751
1752    #[test]
1753    fn test_rule_shortcuts() {
1754        let allow = PermissionRule::allow(RuleSource::User, "read", "*");
1755        assert!(allow.behavior.is_allow());
1756
1757        let deny = PermissionRule::deny(RuleSource::Policy, "bash", "rm *");
1758        assert!(deny.behavior.is_deny());
1759
1760        let ask = PermissionRule::ask(RuleSource::Project, "edit", "*.env");
1761        assert!(ask.behavior.is_ask());
1762    }
1763
1764    #[test]
1765    fn test_rule_matches_exact() {
1766        let rule = PermissionRule::deny(RuleSource::Policy, "bash", "rm -rf /");
1767        assert!(rule.matches("bash", "rm -rf /"));
1768        assert!(!rule.matches("bash", "ls -la"));
1769        assert!(!rule.matches("edit", "rm -rf /"));
1770    }
1771
1772    #[test]
1773    fn test_rule_matches_wildcard() {
1774        let rule = PermissionRule::deny(RuleSource::Policy, "bash", "rm *");
1775        assert!(rule.matches("bash", "rm -rf /"));
1776        assert!(rule.matches("bash", "rm file.txt"));
1777        assert!(!rule.matches("bash", "ls -la"));
1778    }
1779
1780    #[test]
1781    fn test_rule_matches_permission_wildcard() {
1782        let rule = PermissionRule::allow(RuleSource::User, "*", "*");
1783        assert!(rule.matches("bash", "anything"));
1784        assert!(rule.matches("edit", "anything"));
1785    }
1786
1787    #[test]
1788    fn test_rule_serde_roundtrip() {
1789        let rule = PermissionRule::deny(RuleSource::Policy, "bash", "rm *");
1790        let json_str = serde_json::to_string(&rule).unwrap();
1791        let restored: PermissionRule = serde_json::from_str(&json_str).unwrap();
1792        assert_eq!(rule, restored);
1793    }
1794
1795    // -- Ruleset --
1796
1797    #[test]
1798    fn test_ruleset_empty() {
1799        let ruleset = Ruleset::new();
1800        assert!(ruleset.is_empty());
1801        assert_eq!(ruleset.evaluate("bash", "ls"), None);
1802    }
1803
1804    #[test]
1805    fn test_ruleset_single_rule() {
1806        let mut ruleset = Ruleset::new();
1807        ruleset.add(PermissionRule::allow(RuleSource::User, "read", "*"));
1808        assert_eq!(ruleset.evaluate("read", "file.txt"), Some(PermissionBehavior::Allow));
1809        assert_eq!(ruleset.evaluate("bash", "ls"), None);
1810    }
1811
1812    #[test]
1813    fn test_ruleset_last_match_wins_same_source() {
1814        let mut ruleset = Ruleset::new();
1815        ruleset.add(PermissionRule::allow(RuleSource::User, "bash", "*"));
1816        ruleset.add(PermissionRule::deny(RuleSource::User, "bash", "rm *"));
1817
1818        // "rm -rf /" 匹配两条规则,last-match-wins → deny
1819        assert_eq!(ruleset.evaluate("bash", "rm -rf /"), Some(PermissionBehavior::Deny));
1820        // "ls" 只匹配第一条 → allow
1821        assert_eq!(ruleset.evaluate("bash", "ls"), Some(PermissionBehavior::Allow));
1822    }
1823
1824    #[test]
1825    fn test_ruleset_higher_source_wins() {
1826        let mut ruleset = Ruleset::new();
1827        // Session: allow bash *
1828        ruleset.add(PermissionRule::allow(RuleSource::Session, "bash", "*"));
1829        // Policy: deny bash(rm *)
1830        ruleset.add(PermissionRule::deny(RuleSource::Policy, "bash", "rm *"));
1831
1832        // Policy > Session: "rm -rf /" → deny (Policy wins)
1833        assert_eq!(ruleset.evaluate("bash", "rm -rf /"), Some(PermissionBehavior::Deny));
1834        // "ls" 只匹配 Session allow → allow
1835        assert_eq!(ruleset.evaluate("bash", "ls"), Some(PermissionBehavior::Allow));
1836    }
1837
1838    #[test]
1839    fn test_ruleset_policy_deny_cannot_be_overridden() {
1840        let mut ruleset = Ruleset::new();
1841        ruleset.add(PermissionRule::deny(RuleSource::Policy, "bash", "rm *"));
1842        ruleset.add(PermissionRule::allow(RuleSource::User, "bash", "*"));
1843        ruleset.add(PermissionRule::allow(RuleSource::Session, "bash", "rm *"));
1844
1845        // Policy deny 不可被任何低优先级覆盖
1846        assert_eq!(ruleset.evaluate("bash", "rm file"), Some(PermissionBehavior::Deny));
1847    }
1848
1849    #[test]
1850    fn test_ruleset_remove_source() {
1851        let mut ruleset = Ruleset::new();
1852        ruleset.add(PermissionRule::allow(RuleSource::User, "read", "*"));
1853        ruleset.add(PermissionRule::deny(RuleSource::Session, "bash", "*"));
1854        assert_eq!(ruleset.len(), 2);
1855
1856        ruleset.remove_source(RuleSource::Session);
1857        assert_eq!(ruleset.len(), 1);
1858        assert_eq!(ruleset.evaluate("bash", "ls"), None);
1859    }
1860
1861    #[test]
1862    fn test_ruleset_has_deny_for() {
1863        let mut ruleset = Ruleset::new();
1864        ruleset.add(PermissionRule::deny(RuleSource::Policy, "bash", "rm *"));
1865        ruleset.add(PermissionRule::allow(RuleSource::User, "read", "*"));
1866
1867        assert!(ruleset.has_deny_for("bash"));
1868        assert!(!ruleset.has_deny_for("read"));
1869        assert!(!ruleset.has_deny_for("edit"));
1870    }
1871
1872    // -- PermissionRequest --
1873
1874    #[test]
1875    fn test_permission_request_builder() {
1876        let req = PermissionRequest::new("bash", "rm -rf /tmp")
1877            .with_tool_name("bash")
1878            .with_call_id(ToolCallId::new("call_1"))
1879            .with_metadata(json!({"cwd": "/home/user"}))
1880            .with_always_allow(["rm *"]);
1881
1882        assert_eq!(req.permission, "bash");
1883        assert_eq!(req.patterns, vec!["rm -rf /tmp"]);
1884        assert_eq!(req.tool_name, Some("bash".into()));
1885        assert_eq!(req.call_id.unwrap().as_str(), "call_1");
1886        assert_eq!(req.always_allow_patterns, vec!["rm *"]);
1887    }
1888
1889    #[test]
1890    fn test_permission_request_multi_pattern() {
1891        let req = PermissionRequest::with_patterns("edit", ["src/main.rs", "src/lib.rs"]);
1892        assert_eq!(req.patterns.len(), 2);
1893    }
1894
1895    // -- PermissionDecision --
1896
1897    #[test]
1898    fn test_decision_allow() {
1899        let d = PermissionDecision::allow(PermissionReason::Rule {
1900            source: RuleSource::User,
1901        });
1902        assert!(d.is_allow());
1903        assert_eq!(d.behavior(), PermissionBehavior::Allow);
1904    }
1905
1906    #[test]
1907    fn test_decision_deny() {
1908        let d = PermissionDecision::deny(
1909            PermissionReason::Rule {
1910                source: RuleSource::Policy,
1911            },
1912            "Blocked by policy",
1913        );
1914        assert!(d.is_deny());
1915        if let PermissionDecision::Deny { message, .. } = &d {
1916            assert_eq!(message, "Blocked by policy");
1917        }
1918    }
1919
1920    #[test]
1921    fn test_decision_ask() {
1922        let d = PermissionDecision::ask("Allow bash to run 'rm -rf /tmp'?");
1923        assert!(d.is_ask());
1924    }
1925
1926    #[test]
1927    fn test_decision_ask_with_suggestions() {
1928        let d = PermissionDecision::ask_with_suggestions(
1929            "Allow?",
1930            vec![PermissionUpdate::add_rule(
1931                RuleSource::Session,
1932                PermissionRule::allow(RuleSource::Session, "bash", "rm *"),
1933            )],
1934        );
1935        if let PermissionDecision::Ask { suggestions, .. } = &d {
1936            assert_eq!(suggestions.len(), 1);
1937        }
1938    }
1939
1940    // -- PermissionResult --
1941
1942    #[test]
1943    fn test_permission_result_variants() {
1944        assert!(PermissionResult::Allow.is_allow());
1945        assert!(PermissionResult::deny("no").is_deny());
1946        assert!(PermissionResult::ask("confirm?").is_ask());
1947        assert!(PermissionResult::Passthrough.is_passthrough());
1948    }
1949
1950    // -- PermissionReply --
1951
1952    #[test]
1953    fn test_reply_is_allow() {
1954        assert!(PermissionReply::AllowOnce.is_allow());
1955        assert!(PermissionReply::AllowAlways.is_allow());
1956        assert!(!PermissionReply::DenyOnce.is_allow());
1957    }
1958
1959    #[test]
1960    fn test_reply_is_deny() {
1961        assert!(PermissionReply::DenyOnce.is_deny());
1962        assert!(PermissionReply::DenyAlways.is_deny());
1963        assert!(PermissionReply::DenyWithFeedback {
1964            feedback: "use a safer command".into()
1965        }
1966        .is_deny());
1967        assert!(!PermissionReply::AllowOnce.is_deny());
1968    }
1969
1970    #[test]
1971    fn test_reply_is_always() {
1972        assert!(PermissionReply::AllowAlways.is_always());
1973        assert!(PermissionReply::DenyAlways.is_always());
1974        assert!(!PermissionReply::AllowOnce.is_always());
1975        assert!(!PermissionReply::DenyOnce.is_always());
1976    }
1977
1978    #[test]
1979    fn test_reply_feedback() {
1980        let reply = PermissionReply::DenyWithFeedback {
1981            feedback: "try ls instead".into(),
1982        };
1983        assert!(reply.has_feedback());
1984        assert_eq!(reply.feedback(), Some("try ls instead"));
1985
1986        assert!(!PermissionReply::DenyOnce.has_feedback());
1987        assert_eq!(PermissionReply::DenyOnce.feedback(), None);
1988    }
1989
1990    #[test]
1991    fn test_reply_serde_roundtrip() {
1992        for reply in [
1993            PermissionReply::AllowOnce,
1994            PermissionReply::AllowAlways,
1995            PermissionReply::DenyOnce,
1996            PermissionReply::DenyAlways,
1997            PermissionReply::DenyWithFeedback {
1998                feedback: "feedback".into(),
1999            },
2000        ] {
2001            let json_str = serde_json::to_string(&reply).unwrap();
2002            let restored: PermissionReply = serde_json::from_str(&json_str).unwrap();
2003            assert_eq!(reply, restored);
2004        }
2005    }
2006
2007    // -- SessionPermissionCache --
2008
2009    #[test]
2010    fn test_cache_empty() {
2011        let cache = SessionPermissionCache::new();
2012        assert!(cache.is_empty());
2013        assert_eq!(cache.check("bash", "ls"), None);
2014    }
2015
2016    #[test]
2017    fn test_cache_allow_always() {
2018        let mut cache = SessionPermissionCache::new();
2019        cache.allow_always("bash", "git *");
2020
2021        assert_eq!(cache.check("bash", "git pull"), Some(PermissionBehavior::Allow));
2022        assert_eq!(cache.check("bash", "git push"), Some(PermissionBehavior::Allow));
2023        assert_eq!(cache.check("bash", "rm -rf /"), None);
2024    }
2025
2026    #[test]
2027    fn test_cache_deny_always() {
2028        let mut cache = SessionPermissionCache::new();
2029        cache.deny_always("bash", "rm *");
2030
2031        assert_eq!(cache.check("bash", "rm file.txt"), Some(PermissionBehavior::Deny));
2032        assert_eq!(cache.check("bash", "ls"), None);
2033    }
2034
2035    #[test]
2036    fn test_cache_last_match_wins() {
2037        let mut cache = SessionPermissionCache::new();
2038        cache.allow_always("bash", "*");
2039        cache.deny_always("bash", "rm *");
2040
2041        // "rm file" 匹配两条,last-match deny 获胜
2042        assert_eq!(cache.check("bash", "rm file"), Some(PermissionBehavior::Deny));
2043        // "ls" 只匹配第一条 → allow
2044        assert_eq!(cache.check("bash", "ls"), Some(PermissionBehavior::Allow));
2045    }
2046
2047    #[test]
2048    fn test_cache_clear() {
2049        let mut cache = SessionPermissionCache::new();
2050        cache.allow_always("bash", "*");
2051        assert!(!cache.is_empty());
2052
2053        cache.clear();
2054        assert!(cache.is_empty());
2055        assert_eq!(cache.check("bash", "ls"), None);
2056    }
2057
2058    #[test]
2059    fn test_cache_to_ruleset() {
2060        let mut cache = SessionPermissionCache::new();
2061        cache.allow_always("bash", "git *");
2062        cache.deny_always("bash", "rm *");
2063
2064        let ruleset = cache.to_ruleset();
2065        assert_eq!(ruleset.len(), 2);
2066        assert_eq!(ruleset.evaluate("bash", "git pull"), Some(PermissionBehavior::Allow));
2067        assert_eq!(ruleset.evaluate("bash", "rm file"), Some(PermissionBehavior::Deny));
2068    }
2069
2070    // -- DenialTracker --
2071
2072    #[test]
2073    fn test_denial_tracker_basic() {
2074        let tracker = DenialTracker::new(3);
2075        assert!(!tracker.is_tripped());
2076        assert_eq!(tracker.consecutive_count(), 0);
2077        assert_eq!(tracker.total_count(), 0);
2078    }
2079
2080    #[test]
2081    fn test_denial_tracker_trips_at_threshold() {
2082        let mut tracker = DenialTracker::new(3);
2083        tracker.record_denial();
2084        tracker.record_denial();
2085        assert!(!tracker.is_tripped());
2086
2087        tracker.record_denial();
2088        assert!(tracker.is_tripped());
2089        assert_eq!(tracker.consecutive_count(), 3);
2090        assert_eq!(tracker.total_count(), 3);
2091    }
2092
2093    #[test]
2094    fn test_denial_tracker_allow_resets() {
2095        let mut tracker = DenialTracker::new(3);
2096        tracker.record_denial();
2097        tracker.record_denial();
2098        tracker.record_allow(); // 重置连续计数
2099
2100        assert!(!tracker.is_tripped());
2101        assert_eq!(tracker.consecutive_count(), 0);
2102        assert_eq!(tracker.total_count(), 2); // 总数不重置
2103    }
2104
2105    #[test]
2106    fn test_denial_tracker_reset() {
2107        let mut tracker = DenialTracker::new(2);
2108        tracker.record_denial();
2109        tracker.record_denial();
2110        assert!(tracker.is_tripped());
2111
2112        tracker.reset();
2113        assert!(!tracker.is_tripped());
2114    }
2115
2116    // -- wildcard_match --
2117
2118    #[test]
2119    fn test_wildcard_exact() {
2120        assert!(wildcard_match("hello", "hello"));
2121        assert!(!wildcard_match("hello", "world"));
2122    }
2123
2124    #[test]
2125    fn test_wildcard_star_all() {
2126        assert!(wildcard_match("anything", "*"));
2127        assert!(wildcard_match("", "*"));
2128    }
2129
2130    #[test]
2131    fn test_wildcard_prefix() {
2132        assert!(wildcard_match("hello world", "hello *"));
2133        assert!(wildcard_match("hello", "hello*"));
2134        assert!(!wildcard_match("world hello", "hello *"));
2135    }
2136
2137    #[test]
2138    fn test_wildcard_suffix() {
2139        assert!(wildcard_match("file.rs", "*.rs"));
2140        assert!(wildcard_match("test.rs", "*.rs"));
2141        assert!(!wildcard_match("file.ts", "*.rs"));
2142    }
2143
2144    #[test]
2145    fn test_wildcard_middle() {
2146        assert!(wildcard_match("/etc/shadow", "/etc/*"));
2147        assert!(wildcard_match("/home/user/file.txt", "/home/*/file.txt"));
2148        assert!(!wildcard_match("/tmp/file.txt", "/home/*/file.txt"));
2149    }
2150
2151    #[test]
2152    fn test_wildcard_multiple_stars() {
2153        assert!(wildcard_match("/home/user/project/src/main.rs", "/home/*/project/*.rs"));
2154        assert!(!wildcard_match("/home/user/other/src/main.rs", "/home/*/project/*.rs"));
2155    }
2156
2157    #[test]
2158    fn test_wildcard_empty_pattern_segment() {
2159        // "**" has empty segment between stars — should still work
2160        assert!(wildcard_match("anything", "**"));
2161    }
2162
2163    // -- PermissionUpdate --
2164
2165    #[test]
2166    fn test_permission_update_add_rule() {
2167        let update = PermissionUpdate::add_rule(
2168            RuleSource::Session,
2169            PermissionRule::allow(RuleSource::Session, "bash", "git *"),
2170        );
2171        assert!(matches!(update, PermissionUpdate::AddRule { .. }));
2172    }
2173
2174    #[test]
2175    fn test_permission_update_serde_roundtrip() {
2176        let update = PermissionUpdate::set_mode(PermissionMode::Bypass);
2177        let json_str = serde_json::to_string(&update).unwrap();
2178        let restored: PermissionUpdate = serde_json::from_str(&json_str).unwrap();
2179        assert_eq!(update, restored);
2180    }
2181
2182    // -- Integration: Ruleset + SessionCache --
2183
2184    // ================================================================
2185    // 集成测试: evaluate_permission — Hook + 规则 + 工具 + 缓存 + 模式
2186    // ================================================================
2187    //
2188    // 完整管线流程(katu-agent 中的 Agent Loop 按此顺序调用):
2189    //
2190    //   LLM 请求 tool_call(bash, {"command": "rm -rf /"})
2191    //     │
2192    //     ▼
2193    //   ① Hook(PreToolUse) → HookPermission (allow/deny/ask)
2194    //     │                    ↓ aggregated
2195    //     ▼
2196    //   ② Tool.check_permissions(args) → PermissionResult
2197    //     │                    ↓
2198    //     ▼
2199    //   ③ PermissionCheckInput { request, hook_decision, tool_check, mode }
2200    //     │                    ↓
2201    //     ▼
2202    //   ④ evaluate_permission(ruleset, cache, input) → PermissionDecision
2203    //     │
2204    //     ├─ Allow → tool.validate() → tool.execute()
2205    //     ├─ Deny  → ToolOutput::error(message) → 反馈 LLM
2206    //     └─ Ask   → prompt_user() → apply_reply() → Allow/Deny
2207    //                                                    │
2208    //     ⑤ Hook(PostToolUse) ←────────────────────────────
2209
2210    #[test]
2211    fn test_pipeline_policy_deny_beats_everything() {
2212        let mut ruleset = Ruleset::new();
2213        ruleset.add(PermissionRule::deny(RuleSource::Policy, "bash", "rm *"));
2214
2215        let cache = SessionPermissionCache::new();
2216
2217        // 即使 Hook allow + Bypass mode,Policy deny 仍然获胜
2218        let input = PermissionCheckInput::new(
2219            PermissionRequest::new("bash", "rm -rf /"),
2220            PermissionMode::Bypass,
2221        )
2222        .with_hook_decision(HookPermission::Allow);
2223
2224        let decision = evaluate_permission(&ruleset, &cache, &input);
2225        assert!(decision.is_deny());
2226        if let PermissionDecision::Deny { reason, .. } = &decision {
2227            assert!(matches!(reason, PermissionReason::Rule { source: RuleSource::Policy }));
2228        }
2229    }
2230
2231    #[test]
2232    fn test_pipeline_hook_deny_beats_rule_allow() {
2233        let mut ruleset = Ruleset::new();
2234        ruleset.add(PermissionRule::allow(RuleSource::User, "bash", "*"));
2235
2236        let cache = SessionPermissionCache::new();
2237
2238        // 规则说 allow,但 Hook 说 deny → deny
2239        let input = PermissionCheckInput::new(
2240            PermissionRequest::new("bash", "curl evil.com"),
2241            PermissionMode::Default,
2242        )
2243        .with_hook_decision(HookPermission::Deny {
2244            reason: Some("Suspicious URL detected".into()),
2245        });
2246
2247        let decision = evaluate_permission(&ruleset, &cache, &input);
2248        assert!(decision.is_deny());
2249        if let PermissionDecision::Deny { reason, .. } = &decision {
2250            assert!(matches!(reason, PermissionReason::Hook));
2251        }
2252    }
2253
2254    #[test]
2255    fn test_pipeline_hook_allow_cannot_bypass_rule_deny() {
2256        let mut ruleset = Ruleset::new();
2257        ruleset.add(PermissionRule::deny(RuleSource::User, "bash", "rm *"));
2258
2259        let cache = SessionPermissionCache::new();
2260
2261        // Hook 说 allow,但规则说 deny → deny(规则是安全底线)
2262        let input = PermissionCheckInput::new(
2263            PermissionRequest::new("bash", "rm file.txt"),
2264            PermissionMode::Default,
2265        )
2266        .with_hook_decision(HookPermission::Allow);
2267
2268        let decision = evaluate_permission(&ruleset, &cache, &input);
2269        assert!(decision.is_deny());
2270    }
2271
2272    #[test]
2273    fn test_pipeline_tool_deny_is_immediate() {
2274        let ruleset = Ruleset::new(); // 无规则
2275        let cache = SessionPermissionCache::new();
2276
2277        // 工具自身拒绝(如路径安全检查)
2278        let input = PermissionCheckInput::new(
2279            PermissionRequest::new("edit", "/etc/shadow"),
2280            PermissionMode::Bypass, // 即使 bypass
2281        )
2282        .with_tool_check(PermissionResult::deny("Cannot edit system files"));
2283
2284        let decision = evaluate_permission(&ruleset, &cache, &input);
2285        assert!(decision.is_deny());
2286        if let PermissionDecision::Deny { reason, .. } = &decision {
2287            assert!(matches!(reason, PermissionReason::ToolCheck));
2288        }
2289    }
2290
2291    #[test]
2292    fn test_pipeline_hook_ask_overrides_rule_allow() {
2293        let mut ruleset = Ruleset::new();
2294        ruleset.add(PermissionRule::allow(RuleSource::User, "bash", "*"));
2295
2296        let cache = SessionPermissionCache::new();
2297
2298        // 规则 allow,但 Hook 说 ask → ask(Hook 要求确认)
2299        let input = PermissionCheckInput::new(
2300            PermissionRequest::new("bash", "git push --force"),
2301            PermissionMode::Default,
2302        )
2303        .with_hook_decision(HookPermission::Ask {
2304            message: Some("Force push detected, confirm?".into()),
2305        });
2306
2307        let decision = evaluate_permission(&ruleset, &cache, &input);
2308        assert!(decision.is_ask());
2309    }
2310
2311    #[test]
2312    fn test_pipeline_cache_allows_after_user_always() {
2313        let ruleset = Ruleset::new(); // 无规则 → 默认 ask
2314        let mut cache = SessionPermissionCache::new();
2315        cache.allow_always("bash", "git *");
2316
2317        let input = PermissionCheckInput::new(
2318            PermissionRequest::new("bash", "git pull"),
2319            PermissionMode::Default,
2320        );
2321
2322        let decision = evaluate_permission(&ruleset, &cache, &input);
2323        assert!(decision.is_allow());
2324    }
2325
2326    #[test]
2327    fn test_pipeline_bypass_mode_auto_allows() {
2328        let ruleset = Ruleset::new();
2329        let cache = SessionPermissionCache::new();
2330
2331        let input = PermissionCheckInput::new(
2332            PermissionRequest::new("bash", "anything"),
2333            PermissionMode::Bypass,
2334        );
2335
2336        let decision = evaluate_permission(&ruleset, &cache, &input);
2337        assert!(decision.is_allow());
2338    }
2339
2340    #[test]
2341    fn test_pipeline_non_interactive_auto_denies() {
2342        let ruleset = Ruleset::new();
2343        let cache = SessionPermissionCache::new();
2344
2345        let input = PermissionCheckInput::new(
2346            PermissionRequest::new("bash", "anything"),
2347            PermissionMode::NonInteractive,
2348        );
2349
2350        let decision = evaluate_permission(&ruleset, &cache, &input);
2351        assert!(decision.is_deny());
2352        if let PermissionDecision::Deny { reason, .. } = &decision {
2353            assert!(matches!(reason, PermissionReason::Mode));
2354        }
2355    }
2356
2357    #[test]
2358    fn test_pipeline_no_rules_no_hook_defaults_to_ask() {
2359        let ruleset = Ruleset::new();
2360        let cache = SessionPermissionCache::new();
2361
2362        let input = PermissionCheckInput::new(
2363            PermissionRequest::new("bash", "ls -la"),
2364            PermissionMode::Default,
2365        );
2366
2367        let decision = evaluate_permission(&ruleset, &cache, &input);
2368        assert!(decision.is_ask());
2369    }
2370
2371    #[test]
2372    fn test_pipeline_hook_allow_works_when_no_rule_conflicts() {
2373        let ruleset = Ruleset::new(); // 无规则
2374        let cache = SessionPermissionCache::new();
2375
2376        // 无冲突规则时,Hook allow 生效
2377        let input = PermissionCheckInput::new(
2378            PermissionRequest::new("read", "file.txt"),
2379            PermissionMode::Default,
2380        )
2381        .with_hook_decision(HookPermission::Allow);
2382
2383        let decision = evaluate_permission(&ruleset, &cache, &input);
2384        assert!(decision.is_allow());
2385    }
2386
2387    #[test]
2388    fn test_pipeline_tool_ask_overrides_rule_allow() {
2389        let mut ruleset = Ruleset::new();
2390        ruleset.add(PermissionRule::allow(RuleSource::User, "edit", "*"));
2391
2392        let cache = SessionPermissionCache::new();
2393
2394        // 规则 allow,但工具要求确认(如 .git/ 路径)
2395        let input = PermissionCheckInput::new(
2396            PermissionRequest::new("edit", ".git/config"),
2397            PermissionMode::Default,
2398        )
2399        .with_tool_check(PermissionResult::ask("Editing .git/ files requires confirmation"));
2400
2401        let decision = evaluate_permission(&ruleset, &cache, &input);
2402        assert!(decision.is_ask());
2403    }
2404
2405    // -- HookPermission ↔ PermissionBehavior 转换 --
2406
2407    #[test]
2408    fn test_hook_permission_to_behavior() {
2409        assert_eq!(
2410            PermissionBehavior::from(HookPermission::Allow),
2411            PermissionBehavior::Allow
2412        );
2413        assert_eq!(
2414            PermissionBehavior::from(HookPermission::Deny { reason: None }),
2415            PermissionBehavior::Deny
2416        );
2417        assert_eq!(
2418            PermissionBehavior::from(HookPermission::Ask { message: None }),
2419            PermissionBehavior::Ask
2420        );
2421    }
2422
2423    #[test]
2424    fn test_behavior_to_hook_permission() {
2425        let perm: HookPermission = PermissionBehavior::Allow.into();
2426        assert!(perm.is_allow());
2427
2428        let perm: HookPermission = PermissionBehavior::Deny.into();
2429        assert!(perm.is_deny());
2430    }
2431}