gestura_core_foundation/
execution_mode.rs

1//! Execution mode support for agent pipeline
2//!
3//! This module provides Auto vs Agent mode switching, mode-specific tool permissions,
4//! and mode persistence per session. Based on Block Goose architecture patterns.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashSet;
8
9/// Execution mode for the agent
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
11pub enum ExecutionMode {
12    /// Agent mode - interactive conversation with confirmation for dangerous operations
13    #[default]
14    #[serde(alias = "Agent")]
15    Agent,
16    /// Auto mode - autonomous tool execution without confirmation
17    Auto,
18    /// Restricted mode - limited tool access for safety
19    Restricted,
20}
21
22impl ExecutionMode {
23    /// Get a human-readable description of the mode
24    pub fn description(&self) -> &'static str {
25        match self {
26            Self::Agent => "Interactive agent with tool confirmation",
27            Self::Auto => "Autonomous execution without confirmation",
28            Self::Restricted => "Limited tool access for safety",
29        }
30    }
31
32    /// Get the short name for UI display
33    pub fn short_name(&self) -> &'static str {
34        match self {
35            Self::Agent => "Agent",
36            Self::Auto => "Auto",
37            Self::Restricted => "Restricted",
38        }
39    }
40
41    /// Check if this mode requires confirmation for tool execution
42    pub fn requires_confirmation(&self) -> bool {
43        match self {
44            Self::Agent => true,
45            Self::Auto => false,
46            Self::Restricted => true,
47        }
48    }
49
50    /// Check if this mode allows autonomous tool execution
51    pub fn allows_autonomous_execution(&self) -> bool {
52        match self {
53            Self::Agent => false,
54            Self::Auto => true,
55            Self::Restricted => false,
56        }
57    }
58}
59
60impl std::fmt::Display for ExecutionMode {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        write!(f, "{}", self.short_name())
63    }
64}
65
66impl std::str::FromStr for ExecutionMode {
67    type Err = String;
68
69    fn from_str(s: &str) -> Result<Self, Self::Err> {
70        match s.to_lowercase().as_str() {
71            "agent" | "interactive" => Ok(Self::Agent),
72            "auto" | "autonomous" => Ok(Self::Auto),
73            "restricted" | "safe" | "limited" => Ok(Self::Restricted),
74            _ => Err(format!("Unknown execution mode: {}", s)),
75        }
76    }
77}
78
79/// Tool permission level for execution modes
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
81pub enum ToolPermission {
82    /// Tool is always allowed
83    Allowed,
84    /// Tool requires confirmation before execution
85    RequiresConfirmation,
86    /// Tool is blocked in this mode
87    Blocked,
88}
89
90/// Tool category for permission grouping
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
92pub enum ToolCategory {
93    /// Read-only operations (file read, search, etc.)
94    ReadOnly,
95    /// Write operations (file write, create, etc.)
96    Write,
97    /// Shell/command execution
98    Shell,
99    /// Network operations (web fetch, API calls)
100    Network,
101    /// System operations (process management, etc.)
102    System,
103    /// Git operations
104    Git,
105}
106
107impl ToolCategory {
108    /// Get the default permission for this category in a given mode
109    pub fn default_permission(&self, mode: ExecutionMode) -> ToolPermission {
110        match (self, mode) {
111            // Read-only is always allowed
112            (Self::ReadOnly, _) => ToolPermission::Allowed,
113
114            // Write operations
115            (Self::Write, ExecutionMode::Auto) => ToolPermission::Allowed,
116            (Self::Write, ExecutionMode::Agent) => ToolPermission::RequiresConfirmation,
117            (Self::Write, ExecutionMode::Restricted) => ToolPermission::Blocked,
118
119            // Shell operations
120            (Self::Shell, ExecutionMode::Auto) => ToolPermission::Allowed,
121            (Self::Shell, ExecutionMode::Agent) => ToolPermission::RequiresConfirmation,
122            (Self::Shell, ExecutionMode::Restricted) => ToolPermission::Blocked,
123
124            // Network operations
125            (Self::Network, ExecutionMode::Auto) => ToolPermission::Allowed,
126            (Self::Network, ExecutionMode::Agent) => ToolPermission::Allowed,
127            (Self::Network, ExecutionMode::Restricted) => ToolPermission::RequiresConfirmation,
128
129            // System operations
130            (Self::System, ExecutionMode::Auto) => ToolPermission::RequiresConfirmation,
131            (Self::System, ExecutionMode::Agent) => ToolPermission::RequiresConfirmation,
132            (Self::System, ExecutionMode::Restricted) => ToolPermission::Blocked,
133
134            // Git operations
135            (Self::Git, ExecutionMode::Auto) => ToolPermission::Allowed,
136            (Self::Git, ExecutionMode::Agent) => ToolPermission::RequiresConfirmation,
137            (Self::Git, ExecutionMode::Restricted) => ToolPermission::Blocked,
138        }
139    }
140}
141
142/// Configuration for execution mode behavior
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct ModeConfig {
145    /// Current execution mode
146    pub mode: ExecutionMode,
147    /// Custom tool overrides (tool_name -> permission)
148    pub tool_overrides: std::collections::HashMap<String, ToolPermission>,
149    /// Whether to persist mode across sessions
150    pub persist_mode: bool,
151    /// Auto-switch to Agent mode after errors
152    pub auto_fallback_on_error: bool,
153}
154
155impl Default for ModeConfig {
156    fn default() -> Self {
157        Self {
158            mode: ExecutionMode::Agent,
159            tool_overrides: std::collections::HashMap::new(),
160            persist_mode: true,
161            auto_fallback_on_error: true,
162        }
163    }
164}
165
166impl ModeConfig {
167    /// Create a new config with the specified mode
168    pub fn with_mode(mode: ExecutionMode) -> Self {
169        Self {
170            mode,
171            ..Default::default()
172        }
173    }
174
175    /// Get the effective permission for a tool
176    pub fn get_tool_permission(&self, tool_name: &str, category: ToolCategory) -> ToolPermission {
177        // Check for explicit override first
178        if let Some(permission) = self.tool_overrides.get(tool_name) {
179            return *permission;
180        }
181        // Fall back to category default
182        category.default_permission(self.mode)
183    }
184
185    /// Set a custom permission for a specific tool
186    pub fn set_tool_override(&mut self, tool_name: impl Into<String>, permission: ToolPermission) {
187        self.tool_overrides.insert(tool_name.into(), permission);
188    }
189
190    /// Remove a custom permission override
191    pub fn remove_tool_override(&mut self, tool_name: &str) {
192        self.tool_overrides.remove(tool_name);
193    }
194
195    /// Check if a tool is allowed (Allowed or RequiresConfirmation)
196    pub fn is_tool_allowed(&self, tool_name: &str, category: ToolCategory) -> bool {
197        matches!(
198            self.get_tool_permission(tool_name, category),
199            ToolPermission::Allowed | ToolPermission::RequiresConfirmation
200        )
201    }
202
203    /// Check if a tool requires confirmation
204    pub fn tool_requires_confirmation(&self, tool_name: &str, category: ToolCategory) -> bool {
205        matches!(
206            self.get_tool_permission(tool_name, category),
207            ToolPermission::RequiresConfirmation
208        )
209    }
210}
211
212/// Manager for execution mode state
213#[derive(Debug, Clone)]
214pub struct ModeManager {
215    config: ModeConfig,
216    /// Blocked tools for the current session
217    session_blocked_tools: HashSet<String>,
218    /// Tools that have been confirmed this session (skip re-confirmation)
219    confirmed_tools: HashSet<String>,
220}
221
222impl ModeManager {
223    /// Create a new mode manager with default config
224    pub fn new() -> Self {
225        Self::with_config(ModeConfig::default())
226    }
227
228    /// Create a new mode manager with custom config
229    pub fn with_config(config: ModeConfig) -> Self {
230        Self {
231            config,
232            session_blocked_tools: HashSet::new(),
233            confirmed_tools: HashSet::new(),
234        }
235    }
236
237    /// Get the current execution mode
238    pub fn mode(&self) -> ExecutionMode {
239        self.config.mode
240    }
241
242    /// Get the current config
243    pub fn config(&self) -> &ModeConfig {
244        &self.config
245    }
246
247    /// Set the execution mode
248    pub fn set_mode(&mut self, mode: ExecutionMode) {
249        self.config.mode = mode;
250        // Clear confirmed tools when mode changes
251        self.confirmed_tools.clear();
252    }
253
254    /// Check if a tool can be executed
255    pub fn can_execute_tool(&self, tool_name: &str, category: ToolCategory) -> ToolExecutionCheck {
256        // Check session blocks first
257        if self.session_blocked_tools.contains(tool_name) {
258            return ToolExecutionCheck::Blocked {
259                reason: "Tool was blocked for this session".to_string(),
260            };
261        }
262
263        let permission = self.config.get_tool_permission(tool_name, category);
264        match permission {
265            ToolPermission::Allowed => ToolExecutionCheck::Allowed,
266            ToolPermission::RequiresConfirmation => {
267                if self.confirmed_tools.contains(tool_name) {
268                    ToolExecutionCheck::Allowed
269                } else {
270                    ToolExecutionCheck::RequiresConfirmation
271                }
272            }
273            ToolPermission::Blocked => ToolExecutionCheck::Blocked {
274                reason: format!(
275                    "Tool '{}' is blocked in {} mode",
276                    tool_name,
277                    self.config.mode.short_name()
278                ),
279            },
280        }
281    }
282
283    /// Mark a tool as confirmed for this session
284    pub fn confirm_tool(&mut self, tool_name: impl Into<String>) {
285        self.confirmed_tools.insert(tool_name.into());
286    }
287
288    /// Block a tool for this session
289    pub fn block_tool_for_session(&mut self, tool_name: impl Into<String>) {
290        self.session_blocked_tools.insert(tool_name.into());
291    }
292
293    /// Clear session state (confirmed and blocked tools)
294    pub fn clear_session_state(&mut self) {
295        self.confirmed_tools.clear();
296        self.session_blocked_tools.clear();
297    }
298
299    /// Get list of tools that require confirmation
300    pub fn pending_confirmations(&self) -> Vec<&String> {
301        self.session_blocked_tools.iter().collect()
302    }
303}
304
305impl Default for ModeManager {
306    fn default() -> Self {
307        Self::new()
308    }
309}
310
311/// Result of checking if a tool can be executed
312#[derive(Debug, Clone, PartialEq, Eq)]
313pub enum ToolExecutionCheck {
314    /// Tool can be executed immediately
315    Allowed,
316    /// Tool requires user confirmation before execution
317    RequiresConfirmation,
318    /// Tool is blocked and cannot be executed
319    Blocked { reason: String },
320}
321
322impl ToolExecutionCheck {
323    /// Check if execution is allowed (either immediately or after confirmation)
324    pub fn is_allowed(&self) -> bool {
325        matches!(self, Self::Allowed)
326    }
327
328    /// Check if confirmation is required
329    pub fn requires_confirmation(&self) -> bool {
330        matches!(self, Self::RequiresConfirmation)
331    }
332
333    /// Check if execution is blocked
334    pub fn is_blocked(&self) -> bool {
335        matches!(self, Self::Blocked { .. })
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn test_execution_mode_defaults() {
345        let mode = ExecutionMode::default();
346        assert_eq!(mode, ExecutionMode::Agent);
347        assert!(mode.requires_confirmation());
348        assert!(!mode.allows_autonomous_execution());
349    }
350
351    #[test]
352    fn test_execution_mode_from_str() {
353        assert_eq!(
354            "agent".parse::<ExecutionMode>().unwrap(),
355            ExecutionMode::Agent
356        );
357        assert_eq!(
358            "auto".parse::<ExecutionMode>().unwrap(),
359            ExecutionMode::Auto
360        );
361        assert_eq!(
362            "restricted".parse::<ExecutionMode>().unwrap(),
363            ExecutionMode::Restricted
364        );
365        assert!("invalid".parse::<ExecutionMode>().is_err());
366    }
367
368    #[test]
369    fn test_tool_category_permissions() {
370        // Read-only is always allowed
371        assert_eq!(
372            ToolCategory::ReadOnly.default_permission(ExecutionMode::Agent),
373            ToolPermission::Allowed
374        );
375        assert_eq!(
376            ToolCategory::ReadOnly.default_permission(ExecutionMode::Restricted),
377            ToolPermission::Allowed
378        );
379
380        // Shell requires confirmation in Agent mode
381        assert_eq!(
382            ToolCategory::Shell.default_permission(ExecutionMode::Agent),
383            ToolPermission::RequiresConfirmation
384        );
385
386        // Shell is blocked in Restricted mode
387        assert_eq!(
388            ToolCategory::Shell.default_permission(ExecutionMode::Restricted),
389            ToolPermission::Blocked
390        );
391    }
392
393    #[test]
394    fn test_mode_config_overrides() {
395        let mut config = ModeConfig::with_mode(ExecutionMode::Agent);
396
397        // Default: shell requires confirmation
398        assert_eq!(
399            config.get_tool_permission("run_shell", ToolCategory::Shell),
400            ToolPermission::RequiresConfirmation
401        );
402
403        // Override: allow specific shell command
404        config.set_tool_override("run_shell", ToolPermission::Allowed);
405        assert_eq!(
406            config.get_tool_permission("run_shell", ToolCategory::Shell),
407            ToolPermission::Allowed
408        );
409    }
410
411    #[test]
412    fn test_mode_manager_confirmation() {
413        let mut manager = ModeManager::new();
414
415        // Shell requires confirmation in Agent mode
416        let check = manager.can_execute_tool("run_shell", ToolCategory::Shell);
417        assert!(check.requires_confirmation());
418
419        // After confirmation, it's allowed
420        manager.confirm_tool("run_shell");
421        let check = manager.can_execute_tool("run_shell", ToolCategory::Shell);
422        assert!(check.is_allowed());
423
424        // Changing mode clears confirmations
425        manager.set_mode(ExecutionMode::Auto);
426        let check = manager.can_execute_tool("run_shell", ToolCategory::Shell);
427        assert!(check.is_allowed()); // Auto mode allows without confirmation
428    }
429
430    #[test]
431    fn test_mode_manager_session_block() {
432        let mut manager = ModeManager::with_config(ModeConfig::with_mode(ExecutionMode::Auto));
433
434        // Initially allowed
435        let check = manager.can_execute_tool("dangerous_tool", ToolCategory::System);
436        assert!(!check.is_blocked());
437
438        // Block for session
439        manager.block_tool_for_session("dangerous_tool");
440        let check = manager.can_execute_tool("dangerous_tool", ToolCategory::System);
441        assert!(check.is_blocked());
442
443        // Clear session state
444        manager.clear_session_state();
445        let check = manager.can_execute_tool("dangerous_tool", ToolCategory::System);
446        assert!(!check.is_blocked());
447    }
448}