claude_code_acp/session/
permission.rs

1//! Permission handling for tool execution
2//!
3//! This module provides permission checking using a strategy pattern,
4//! where each permission mode has its own strategy implementation.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8use std::sync::Arc;
9use tokio::sync::RwLock;
10
11use crate::permissions::strategies::{
12    AcceptEditsModeStrategy, BypassPermissionsModeStrategy, DefaultModeStrategy,
13    DontAskModeStrategy, PermissionModeStrategy, PlanModeStrategy,
14};
15use crate::settings::{PermissionChecker, PermissionDecision};
16use claude_code_agent_sdk::PermissionMode as SdkPermissionMode;
17
18/// Permission mode for tool execution
19///
20/// Controls how tool calls are approved during a session.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub enum PermissionMode {
24    /// Default mode - prompt for dangerous operations
25    Default,
26    /// Auto-approve file edits
27    AcceptEdits,
28    /// Planning mode - read-only operations
29    Plan,
30    /// Don't ask mode - deny if not pre-approved
31    DontAsk,
32    /// Bypass all permission checks (default mode for development)
33    #[default]
34    BypassPermissions,
35}
36
37impl PermissionMode {
38    /// Parse from string (ACP setMode request)
39    pub fn parse(s: &str) -> Option<Self> {
40        match s {
41            "default" => Some(Self::Default),
42            "acceptEdits" => Some(Self::AcceptEdits),
43            "plan" => Some(Self::Plan),
44            "dontAsk" => Some(Self::DontAsk),
45            "bypassPermissions" => Some(Self::BypassPermissions),
46            _ => None,
47        }
48    }
49
50    /// Convert to string for SDK
51    pub fn as_str(&self) -> &'static str {
52        match self {
53            Self::Default => "default",
54            Self::AcceptEdits => "acceptEdits",
55            Self::Plan => "plan",
56            Self::DontAsk => "dontAsk",
57            Self::BypassPermissions => "bypassPermissions",
58        }
59    }
60
61    /// Convert to SDK PermissionMode
62    ///
63    /// Note: SDK doesn't support DontAsk mode yet, so we map it to Default
64    pub fn to_sdk_mode(&self) -> SdkPermissionMode {
65        match self {
66            PermissionMode::Default => SdkPermissionMode::Default,
67            PermissionMode::AcceptEdits => SdkPermissionMode::AcceptEdits,
68            PermissionMode::Plan => SdkPermissionMode::Plan,
69            PermissionMode::DontAsk => {
70                // SDK doesn't support DontAsk yet, treat as Default
71                SdkPermissionMode::Default
72            }
73            PermissionMode::BypassPermissions => SdkPermissionMode::BypassPermissions,
74        }
75    }
76
77    /// Check if this mode allows write operations
78    pub fn allows_writes(&self) -> bool {
79        matches!(
80            self,
81            Self::Default | Self::AcceptEdits | Self::BypassPermissions
82        )
83    }
84
85    /// Check if this mode auto-approves edits
86    pub fn auto_approve_edits(&self) -> bool {
87        matches!(self, Self::AcceptEdits | Self::BypassPermissions)
88    }
89}
90
91/// Permission check result from the handler
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub enum ToolPermissionResult {
94    /// Tool execution is allowed (auto-approved or by rule)
95    Allowed,
96    /// Tool execution is blocked (by rule or mode)
97    Blocked { reason: String },
98    /// User should be asked for permission
99    NeedsPermission,
100}
101
102/// Permission handler for tool execution
103///
104/// Uses a strategy pattern where each permission mode has its own strategy.
105pub struct PermissionHandler {
106    mode: PermissionMode,
107    /// Strategy for current permission mode
108    strategy: Arc<dyn PermissionModeStrategy>,
109    /// Shared permission checker from settings (shared with hook)
110    checker: Option<Arc<RwLock<PermissionChecker>>>,
111}
112
113impl fmt::Debug for PermissionHandler {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        f.debug_struct("PermissionHandler")
116            .field("mode", &self.mode)
117            .field("strategy", &"<strategy>")
118            .field("checker", &self.checker)
119            .finish()
120    }
121}
122
123impl Default for PermissionHandler {
124    fn default() -> Self {
125        Self {
126            mode: PermissionMode::Default,
127            strategy: Arc::new(DefaultModeStrategy),
128            checker: None,
129        }
130    }
131}
132
133impl PermissionHandler {
134    /// Create a new permission handler
135    ///
136    /// Uses Default mode (standard behavior with permission prompts).
137    pub fn new() -> Self {
138        Self::default()
139    }
140
141    /// Create with a specific mode
142    pub fn with_mode(mode: PermissionMode) -> Self {
143        Self {
144            mode,
145            strategy: Self::create_strategy(mode),
146            checker: None,
147        }
148    }
149
150    /// Create with settings-based checker
151    ///
152    /// Uses Default mode (standard behavior with permission prompts).
153    pub fn with_checker(checker: Arc<RwLock<PermissionChecker>>) -> Self {
154        Self {
155            mode: PermissionMode::Default,
156            strategy: Arc::new(DefaultModeStrategy),
157            checker: Some(checker),
158        }
159    }
160
161    /// Create with settings-based checker (non-async, for convenience)
162    ///
163    /// Uses Default mode (standard behavior with permission prompts).
164    pub fn with_checker_owned(checker: PermissionChecker) -> Self {
165        Self {
166            mode: PermissionMode::Default,
167            strategy: Arc::new(DefaultModeStrategy),
168            checker: Some(Arc::new(RwLock::new(checker))),
169        }
170    }
171
172    /// Create strategy for a given mode
173    fn create_strategy(mode: PermissionMode) -> Arc<dyn PermissionModeStrategy> {
174        match mode {
175            PermissionMode::Default => Arc::new(DefaultModeStrategy),
176            PermissionMode::AcceptEdits => Arc::new(AcceptEditsModeStrategy),
177            PermissionMode::Plan => Arc::new(PlanModeStrategy),
178            PermissionMode::DontAsk => Arc::new(DontAskModeStrategy),
179            PermissionMode::BypassPermissions => Arc::new(BypassPermissionsModeStrategy),
180        }
181    }
182
183    /// Get current permission mode
184    pub fn mode(&self) -> PermissionMode {
185        self.mode
186    }
187
188    /// Set permission mode
189    pub fn set_mode(&mut self, mode: PermissionMode) {
190        self.mode = mode;
191        self.strategy = Self::create_strategy(mode);
192    }
193
194    /// Set the permission checker
195    pub fn set_checker(&mut self, checker: Arc<RwLock<PermissionChecker>>) {
196        self.checker = Some(checker);
197    }
198
199    /// Get mutable reference to checker (for adding runtime rules)
200    pub async fn checker_mut(
201        &mut self,
202    ) -> Option<tokio::sync::RwLockWriteGuard<'_, PermissionChecker>> {
203        if let Some(ref checker) = self.checker {
204            Some(checker.write().await)
205        } else {
206            None
207        }
208    }
209
210    /// Check if a tool operation should be auto-approved
211    ///
212    /// Returns true if the operation should proceed without user prompt.
213    ///
214    /// Delegates to the current strategy.
215    pub fn should_auto_approve(&self, tool_name: &str, input: &serde_json::Value) -> bool {
216        self.strategy.should_auto_approve(tool_name, input)
217    }
218
219    /// Check if a tool is blocked in current mode
220    ///
221    /// Returns true if the tool is blocked.
222    ///
223    /// Note: This method doesn't take tool_input, so it's less precise than
224    /// the strategy method. For plan mode, it conservatively blocks all writes
225    /// since it can't check if the file is in the plans directory.
226    pub fn is_tool_blocked(&self, tool_name: &str) -> bool {
227        self.strategy
228            .is_tool_blocked(tool_name, &serde_json::Value::Null)
229            .is_some()
230    }
231
232    /// Check permission for a tool with full context
233    ///
234    /// Combines strategy-based checking with settings rules.
235    /// Returns the permission result.
236    pub async fn check_permission(
237        &self,
238        tool_name: &str,
239        tool_input: &serde_json::Value,
240    ) -> ToolPermissionResult {
241        // Check settings rules first (if available)
242        if let Some(ref checker) = self.checker {
243            let checker_read = checker.read().await;
244            let result = checker_read.check_permission(tool_name, tool_input);
245            match result.decision {
246                PermissionDecision::Deny => {
247                    return ToolPermissionResult::Blocked {
248                        reason: result
249                            .rule
250                            .map(|r| format!("Denied by rule: {}", r))
251                            .unwrap_or_else(|| "Denied by settings".to_string()),
252                    };
253                }
254                PermissionDecision::Allow => {
255                    return ToolPermissionResult::Allowed;
256                }
257                PermissionDecision::Ask => {
258                    // Fall through to strategy-based check
259                }
260            }
261        }
262
263        // Use strategy for mode-specific logic
264        let strategy_result = self.strategy.check_permission(tool_name, tool_input);
265
266        // Special handling for DontAsk mode: convert NeedsPermission to Blocked
267        if self.mode == PermissionMode::DontAsk {
268            if strategy_result == ToolPermissionResult::NeedsPermission {
269                return ToolPermissionResult::Blocked {
270                    reason: "Tool not pre-approved by settings rules in DontAsk mode".to_string(),
271                };
272            }
273        }
274
275        // User interaction tools should always be allowed
276        if matches!(
277            tool_name,
278            "AskUserQuestion" | "Task" | "TodoWrite" | "SlashCommand"
279        ) {
280            return ToolPermissionResult::Allowed;
281        }
282
283        strategy_result
284    }
285
286    /// Add a runtime allow rule (e.g., from user's "Always Allow" choice)
287    pub async fn add_allow_rule(&self, tool_name: &str) {
288        if let Some(ref checker) = self.checker {
289            let mut checker_write = checker.write().await;
290            checker_write.add_allow_rule(tool_name);
291        }
292    }
293
294    /// Add a fine-grained allow rule based on tool call details
295    /// This is used for "Always Allow" with specific parameters
296    pub fn add_allow_rule_for_tool_call(&self, tool_name: &str, tool_input: &serde_json::Value) {
297        if let Some(ref checker) = self.checker {
298            // Use try_write to avoid blocking - if lock is held, rule addition will be skipped
299            if let Ok(mut checker_write) = checker.try_write() {
300                checker_write.add_allow_rule_for_tool_call(tool_name, tool_input);
301            }
302        }
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use serde_json::json;
310
311    #[test]
312    fn test_permission_mode_parse() {
313        assert_eq!(
314            PermissionMode::parse("default"),
315            Some(PermissionMode::Default)
316        );
317        assert_eq!(
318            PermissionMode::parse("acceptEdits"),
319            Some(PermissionMode::AcceptEdits)
320        );
321        assert_eq!(PermissionMode::parse("plan"), Some(PermissionMode::Plan));
322        assert_eq!(
323            PermissionMode::parse("bypassPermissions"),
324            Some(PermissionMode::BypassPermissions)
325        );
326        assert_eq!(PermissionMode::parse("invalid"), None);
327    }
328
329    #[test]
330    fn test_permission_mode_str() {
331        assert_eq!(PermissionMode::Default.as_str(), "default");
332        assert_eq!(PermissionMode::AcceptEdits.as_str(), "acceptEdits");
333        assert_eq!(PermissionMode::Plan.as_str(), "plan");
334        assert_eq!(
335            PermissionMode::BypassPermissions.as_str(),
336            "bypassPermissions"
337        );
338    }
339
340    #[test]
341    fn test_permission_handler_default() {
342        let handler = PermissionHandler::new();
343        let input = json!({});
344
345        // Default mode auto-approves reads
346        assert!(handler.should_auto_approve("Read", &input));
347        assert!(handler.should_auto_approve("Glob", &input));
348        assert!(handler.should_auto_approve("Grep", &input));
349        assert!(handler.should_auto_approve("LS", &input));
350        assert!(handler.should_auto_approve("NotebookRead", &input));
351        // But not writes - these require permission
352        assert!(!handler.should_auto_approve("Edit", &input));
353        assert!(!handler.should_auto_approve("Bash", &input));
354    }
355
356    #[test]
357    fn test_permission_handler_accept_edits() {
358        let handler = PermissionHandler::with_mode(PermissionMode::AcceptEdits);
359        let input = json!({});
360
361        // AcceptEdits now auto-approves ALL tools (same as BypassPermissions)
362        // This is needed for root user compatibility
363        assert!(handler.should_auto_approve("Read", &input));
364        assert!(handler.should_auto_approve("Edit", &input));
365        assert!(handler.should_auto_approve("Write", &input));
366        assert!(handler.should_auto_approve("Bash", &input));
367    }
368
369    #[test]
370    fn test_permission_handler_bypass() {
371        let handler = PermissionHandler::with_mode(PermissionMode::BypassPermissions);
372        let input = json!({});
373
374        // Everything auto-approved
375        assert!(handler.should_auto_approve("Read", &input));
376        assert!(handler.should_auto_approve("Edit", &input));
377        assert!(handler.should_auto_approve("Bash", &input));
378    }
379
380    #[test]
381    fn test_permission_handler_plan_mode() {
382        let handler = PermissionHandler::with_mode(PermissionMode::Plan);
383        let input = json!({});
384
385        // Only reads auto-approved
386        assert!(handler.should_auto_approve("Read", &input));
387        assert!(handler.should_auto_approve("Glob", &input));
388        assert!(handler.should_auto_approve("Grep", &input));
389        assert!(handler.should_auto_approve("LS", &input));
390        assert!(handler.should_auto_approve("NotebookRead", &input));
391        assert!(!handler.should_auto_approve("Edit", &input));
392
393        // Writes are blocked
394        assert!(handler.is_tool_blocked("Edit"));
395        assert!(handler.is_tool_blocked("Bash"));
396        assert!(!handler.is_tool_blocked("Read"));
397        assert!(!handler.is_tool_blocked("LS"));
398    }
399
400    #[tokio::test]
401    async fn test_plan_mode_strategy_allows_plan_file_writes() {
402        let handler = PermissionHandler::with_mode(PermissionMode::Plan);
403        let home = dirs::home_dir().unwrap();
404        let plan_file = home.join(".claude").join("plans").join("test.md");
405
406        match handler
407            .check_permission(
408                "Write",
409                &json!({"file_path": plan_file.to_str().unwrap(), "content": "test"}),
410            )
411            .await
412        {
413            ToolPermissionResult::Allowed => {}
414            _ => panic!("Expected Allowed for plan file writes"),
415        }
416    }
417
418    #[tokio::test]
419    async fn test_plan_mode_strategy_blocks_non_plan_writes() {
420        let handler = PermissionHandler::with_mode(PermissionMode::Plan);
421
422        match handler
423            .check_permission("Write", &json!({"file_path": "/tmp/test.txt", "content": "test"}))
424            .await
425        {
426            ToolPermissionResult::Blocked { .. } => {}
427            _ => panic!("Expected Blocked for non-plan file writes"),
428        }
429    }
430
431    #[tokio::test]
432    async fn test_default_mode_strategy() {
433        let handler = PermissionHandler::new();
434
435        // Reads are auto-approved
436        match handler.check_permission("Read", &json!({})).await {
437            ToolPermissionResult::Allowed => {}
438            _ => panic!("Expected Allowed for Read"),
439        }
440
441        // Writes need permission
442        match handler.check_permission("Write", &json!({})).await {
443            ToolPermissionResult::NeedsPermission => {}
444            _ => panic!("Expected NeedsPermission for Write"),
445        }
446    }
447
448    #[tokio::test]
449    async fn test_bypass_permissions_strategy() {
450        let handler = PermissionHandler::with_mode(PermissionMode::BypassPermissions);
451
452        // Everything is allowed
453        match handler
454            .check_permission("Bash", &json!({"command": "rm -rf /"}))
455            .await
456        {
457            ToolPermissionResult::Allowed => {}
458            _ => panic!("Expected Allowed for Bash in BypassPermissions mode"),
459        }
460    }
461
462    #[tokio::test]
463    async fn test_accept_edits_strategy() {
464        let handler = PermissionHandler::with_mode(PermissionMode::AcceptEdits);
465
466        // Everything is allowed
467        match handler.check_permission("Write", &json!({})).await {
468            ToolPermissionResult::Allowed => {}
469            _ => panic!("Expected Allowed for Write in AcceptEdits mode"),
470        }
471    }
472}