1use serde::{Deserialize, Serialize};
7use std::collections::HashSet;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
11pub enum ExecutionMode {
12 #[default]
14 #[serde(alias = "Agent")]
15 Agent,
16 Auto,
18 Restricted,
20}
21
22impl ExecutionMode {
23 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
81pub enum ToolPermission {
82 Allowed,
84 RequiresConfirmation,
86 Blocked,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
92pub enum ToolCategory {
93 ReadOnly,
95 Write,
97 Shell,
99 Network,
101 System,
103 Git,
105}
106
107impl ToolCategory {
108 pub fn default_permission(&self, mode: ExecutionMode) -> ToolPermission {
110 match (self, mode) {
111 (Self::ReadOnly, _) => ToolPermission::Allowed,
113
114 (Self::Write, ExecutionMode::Auto) => ToolPermission::Allowed,
116 (Self::Write, ExecutionMode::Agent) => ToolPermission::RequiresConfirmation,
117 (Self::Write, ExecutionMode::Restricted) => ToolPermission::Blocked,
118
119 (Self::Shell, ExecutionMode::Auto) => ToolPermission::Allowed,
121 (Self::Shell, ExecutionMode::Agent) => ToolPermission::RequiresConfirmation,
122 (Self::Shell, ExecutionMode::Restricted) => ToolPermission::Blocked,
123
124 (Self::Network, ExecutionMode::Auto) => ToolPermission::Allowed,
126 (Self::Network, ExecutionMode::Agent) => ToolPermission::Allowed,
127 (Self::Network, ExecutionMode::Restricted) => ToolPermission::RequiresConfirmation,
128
129 (Self::System, ExecutionMode::Auto) => ToolPermission::RequiresConfirmation,
131 (Self::System, ExecutionMode::Agent) => ToolPermission::RequiresConfirmation,
132 (Self::System, ExecutionMode::Restricted) => ToolPermission::Blocked,
133
134 (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#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct ModeConfig {
145 pub mode: ExecutionMode,
147 pub tool_overrides: std::collections::HashMap<String, ToolPermission>,
149 pub persist_mode: bool,
151 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 pub fn with_mode(mode: ExecutionMode) -> Self {
169 Self {
170 mode,
171 ..Default::default()
172 }
173 }
174
175 pub fn get_tool_permission(&self, tool_name: &str, category: ToolCategory) -> ToolPermission {
177 if let Some(permission) = self.tool_overrides.get(tool_name) {
179 return *permission;
180 }
181 category.default_permission(self.mode)
183 }
184
185 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 pub fn remove_tool_override(&mut self, tool_name: &str) {
192 self.tool_overrides.remove(tool_name);
193 }
194
195 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 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#[derive(Debug, Clone)]
214pub struct ModeManager {
215 config: ModeConfig,
216 session_blocked_tools: HashSet<String>,
218 confirmed_tools: HashSet<String>,
220}
221
222impl ModeManager {
223 pub fn new() -> Self {
225 Self::with_config(ModeConfig::default())
226 }
227
228 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 pub fn mode(&self) -> ExecutionMode {
239 self.config.mode
240 }
241
242 pub fn config(&self) -> &ModeConfig {
244 &self.config
245 }
246
247 pub fn set_mode(&mut self, mode: ExecutionMode) {
249 self.config.mode = mode;
250 self.confirmed_tools.clear();
252 }
253
254 pub fn can_execute_tool(&self, tool_name: &str, category: ToolCategory) -> ToolExecutionCheck {
256 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 pub fn confirm_tool(&mut self, tool_name: impl Into<String>) {
285 self.confirmed_tools.insert(tool_name.into());
286 }
287
288 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 pub fn clear_session_state(&mut self) {
295 self.confirmed_tools.clear();
296 self.session_blocked_tools.clear();
297 }
298
299 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#[derive(Debug, Clone, PartialEq, Eq)]
313pub enum ToolExecutionCheck {
314 Allowed,
316 RequiresConfirmation,
318 Blocked { reason: String },
320}
321
322impl ToolExecutionCheck {
323 pub fn is_allowed(&self) -> bool {
325 matches!(self, Self::Allowed)
326 }
327
328 pub fn requires_confirmation(&self) -> bool {
330 matches!(self, Self::RequiresConfirmation)
331 }
332
333 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 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 assert_eq!(
382 ToolCategory::Shell.default_permission(ExecutionMode::Agent),
383 ToolPermission::RequiresConfirmation
384 );
385
386 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 assert_eq!(
399 config.get_tool_permission("run_shell", ToolCategory::Shell),
400 ToolPermission::RequiresConfirmation
401 );
402
403 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 let check = manager.can_execute_tool("run_shell", ToolCategory::Shell);
417 assert!(check.requires_confirmation());
418
419 manager.confirm_tool("run_shell");
421 let check = manager.can_execute_tool("run_shell", ToolCategory::Shell);
422 assert!(check.is_allowed());
423
424 manager.set_mode(ExecutionMode::Auto);
426 let check = manager.can_execute_tool("run_shell", ToolCategory::Shell);
427 assert!(check.is_allowed()); }
429
430 #[test]
431 fn test_mode_manager_session_block() {
432 let mut manager = ModeManager::with_config(ModeConfig::with_mode(ExecutionMode::Auto));
433
434 let check = manager.can_execute_tool("dangerous_tool", ToolCategory::System);
436 assert!(!check.is_blocked());
437
438 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 manager.clear_session_state();
445 let check = manager.can_execute_tool("dangerous_tool", ToolCategory::System);
446 assert!(!check.is_blocked());
447 }
448}