1use crate::error::AppError;
8use crate::permissions::{PermissionManager, PermissionScope};
9use gestura_core_foundation::execution_mode::{
10 ExecutionMode, ModeManager, ToolCategory, ToolExecutionCheck,
11};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::sync::{Arc, RwLock};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ToolMetadata {
19 pub name: String,
21 pub description: String,
23 pub category: ToolCategory,
25 pub has_side_effects: bool,
27 pub risk_level: u8,
29 pub required_capabilities: Vec<String>,
31}
32
33impl ToolMetadata {
34 pub fn read_only(name: impl Into<String>, description: impl Into<String>) -> Self {
36 Self {
37 name: name.into(),
38 description: description.into(),
39 category: ToolCategory::ReadOnly,
40 has_side_effects: false,
41 risk_level: 0,
42 required_capabilities: vec![],
43 }
44 }
45
46 pub fn write(name: impl Into<String>, description: impl Into<String>) -> Self {
48 Self {
49 name: name.into(),
50 description: description.into(),
51 category: ToolCategory::Write,
52 has_side_effects: true,
53 risk_level: 3,
54 required_capabilities: vec!["filesystem".to_string()],
55 }
56 }
57
58 pub fn shell(name: impl Into<String>, description: impl Into<String>) -> Self {
60 Self {
61 name: name.into(),
62 description: description.into(),
63 category: ToolCategory::Shell,
64 has_side_effects: true,
65 risk_level: 7,
66 required_capabilities: vec!["shell".to_string()],
67 }
68 }
69
70 pub fn network(name: impl Into<String>, description: impl Into<String>) -> Self {
72 Self {
73 name: name.into(),
74 description: description.into(),
75 category: ToolCategory::Network,
76 has_side_effects: false,
77 risk_level: 2,
78 required_capabilities: vec!["network".to_string()],
79 }
80 }
81
82 pub fn git(name: impl Into<String>, description: impl Into<String>) -> Self {
84 Self {
85 name: name.into(),
86 description: description.into(),
87 category: ToolCategory::Git,
88 has_side_effects: true,
89 risk_level: 5,
90 required_capabilities: vec!["git".to_string()],
91 }
92 }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct InspectionResult {
98 pub tool_name: String,
100 pub allowed: bool,
102 pub requires_confirmation: bool,
104 pub reason: String,
106 pub metadata: Option<ToolMetadata>,
108 pub confirmation_message: Option<String>,
110}
111
112impl InspectionResult {
113 pub fn allowed(tool_name: impl Into<String>) -> Self {
115 Self {
116 tool_name: tool_name.into(),
117 allowed: true,
118 requires_confirmation: false,
119 reason: "Tool execution allowed".to_string(),
120 metadata: None,
121 confirmation_message: None,
122 }
123 }
124
125 pub fn needs_confirmation(tool_name: impl Into<String>, message: impl Into<String>) -> Self {
127 let name = tool_name.into();
128 Self {
129 tool_name: name.clone(),
130 allowed: true,
131 requires_confirmation: true,
132 reason: "Tool requires user confirmation".to_string(),
133 metadata: None,
134 confirmation_message: Some(message.into()),
135 }
136 }
137
138 pub fn blocked(tool_name: impl Into<String>, reason: impl Into<String>) -> Self {
140 Self {
141 tool_name: tool_name.into(),
142 allowed: false,
143 requires_confirmation: false,
144 reason: reason.into(),
145 metadata: None,
146 confirmation_message: None,
147 }
148 }
149
150 pub fn with_metadata(mut self, metadata: ToolMetadata) -> Self {
152 self.metadata = Some(metadata);
153 self
154 }
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct ConfirmationRequest {
160 pub id: String,
162 pub tool_name: String,
164 pub arguments: String,
166 pub description: String,
168 pub risk_level: u8,
170 pub remember_decision: bool,
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
176pub enum ConfirmationResponse {
177 Allow,
179 AllowSession,
181 AllowAlways,
183 Deny,
185 DenySession,
187}
188
189pub struct ToolInspectionManager {
197 mode_manager: Arc<RwLock<ModeManager>>,
199 permission_manager: Arc<PermissionManager>,
201 tool_registry: RwLock<HashMap<String, ToolMetadata>>,
203 pending_confirmations: RwLock<HashMap<String, ConfirmationRequest>>,
205}
206
207impl Default for ToolInspectionManager {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213impl ToolInspectionManager {
214 pub fn new() -> Self {
216 let manager = Self {
217 mode_manager: Arc::new(RwLock::new(ModeManager::new())),
218 permission_manager: Arc::new(PermissionManager::new()),
219 tool_registry: RwLock::new(HashMap::new()),
220 pending_confirmations: RwLock::new(HashMap::new()),
221 };
222 manager.register_builtin_tools();
223 manager
224 }
225
226 pub fn with_mode_manager(mode_manager: ModeManager) -> Self {
228 let manager = Self {
229 mode_manager: Arc::new(RwLock::new(mode_manager)),
230 permission_manager: Arc::new(PermissionManager::new()),
231 tool_registry: RwLock::new(HashMap::new()),
232 pending_confirmations: RwLock::new(HashMap::new()),
233 };
234 manager.register_builtin_tools();
235 manager
236 }
237
238 fn register_builtin_tools(&self) {
240 let tools = vec![
241 ToolMetadata::read_only("read_file", "Read contents of a file"),
242 ToolMetadata::read_only("list_directory", "List files in a directory"),
243 ToolMetadata::read_only("search_files", "Search for files by pattern"),
244 ToolMetadata::write("write_file", "Write content to a file"),
245 ToolMetadata::write("edit_file", "Apply an exact replacement inside a file"),
246 ToolMetadata::write("create_file", "Create a new file"),
247 ToolMetadata::write("delete_file", "Delete a file"),
248 ToolMetadata::shell("shell", "Execute a shell command"),
249 ToolMetadata::shell("bash", "Execute a bash command"),
250 ToolMetadata::shell("execute", "Execute a command"),
251 ToolMetadata::network("web_search", "Search the web"),
252 ToolMetadata::network("web_fetch", "Fetch a web page"),
253 ToolMetadata::git("git", "Execute git commands"),
254 ToolMetadata::git("git_status", "Get git repository status"),
255 ToolMetadata::git("git_commit", "Create a git commit"),
256 ToolMetadata::git("git_push", "Push to remote repository"),
257 ];
258
259 if let Ok(mut registry) = self.tool_registry.write() {
260 for tool in tools {
261 registry.insert(tool.name.clone(), tool);
262 }
263 }
264 }
265
266 pub fn register_tool(&self, metadata: ToolMetadata) {
268 if let Ok(mut registry) = self.tool_registry.write() {
269 registry.insert(metadata.name.clone(), metadata);
270 }
271 }
272
273 pub fn get_tool_metadata(&self, tool_name: &str) -> Option<ToolMetadata> {
275 self.tool_registry
276 .read()
277 .ok()
278 .and_then(|r| r.get(tool_name).cloned())
279 }
280
281 pub fn current_mode(&self) -> ExecutionMode {
283 self.mode_manager
284 .read()
285 .map(|m| m.mode())
286 .unwrap_or_default()
287 }
288
289 pub fn set_mode(&self, mode: ExecutionMode) {
291 if let Ok(mut manager) = self.mode_manager.write() {
292 manager.set_mode(mode);
293 }
294 }
295
296 pub fn inspect_tool(
301 &self,
302 tool_name: &str,
303 arguments: Option<&str>,
304 ) -> Result<InspectionResult, AppError> {
305 let metadata = self.get_tool_metadata(tool_name);
307 let category = metadata
308 .as_ref()
309 .map(|m| m.category)
310 .unwrap_or(ToolCategory::Shell); let mode_check = self
314 .mode_manager
315 .read()
316 .map_err(|e| AppError::Io(std::io::Error::other(format!("Lock error: {e}"))))?
317 .can_execute_tool(tool_name, category);
318
319 match mode_check {
320 ToolExecutionCheck::Allowed => {
321 let perm_check = self
323 .permission_manager
324 .check(tool_name, "execute", arguments)?;
325 if perm_check.allowed {
326 Ok(InspectionResult::allowed(tool_name))
327 } else {
328 Ok(InspectionResult::allowed(tool_name))
330 }
331 }
332 ToolExecutionCheck::RequiresConfirmation => {
333 let perm_check = self
335 .permission_manager
336 .check(tool_name, "execute", arguments)?;
337 if perm_check.allowed {
338 Ok(InspectionResult::allowed(tool_name))
339 } else {
340 let message = self.build_confirmation_message(tool_name, arguments, &metadata);
341 let mut result = InspectionResult::needs_confirmation(tool_name, message);
342 if let Some(meta) = metadata {
343 result = result.with_metadata(meta);
344 }
345 Ok(result)
346 }
347 }
348 ToolExecutionCheck::Blocked { reason } => {
349 let mut result = InspectionResult::blocked(tool_name, reason);
350 if let Some(meta) = metadata {
351 result = result.with_metadata(meta);
352 }
353 Ok(result)
354 }
355 }
356 }
357
358 fn build_confirmation_message(
360 &self,
361 tool_name: &str,
362 arguments: Option<&str>,
363 metadata: &Option<ToolMetadata>,
364 ) -> String {
365 let desc = metadata
366 .as_ref()
367 .map(|m| m.description.as_str())
368 .unwrap_or("Execute tool");
369 let args_preview = arguments
370 .map(|a| {
371 if a.len() > 100 {
372 format!("{}...", &a[..100])
373 } else {
374 a.to_string()
375 }
376 })
377 .unwrap_or_default();
378
379 format!(
380 "Allow '{}' to {}?\n\nArguments: {}",
381 tool_name, desc, args_preview
382 )
383 }
384
385 pub fn create_confirmation_request(
387 &self,
388 tool_name: &str,
389 arguments: &str,
390 ) -> ConfirmationRequest {
391 let metadata = self.get_tool_metadata(tool_name);
392 let id = uuid::Uuid::new_v4().to_string();
393 let description = self.build_confirmation_message(tool_name, Some(arguments), &metadata);
394 let risk_level = metadata.as_ref().map(|m| m.risk_level).unwrap_or(5);
395
396 let request = ConfirmationRequest {
397 id: id.clone(),
398 tool_name: tool_name.to_string(),
399 arguments: arguments.to_string(),
400 description,
401 risk_level,
402 remember_decision: false,
403 };
404
405 if let Ok(mut pending) = self.pending_confirmations.write() {
407 pending.insert(id, request.clone());
408 }
409
410 request
411 }
412
413 pub fn handle_confirmation(
415 &self,
416 request_id: &str,
417 response: ConfirmationResponse,
418 ) -> Result<bool, AppError> {
419 let request = self
421 .pending_confirmations
422 .write()
423 .ok()
424 .and_then(|mut p| p.remove(request_id));
425
426 let Some(request) = request else {
427 return Err(AppError::Io(std::io::Error::other(format!(
428 "No pending confirmation with id: {}",
429 request_id
430 ))));
431 };
432
433 match response {
434 ConfirmationResponse::Allow => {
435 Ok(true)
437 }
438 ConfirmationResponse::AllowSession => {
439 if let Ok(mut manager) = self.mode_manager.write() {
441 manager.confirm_tool(&request.tool_name);
442 }
443 Ok(true)
444 }
445 ConfirmationResponse::AllowAlways => {
446 self.permission_manager.grant(
448 &request.tool_name,
449 "execute",
450 PermissionScope::Global,
451 None,
452 )?;
453 Ok(true)
454 }
455 ConfirmationResponse::Deny => {
456 Ok(false)
458 }
459 ConfirmationResponse::DenySession => {
460 if let Ok(mut manager) = self.mode_manager.write() {
462 manager.block_tool_for_session(&request.tool_name);
463 }
464 Ok(false)
465 }
466 }
467 }
468
469 pub fn pending_requests(&self) -> Vec<ConfirmationRequest> {
471 self.pending_confirmations
472 .read()
473 .map(|p| p.values().cloned().collect())
474 .unwrap_or_default()
475 }
476
477 pub fn clear_pending(&self) {
479 if let Ok(mut pending) = self.pending_confirmations.write() {
480 pending.clear();
481 }
482 }
483
484 pub fn list_tools(&self) -> Vec<ToolMetadata> {
486 self.tool_registry
487 .read()
488 .map(|r| r.values().cloned().collect())
489 .unwrap_or_default()
490 }
491
492 pub fn tools_by_category(&self, category: ToolCategory) -> Vec<ToolMetadata> {
494 self.tool_registry
495 .read()
496 .map(|r| {
497 r.values()
498 .filter(|t| t.category == category)
499 .cloned()
500 .collect()
501 })
502 .unwrap_or_default()
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 #[test]
511 fn test_tool_metadata_factories() {
512 let read = ToolMetadata::read_only("test", "Test tool");
513 assert_eq!(read.category, ToolCategory::ReadOnly);
514 assert!(!read.has_side_effects);
515 assert_eq!(read.risk_level, 0);
516
517 let shell = ToolMetadata::shell("bash", "Bash shell");
518 assert_eq!(shell.category, ToolCategory::Shell);
519 assert!(shell.has_side_effects);
520 assert_eq!(shell.risk_level, 7);
521 }
522
523 #[test]
524 fn test_inspection_result_factories() {
525 let allowed = InspectionResult::allowed("test");
526 assert!(allowed.allowed);
527 assert!(!allowed.requires_confirmation);
528
529 let needs_confirm = InspectionResult::needs_confirmation("test", "Please confirm");
530 assert!(needs_confirm.allowed);
531 assert!(needs_confirm.requires_confirmation);
532
533 let blocked = InspectionResult::blocked("test", "Not allowed");
534 assert!(!blocked.allowed);
535 assert!(!blocked.requires_confirmation);
536 }
537
538 #[test]
539 fn test_tool_inspection_manager_creation() {
540 let manager = ToolInspectionManager::new();
541 assert_eq!(manager.current_mode(), ExecutionMode::Agent);
542
543 let tools = manager.list_tools();
545 assert!(!tools.is_empty());
546 assert!(manager.get_tool_metadata("read_file").is_some());
547 assert!(manager.get_tool_metadata("shell").is_some());
548 }
549
550 #[test]
551 fn test_inspect_read_only_tool() {
552 let manager = ToolInspectionManager::new();
553
554 let result = manager.inspect_tool("read_file", None).unwrap();
556 assert!(result.allowed);
557 assert!(!result.requires_confirmation);
558 }
559
560 #[test]
561 fn test_inspect_shell_tool_agent_mode() {
562 let manager = ToolInspectionManager::new();
563
564 let result = manager.inspect_tool("shell", Some("ls -la")).unwrap();
566 assert!(result.allowed);
567 assert!(result.requires_confirmation);
568 }
569
570 #[test]
571 fn test_inspect_shell_tool_auto_mode() {
572 let manager = ToolInspectionManager::new();
573 manager.set_mode(ExecutionMode::Auto);
574
575 let result = manager.inspect_tool("shell", Some("ls -la")).unwrap();
577 assert!(result.allowed);
578 assert!(!result.requires_confirmation);
579 }
580
581 #[test]
582 fn test_inspect_shell_tool_restricted_mode() {
583 let manager = ToolInspectionManager::new();
584 manager.set_mode(ExecutionMode::Restricted);
585
586 let result = manager.inspect_tool("shell", Some("ls -la")).unwrap();
588 assert!(!result.allowed);
589 }
590
591 #[test]
592 fn test_confirmation_flow() {
593 let manager = ToolInspectionManager::new();
594
595 let request = manager.create_confirmation_request("shell", "rm -rf /tmp/test");
597 assert!(!request.id.is_empty());
598 assert_eq!(request.tool_name, "shell");
599
600 assert_eq!(manager.pending_requests().len(), 1);
602
603 let allowed = manager
605 .handle_confirmation(&request.id, ConfirmationResponse::AllowSession)
606 .unwrap();
607 assert!(allowed);
608
609 assert!(manager.pending_requests().is_empty());
611
612 let result = manager.inspect_tool("shell", None).unwrap();
614 assert!(result.allowed);
615 assert!(!result.requires_confirmation);
616 }
617
618 #[test]
619 fn test_tools_by_category() {
620 let manager = ToolInspectionManager::new();
621
622 let read_tools = manager.tools_by_category(ToolCategory::ReadOnly);
623 assert!(!read_tools.is_empty());
624 assert!(
625 read_tools
626 .iter()
627 .all(|t| t.category == ToolCategory::ReadOnly)
628 );
629
630 let shell_tools = manager.tools_by_category(ToolCategory::Shell);
631 assert!(!shell_tools.is_empty());
632 assert!(
633 shell_tools
634 .iter()
635 .all(|t| t.category == ToolCategory::Shell)
636 );
637 }
638}