An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.
at new-directions 155 lines 4.8 kB view raw
1use crate::security::permission::{ 2 PermissionHandler, PermissionRequest, PermissionResult, ResourceType, 3}; 4use crate::security::{SecurityValidator, ValidationResult}; 5use crate::tools::Tool; 6use anyhow::Result; 7use async_trait::async_trait; 8use serde::{Deserialize, Serialize}; 9use std::collections::HashSet; 10use std::process::Stdio; 11use std::sync::{Arc, RwLock}; 12use tokio::io::AsyncReadExt; 13use tokio::process::Command; 14 15pub struct RunCommandTool { 16 validator: Arc<SecurityValidator>, 17 permission_handler: Arc<dyn PermissionHandler>, 18 runtime_allowed: Arc<RwLock<HashSet<String>>>, 19} 20 21impl RunCommandTool { 22 pub fn new( 23 validator: Arc<SecurityValidator>, 24 permission_handler: Arc<dyn PermissionHandler>, 25 ) -> Self { 26 Self { 27 validator, 28 permission_handler, 29 runtime_allowed: Arc::new(RwLock::new(HashSet::new())), 30 } 31 } 32 33 async fn execute_command(&self, params: &RunCommandParams) -> Result<String> { 34 let mut cmd = if cfg!(target_os = "windows") { 35 let mut c = Command::new("cmd"); 36 c.args(["/C", &params.command]); 37 c 38 } else { 39 let mut c = Command::new("sh"); 40 c.args(["-c", &params.command]); 41 c 42 }; 43 44 if let Some(ref dir) = params.working_dir { 45 cmd.current_dir(dir); 46 } 47 48 cmd.stdout(Stdio::piped()); 49 cmd.stderr(Stdio::piped()); 50 51 let mut child = cmd.spawn()?; 52 53 let mut stdout = String::new(); 54 let mut stderr = String::new(); 55 56 if let Some(mut out) = child.stdout.take() { 57 out.read_to_string(&mut stdout).await?; 58 } 59 60 if let Some(mut err) = child.stderr.take() { 61 err.read_to_string(&mut stderr).await?; 62 } 63 64 let status = child.wait().await?; 65 66 let output = if !stdout.is_empty() { stdout } else { stderr }; 67 68 if status.success() { 69 Ok(output) 70 } else { 71 anyhow::bail!("Command failed: {}", output) 72 } 73 } 74} 75 76#[derive(Debug, Serialize, Deserialize)] 77struct RunCommandParams { 78 command: String, 79 #[serde(default)] 80 working_dir: Option<String>, 81} 82 83#[async_trait] 84impl Tool for RunCommandTool { 85 fn name(&self) -> &str { 86 "run_command" 87 } 88 89 fn description(&self) -> &str { 90 "Execute a shell command and return its output" 91 } 92 93 fn parameters(&self) -> serde_json::Value { 94 serde_json::json!({ 95 "type": "object", 96 "properties": { 97 "command": { 98 "type": "string", 99 "description": "The shell command to execute" 100 }, 101 "working_dir": { 102 "type": "string", 103 "description": "Optional working directory for the command" 104 } 105 }, 106 "required": ["command"] 107 }) 108 } 109 110 async fn execute(&self, params: serde_json::Value) -> Result<String> { 111 let params: RunCommandParams = serde_json::from_value(params)?; 112 113 // Check if command was previously allowed 114 let base_cmd = params.command.split_whitespace().next().unwrap_or(""); 115 let is_allowed = { 116 let allowed = self.runtime_allowed.read().unwrap(); 117 allowed.contains(base_cmd) 118 }; 119 if is_allowed { 120 return self.execute_command(&params).await; 121 } 122 123 // Validate command 124 match self.validator.validate_shell_command(&params.command) { 125 ValidationResult::Allowed => self.execute_command(&params).await, 126 ValidationResult::Denied(reason) => { 127 anyhow::bail!("Command denied: {}", reason) 128 } 129 ValidationResult::RequiresPermission(reason) => { 130 let request = PermissionRequest { 131 resource_type: ResourceType::ShellCommand, 132 action: params.command.clone(), 133 reason, 134 }; 135 136 match self.permission_handler.request_permission(&request) { 137 PermissionResult::Allow => self.execute_command(&params).await, 138 PermissionResult::Deny => { 139 anyhow::bail!("Permission denied by user") 140 } 141 PermissionResult::AllowAlways(cmd) => { 142 { 143 let mut allowed = self.runtime_allowed.write().unwrap(); 144 allowed.insert(cmd); 145 } 146 self.execute_command(&params).await 147 } 148 PermissionResult::Quit => { 149 anyhow::bail!("User requested quit") 150 } 151 } 152 } 153 } 154 } 155}