An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.
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", ¶ms.command]);
37 c
38 } else {
39 let mut c = Command::new("sh");
40 c.args(["-c", ¶ms.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(¶ms).await;
121 }
122
123 // Validate command
124 match self.validator.validate_shell_command(¶ms.command) {
125 ValidationResult::Allowed => self.execute_command(¶ms).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(¶ms).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(¶ms).await
147 }
148 PermissionResult::Quit => {
149 anyhow::bail!("User requested quit")
150 }
151 }
152 }
153 }
154 }
155}