Skip to main content

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            // Use Default mode (standard behavior with permission prompts)
114            mode: PermissionMode::Default,
115            checker: None,
116        }
117    }
118}
119
120impl PermissionHandler {
121    /// Create a new permission handler
122    ///
123    /// Uses Default mode (standard behavior with permission prompts).
124    pub fn new() -> Self {
125        Self::default()
126    }
127
128    /// Create with a specific mode
129    pub fn with_mode(mode: PermissionMode) -> Self {
130        Self {
131            mode,
132            checker: None,
133        }
134    }
135
136    /// Create with settings-based checker
137    ///
138    /// Uses Default mode (standard behavior with permission prompts).
139    pub fn with_checker(checker: Arc<RwLock<PermissionChecker>>) -> Self {
140        Self {
141            mode: PermissionMode::Default,
142            checker: Some(checker),
143        }
144    }
145
146    /// Create with settings-based checker (non-async, for convenience)
147    ///
148    /// Uses Default mode (standard behavior with permission prompts).
149    pub fn with_checker_owned(checker: PermissionChecker) -> Self {
150        Self {
151            mode: PermissionMode::Default,
152            checker: Some(Arc::new(RwLock::new(checker))),
153        }
154    }
155
156    /// Get current permission mode
157    pub fn mode(&self) -> PermissionMode {
158        self.mode
159    }
160
161    /// Set permission mode
162    pub fn set_mode(&mut self, mode: PermissionMode) {
163        self.mode = mode;
164    }
165
166    /// Set the permission checker
167    pub fn set_checker(&mut self, checker: Arc<RwLock<PermissionChecker>>) {
168        self.checker = Some(checker);
169    }
170
171    /// Get mutable reference to checker (for adding runtime rules)
172    pub async fn checker_mut(
173        &mut self,
174    ) -> Option<tokio::sync::RwLockWriteGuard<'_, PermissionChecker>> {
175        if let Some(ref checker) = self.checker {
176            Some(checker.write().await)
177        } else {
178            None
179        }
180    }
181
182    /// Check if a tool operation should be auto-approved
183    ///
184    /// Returns true if the operation should proceed without user prompt.
185    ///
186    /// Note: AcceptEdits mode auto-approves ALL tools (same as BypassPermissions)
187    /// to maintain compatibility with root user environments while providing
188    /// full automation capabilities.
189    pub fn should_auto_approve(&self, tool_name: &str, _input: &serde_json::Value) -> bool {
190        match self.mode {
191            PermissionMode::BypassPermissions => true,
192            PermissionMode::AcceptEdits => {
193                // Auto-approve ALL tools (same as BypassPermissions)
194                // This is needed because BypassPermissions cannot be used with root
195                true
196            }
197            PermissionMode::Plan => {
198                // Only allow read operations in plan mode
199                matches!(tool_name, "Read" | "Glob" | "Grep" | "NotebookRead")
200            }
201            PermissionMode::DontAsk => {
202                // DontAsk mode: only pre-approved tools via settings rules
203                // No auto-approval
204                false
205            }
206            PermissionMode::Default => {
207                // Only auto-approve read operations
208                matches!(tool_name, "Read" | "Glob" | "Grep" | "NotebookRead")
209            }
210        }
211    }
212
213    /// Check if a tool is blocked in current mode
214    pub fn is_tool_blocked(&self, tool_name: &str) -> bool {
215        if self.mode == PermissionMode::Plan {
216            // Block write operations in plan mode
217            matches!(tool_name, "Edit" | "Write" | "Bash" | "NotebookEdit")
218        } else {
219            false
220        }
221    }
222
223    /// Check permission for a tool with full context
224    ///
225    /// Combines mode-based checking with settings rules.
226    /// Returns the permission result.
227    ///
228    /// Note: Both BypassPermissions and AcceptEdits modes bypass all permission
229    /// checks (BypassPermissions for compatibility with non-root environments,
230    /// AcceptEdits for root user compatibility).
231    pub async fn check_permission(
232        &self,
233        tool_name: &str,
234        tool_input: &serde_json::Value,
235    ) -> ToolPermissionResult {
236        // BypassPermissions and AcceptEdits modes allow everything
237        // (AcceptEdits behaves like BypassPermissions for root compatibility)
238        if matches!(
239            self.mode,
240            PermissionMode::BypassPermissions | PermissionMode::AcceptEdits
241        ) {
242            return ToolPermissionResult::Allowed;
243        }
244
245        // Check if tool is blocked in current mode
246        if self.is_tool_blocked(tool_name) {
247            return ToolPermissionResult::Blocked {
248                reason: format!(
249                    "Tool {} is blocked in {} mode",
250                    tool_name,
251                    self.mode.as_str()
252                ),
253            };
254        }
255
256        // Check settings rules if available
257        if let Some(ref checker) = self.checker {
258            let checker_read = checker.read().await;
259            let result = checker_read.check_permission(tool_name, tool_input);
260            match result.decision {
261                PermissionDecision::Deny => {
262                    return ToolPermissionResult::Blocked {
263                        reason: result
264                            .rule
265                            .map(|r| format!("Denied by rule: {}", r))
266                            .unwrap_or_else(|| "Denied by settings".to_string()),
267                    };
268                }
269                PermissionDecision::Allow => {
270                    return ToolPermissionResult::Allowed;
271                }
272                PermissionDecision::Ask => {
273                    // Fall through to mode-based check
274                }
275            }
276        }
277
278        // User interaction tools should always be allowed
279        // These tools themselves facilitate user interaction and shouldn't be blocked
280        if matches!(
281            tool_name,
282            "AskUserQuestion" | "Task" | "TodoWrite" | "SlashCommand"
283        ) {
284            return ToolPermissionResult::Allowed;
285        }
286
287        // Mode-based auto-approve
288        if self.should_auto_approve(tool_name, tool_input) {
289            return ToolPermissionResult::Allowed;
290        }
291
292        // Default: need to ask user
293        ToolPermissionResult::NeedsPermission
294    }
295
296    /// Add a runtime allow rule (e.g., from user's "Always Allow" choice)
297    pub async fn add_allow_rule(&self, tool_name: &str) {
298        if let Some(ref checker) = self.checker {
299            let mut checker_write = checker.write().await;
300            checker_write.add_allow_rule(tool_name);
301        }
302    }
303
304    /// Add a fine-grained allow rule based on tool call details
305    /// This is used for "Always Allow" with specific parameters
306    pub fn add_allow_rule_for_tool_call(&self, tool_name: &str, tool_input: &serde_json::Value) {
307        if let Some(ref checker) = self.checker {
308            // Use try_write to avoid blocking - if lock is held, rule addition will be skipped
309            if let Ok(mut checker_write) = checker.try_write() {
310                checker_write.add_allow_rule_for_tool_call(tool_name, tool_input);
311            }
312        }
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319    use serde_json::json;
320
321    #[test]
322    fn test_permission_mode_parse() {
323        assert_eq!(
324            PermissionMode::parse("default"),
325            Some(PermissionMode::Default)
326        );
327        assert_eq!(
328            PermissionMode::parse("acceptEdits"),
329            Some(PermissionMode::AcceptEdits)
330        );
331        assert_eq!(PermissionMode::parse("plan"), Some(PermissionMode::Plan));
332        assert_eq!(
333            PermissionMode::parse("bypassPermissions"),
334            Some(PermissionMode::BypassPermissions)
335        );
336        assert_eq!(PermissionMode::parse("invalid"), None);
337    }
338
339    #[test]
340    fn test_permission_mode_str() {
341        assert_eq!(PermissionMode::Default.as_str(), "default");
342        assert_eq!(PermissionMode::AcceptEdits.as_str(), "acceptEdits");
343    }
344
345    #[test]
346    fn test_permission_handler_default() {
347        let handler = PermissionHandler::new();
348        let input = json!({});
349
350        // Default mode (PermissionMode::Default) auto-approves reads
351        assert!(handler.should_auto_approve("Read", &input));
352        assert!(handler.should_auto_approve("Glob", &input));
353        // But not writes - these require permission
354        assert!(!handler.should_auto_approve("Edit", &input));
355        assert!(!handler.should_auto_approve("Bash", &input));
356    }
357
358    #[test]
359    fn test_permission_handler_accept_edits() {
360        let handler = PermissionHandler::with_mode(PermissionMode::AcceptEdits);
361        let input = json!({});
362
363        // AcceptEdits now auto-approves ALL tools (same as BypassPermissions)
364        // This is needed for root user compatibility
365        assert!(handler.should_auto_approve("Read", &input));
366        assert!(handler.should_auto_approve("Edit", &input));
367        assert!(handler.should_auto_approve("Write", &input));
368        assert!(handler.should_auto_approve("Bash", &input));
369    }
370
371    #[test]
372    fn test_permission_handler_bypass() {
373        let handler = PermissionHandler::with_mode(PermissionMode::BypassPermissions);
374        let input = json!({});
375
376        // Everything auto-approved
377        assert!(handler.should_auto_approve("Read", &input));
378        assert!(handler.should_auto_approve("Edit", &input));
379        assert!(handler.should_auto_approve("Bash", &input));
380    }
381
382    #[test]
383    fn test_permission_handler_plan_mode() {
384        let handler = PermissionHandler::with_mode(PermissionMode::Plan);
385        let input = json!({});
386
387        // Only reads auto-approved
388        assert!(handler.should_auto_approve("Read", &input));
389        assert!(!handler.should_auto_approve("Edit", &input));
390
391        // Writes are blocked
392        assert!(handler.is_tool_blocked("Edit"));
393        assert!(handler.is_tool_blocked("Bash"));
394        assert!(!handler.is_tool_blocked("Read"));
395    }
396}