claude_code_acp/session/
permission.rs

1//! Permission handling for tool execution
2//!
3//! Phase 1: Simplified permission handling with auto-approve mode.
4//! Phase 2: Full permission prompts with settings rules and user interaction.
5
6use serde::{Deserialize, Serialize};
7use std::sync::Arc;
8use tokio::sync::RwLock;
9
10use crate::settings::{PermissionChecker, PermissionDecision};
11use claude_code_agent_sdk::PermissionMode as SdkPermissionMode;
12
13/// Permission mode for tool execution
14///
15/// Controls how tool calls are approved during a session.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub enum PermissionMode {
19    /// Default mode - prompt for dangerous operations
20    Default,
21    /// Auto-approve file edits
22    AcceptEdits,
23    /// Planning mode - read-only operations
24    Plan,
25    /// Don't ask mode - deny if not pre-approved
26    DontAsk,
27    /// Bypass all permission checks (default mode for development)
28    #[default]
29    BypassPermissions,
30}
31
32impl PermissionMode {
33    /// Parse from string (ACP setMode request)
34    pub fn parse(s: &str) -> Option<Self> {
35        match s {
36            "default" => Some(Self::Default),
37            "acceptEdits" => Some(Self::AcceptEdits),
38            "plan" => Some(Self::Plan),
39            "dontAsk" => Some(Self::DontAsk),
40            "bypassPermissions" => Some(Self::BypassPermissions),
41            _ => None,
42        }
43    }
44
45    /// Convert to string for SDK
46    pub fn as_str(&self) -> &'static str {
47        match self {
48            Self::Default => "default",
49            Self::AcceptEdits => "acceptEdits",
50            Self::Plan => "plan",
51            Self::DontAsk => "dontAsk",
52            Self::BypassPermissions => "bypassPermissions",
53        }
54    }
55
56    /// Convert to SDK PermissionMode
57    ///
58    /// Note: SDK doesn't support DontAsk mode yet, so we map it to Default
59    pub fn to_sdk_mode(&self) -> SdkPermissionMode {
60        match self {
61            PermissionMode::Default => SdkPermissionMode::Default,
62            PermissionMode::AcceptEdits => SdkPermissionMode::AcceptEdits,
63            PermissionMode::Plan => SdkPermissionMode::Plan,
64            PermissionMode::DontAsk => {
65                // SDK doesn't support DontAsk yet, treat as Default
66                SdkPermissionMode::Default
67            }
68            PermissionMode::BypassPermissions => SdkPermissionMode::BypassPermissions,
69        }
70    }
71
72    /// Check if this mode allows write operations
73    pub fn allows_writes(&self) -> bool {
74        matches!(
75            self,
76            Self::Default | Self::AcceptEdits | Self::BypassPermissions
77        )
78    }
79
80    /// Check if this mode auto-approves edits
81    pub fn auto_approve_edits(&self) -> bool {
82        matches!(self, Self::AcceptEdits | Self::BypassPermissions)
83    }
84}
85
86/// Permission check result from the handler
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum ToolPermissionResult {
89    /// Tool execution is allowed (auto-approved or by rule)
90    Allowed,
91    /// Tool execution is blocked (by rule or mode)
92    Blocked { reason: String },
93    /// User should be asked for permission
94    NeedsPermission,
95}
96
97/// Permission handler for tool execution
98///
99/// Combines mode-based checking with settings rules.
100///
101/// The permission checker is shared with the pre_tool_use_hook to ensure
102/// that runtime rule changes (e.g., "Always Allow") are reflected in both places.
103#[derive(Debug)]
104pub struct PermissionHandler {
105    mode: PermissionMode,
106    /// Shared permission checker from settings (shared with hook)
107    checker: Option<Arc<RwLock<PermissionChecker>>>,
108}
109
110impl Default for PermissionHandler {
111    fn default() -> Self {
112        Self {
113            mode: PermissionMode::BypassPermissions,
114            checker: None,
115        }
116    }
117}
118
119impl PermissionHandler {
120    /// Create a new permission handler
121    pub fn new() -> Self {
122        Self::default()
123    }
124
125    /// Create with a specific mode
126    pub fn with_mode(mode: PermissionMode) -> Self {
127        Self {
128            mode,
129            checker: None,
130        }
131    }
132
133    /// Create with settings-based checker
134    pub fn with_checker(checker: Arc<RwLock<PermissionChecker>>) -> Self {
135        Self {
136            mode: PermissionMode::BypassPermissions,
137            checker: Some(checker),
138        }
139    }
140
141    /// Create with settings-based checker (non-async, for convenience)
142    pub fn with_checker_owned(checker: PermissionChecker) -> Self {
143        Self {
144            mode: PermissionMode::BypassPermissions,
145            checker: Some(Arc::new(RwLock::new(checker))),
146        }
147    }
148
149    /// Get current permission mode
150    pub fn mode(&self) -> PermissionMode {
151        self.mode
152    }
153
154    /// Set permission mode
155    pub fn set_mode(&mut self, mode: PermissionMode) {
156        self.mode = mode;
157    }
158
159    /// Set the permission checker
160    pub fn set_checker(&mut self, checker: Arc<RwLock<PermissionChecker>>) {
161        self.checker = Some(checker);
162    }
163
164    /// Get mutable reference to checker (for adding runtime rules)
165    pub async fn checker_mut(
166        &mut self,
167    ) -> Option<tokio::sync::RwLockWriteGuard<'_, PermissionChecker>> {
168        if let Some(ref checker) = self.checker {
169            Some(checker.write().await)
170        } else {
171            None
172        }
173    }
174
175    /// Check if a tool operation should be auto-approved
176    ///
177    /// Returns true if the operation should proceed without user prompt.
178    pub fn should_auto_approve(&self, tool_name: &str, _input: &serde_json::Value) -> bool {
179        match self.mode {
180            PermissionMode::BypassPermissions => true,
181            PermissionMode::AcceptEdits => {
182                // Auto-approve read and edit operations
183                matches!(
184                    tool_name,
185                    "Read" | "Edit" | "Write" | "Glob" | "Grep" | "NotebookRead" | "NotebookEdit"
186                )
187            }
188            PermissionMode::Plan => {
189                // Only allow read operations in plan mode
190                matches!(tool_name, "Read" | "Glob" | "Grep" | "NotebookRead")
191            }
192            PermissionMode::DontAsk => {
193                // DontAsk mode: only pre-approved tools via settings rules
194                // No auto-approval
195                false
196            }
197            PermissionMode::Default => {
198                // Only auto-approve read operations
199                matches!(tool_name, "Read" | "Glob" | "Grep" | "NotebookRead")
200            }
201        }
202    }
203
204    /// Check if a tool is blocked in current mode
205    pub fn is_tool_blocked(&self, tool_name: &str) -> bool {
206        if self.mode == PermissionMode::Plan {
207            // Block write operations in plan mode
208            matches!(tool_name, "Edit" | "Write" | "Bash" | "NotebookEdit")
209        } else {
210            false
211        }
212    }
213
214    /// Check permission for a tool with full context
215    ///
216    /// Combines mode-based checking with settings rules.
217    /// Returns the permission result.
218    pub async fn check_permission(
219        &self,
220        tool_name: &str,
221        tool_input: &serde_json::Value,
222    ) -> ToolPermissionResult {
223        // BypassPermissions mode allows everything
224        if self.mode == PermissionMode::BypassPermissions {
225            return ToolPermissionResult::Allowed;
226        }
227
228        // Check if tool is blocked in current mode
229        if self.is_tool_blocked(tool_name) {
230            return ToolPermissionResult::Blocked {
231                reason: format!(
232                    "Tool {} is blocked in {} mode",
233                    tool_name,
234                    self.mode.as_str()
235                ),
236            };
237        }
238
239        // Check settings rules if available
240        if let Some(ref checker) = self.checker {
241            let checker_read = checker.read().await;
242            let result = checker_read.check_permission(tool_name, tool_input);
243            match result.decision {
244                PermissionDecision::Deny => {
245                    return ToolPermissionResult::Blocked {
246                        reason: result
247                            .rule
248                            .map(|r| format!("Denied by rule: {}", r))
249                            .unwrap_or_else(|| "Denied by settings".to_string()),
250                    };
251                }
252                PermissionDecision::Allow => {
253                    return ToolPermissionResult::Allowed;
254                }
255                PermissionDecision::Ask => {
256                    // Fall through to mode-based check
257                }
258            }
259        }
260
261        // User interaction tools should always be allowed
262        // These tools themselves facilitate user interaction and shouldn't be blocked
263        if matches!(
264            tool_name,
265            "AskUserQuestion" | "Task" | "TodoWrite" | "SlashCommand"
266        ) {
267            return ToolPermissionResult::Allowed;
268        }
269
270        // Mode-based auto-approve
271        if self.should_auto_approve(tool_name, tool_input) {
272            return ToolPermissionResult::Allowed;
273        }
274
275        // Default: need to ask user
276        ToolPermissionResult::NeedsPermission
277    }
278
279    /// Add a runtime allow rule (e.g., from user's "Always Allow" choice)
280    pub async fn add_allow_rule(&self, tool_name: &str) {
281        if let Some(ref checker) = self.checker {
282            let mut checker_write = checker.write().await;
283            checker_write.add_allow_rule(tool_name);
284        }
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use serde_json::json;
292
293    #[test]
294    fn test_permission_mode_parse() {
295        assert_eq!(
296            PermissionMode::parse("default"),
297            Some(PermissionMode::Default)
298        );
299        assert_eq!(
300            PermissionMode::parse("acceptEdits"),
301            Some(PermissionMode::AcceptEdits)
302        );
303        assert_eq!(PermissionMode::parse("plan"), Some(PermissionMode::Plan));
304        assert_eq!(
305            PermissionMode::parse("bypassPermissions"),
306            Some(PermissionMode::BypassPermissions)
307        );
308        assert_eq!(PermissionMode::parse("invalid"), None);
309    }
310
311    #[test]
312    fn test_permission_mode_str() {
313        assert_eq!(PermissionMode::Default.as_str(), "default");
314        assert_eq!(PermissionMode::AcceptEdits.as_str(), "acceptEdits");
315    }
316
317    #[test]
318    fn test_permission_handler_default() {
319        let handler = PermissionHandler::new();
320        let input = json!({});
321
322        // Default mode is now BypassPermissions - everything auto-approved
323        assert!(handler.should_auto_approve("Read", &input));
324        assert!(handler.should_auto_approve("Glob", &input));
325        assert!(handler.should_auto_approve("Edit", &input));
326        assert!(handler.should_auto_approve("Bash", &input));
327    }
328
329    #[test]
330    fn test_permission_handler_explicit_default_mode() {
331        // Test the old Default mode explicitly
332        let handler = PermissionHandler::with_mode(PermissionMode::Default);
333        let input = json!({});
334
335        // Default mode auto-approves reads
336        assert!(handler.should_auto_approve("Read", &input));
337        assert!(handler.should_auto_approve("Glob", &input));
338        // But not writes
339        assert!(!handler.should_auto_approve("Edit", &input));
340        assert!(!handler.should_auto_approve("Bash", &input));
341    }
342
343    #[test]
344    fn test_permission_handler_accept_edits() {
345        let handler = PermissionHandler::with_mode(PermissionMode::AcceptEdits);
346        let input = json!({});
347
348        assert!(handler.should_auto_approve("Read", &input));
349        assert!(handler.should_auto_approve("Edit", &input));
350        assert!(handler.should_auto_approve("Write", &input));
351        // Bash still not auto-approved
352        assert!(!handler.should_auto_approve("Bash", &input));
353    }
354
355    #[test]
356    fn test_permission_handler_bypass() {
357        let handler = PermissionHandler::with_mode(PermissionMode::BypassPermissions);
358        let input = json!({});
359
360        // Everything auto-approved
361        assert!(handler.should_auto_approve("Read", &input));
362        assert!(handler.should_auto_approve("Edit", &input));
363        assert!(handler.should_auto_approve("Bash", &input));
364    }
365
366    #[test]
367    fn test_permission_handler_plan_mode() {
368        let handler = PermissionHandler::with_mode(PermissionMode::Plan);
369        let input = json!({});
370
371        // Only reads auto-approved
372        assert!(handler.should_auto_approve("Read", &input));
373        assert!(!handler.should_auto_approve("Edit", &input));
374
375        // Writes are blocked
376        assert!(handler.is_tool_blocked("Edit"));
377        assert!(handler.is_tool_blocked("Bash"));
378        assert!(!handler.is_tool_blocked("Read"));
379    }
380}