brainwires_tool_builtins/
code_exec.rs1use crate::interpreters::{ExecutionLimits, ExecutionRequest, Executor, Language};
13use anyhow::Result;
14use serde::Deserialize;
15use serde_json::{Value, json};
16use std::collections::HashMap;
17
18use brainwires_core::{Tool, ToolContext, ToolInputSchema, ToolResult};
19
20const DEFAULT_TIMEOUT_MS: u64 = 10_000;
22
23const MAX_TIMEOUT_MS: u64 = 60_000;
25
26pub struct CodeExecTool;
28
29impl CodeExecTool {
30 pub fn get_tools() -> Vec<Tool> {
32 vec![Self::execute_code_tool()]
33 }
34
35 fn execute_code_tool() -> Tool {
37 let mut properties = HashMap::new();
38 properties.insert(
39 "language".to_string(),
40 json!({
41 "type": "string",
42 "description": Self::language_description()
43 }),
44 );
45 properties.insert(
46 "code".to_string(),
47 json!({
48 "type": "string",
49 "description": "Source code to execute"
50 }),
51 );
52 properties.insert(
53 "timeout_ms".to_string(),
54 json!({
55 "type": "integer",
56 "description": "Execution timeout in milliseconds (default: 10000, max: 60000)",
57 "default": 10000
58 }),
59 );
60 properties.insert(
61 "context".to_string(),
62 json!({
63 "type": "object",
64 "description": "Context variables to inject into the script (as global variables)",
65 "default": {}
66 }),
67 );
68
69 Tool {
70 name: "execute_code".to_string(),
71 description: Self::tool_description(),
72 input_schema: ToolInputSchema::object(
73 properties,
74 vec!["language".to_string(), "code".to_string()],
75 ),
76 requires_approval: true,
77 defer_loading: true,
78 ..Default::default()
79 }
80 }
81
82 fn language_description() -> String {
84 let langs = ["'rhai'", "'lua'", "'javascript'"];
85 format!(
86 "Programming language identifier: {}. Native interpreters run in-process.",
87 langs.join(", ")
88 )
89 }
90
91 fn tool_description() -> String {
93 String::from(
94 r#"Execute code in a sandboxed environment.
95
96Native interpreters (no Docker required):
97- rhai: Lightweight Rust scripting
98- lua: Lua 5.4
99- javascript: ES2022+ via Boa engine
100
101Examples:
102- Rhai: language="rhai", code="let x = 1 + 2; x"
103- Lua: language="lua", code="return 1 + 2"
104- Use 'context' parameter to inject variables into scripts."#,
105 )
106 }
107
108 pub async fn execute(
110 tool_use_id: &str,
111 tool_name: &str,
112 input: &Value,
113 _context: &ToolContext,
114 ) -> ToolResult {
115 let result = match tool_name {
116 "execute_code" => Self::execute_code(input).await,
117 _ => Err(anyhow::anyhow!(
118 "Unknown code execution tool: {}",
119 tool_name
120 )),
121 };
122
123 match result {
124 Ok(output) => ToolResult::success(tool_use_id.to_string(), output),
125 Err(e) => ToolResult::error(
126 tool_use_id.to_string(),
127 format!("Code execution failed: {}", e),
128 ),
129 }
130 }
131
132 async fn execute_code(input: &Value) -> Result<String> {
134 #[derive(Deserialize)]
135 struct ExecuteCodeInput {
136 language: String,
137 code: String,
138 #[serde(default = "default_timeout")]
139 timeout_ms: u64,
140 #[serde(default)]
141 context: Option<serde_json::Value>,
142 }
143
144 fn default_timeout() -> u64 {
145 DEFAULT_TIMEOUT_MS
146 }
147
148 let params: ExecuteCodeInput = serde_json::from_value(input.clone())?;
149 let timeout_ms = params.timeout_ms.min(MAX_TIMEOUT_MS);
150
151 let language_lower = params.language.to_lowercase();
152 if let Some(lang) = Self::parse_native_language(&language_lower) {
153 return Self::execute_native(lang, ¶ms.code, timeout_ms, params.context.as_ref());
154 }
155
156 let supported = Self::supported_languages();
157 Err(anyhow::anyhow!(
158 "Language '{}' is not supported. Supported languages: {}.",
159 params.language,
160 supported.join(", ")
161 ))
162 }
163
164 fn parse_native_language(lang: &str) -> Option<Language> {
166 match lang {
167 "rhai" => Some(Language::Rhai),
168 "lua" => Some(Language::Lua),
169 "javascript" | "js" => Some(Language::JavaScript),
170 _ => None,
171 }
172 }
173
174 fn supported_languages() -> Vec<&'static str> {
176 vec!["rhai", "lua", "javascript"]
177 }
178
179 fn execute_native(
181 language: Language,
182 code: &str,
183 timeout_ms: u64,
184 context: Option<&serde_json::Value>,
185 ) -> Result<String> {
186 let limits = ExecutionLimits {
187 max_timeout_ms: timeout_ms,
188 max_memory_mb: 256,
189 max_output_bytes: 1_048_576,
190 max_operations: 100_000,
191 max_call_depth: 64,
192 max_string_length: 1_000_000,
193 max_array_length: 10_000,
194 max_map_size: 10_000,
195 };
196
197 let executor = Executor::with_limits(limits.clone());
198
199 let request = ExecutionRequest {
200 language,
201 code: code.to_string(),
202 stdin: None,
203 timeout_ms,
204 memory_limit_mb: 256,
205 context: context.cloned(),
206 limits: Some(limits),
207 };
208
209 let result = executor.execute(request);
210
211 let lang_name = match language {
212 Language::Rhai => "rhai",
213 Language::Lua => "lua",
214 Language::JavaScript => "javascript",
215 };
216
217 let mut output = format!(
218 "Language: {} (native)\nSuccess: {}\nDuration: {}ms\n",
219 lang_name, result.success, result.timing_ms
220 );
221
222 if let Some(ops) = result.operations_count {
223 output.push_str(&format!("Operations: {}\n", ops));
224 }
225
226 output.push_str("\n--- stdout ---\n");
227 if result.stdout.is_empty() {
228 output.push_str("(empty)\n");
229 } else {
230 output.push_str(&result.stdout);
231 if !result.stdout.ends_with('\n') {
232 output.push('\n');
233 }
234 }
235
236 if !result.stderr.is_empty() {
237 output.push_str("\n--- stderr ---\n");
238 output.push_str(&result.stderr);
239 if !result.stderr.ends_with('\n') {
240 output.push('\n');
241 }
242 }
243
244 if let Some(json_result) = &result.result {
245 output.push_str(&format!("\n--- result ---\n{}\n", json_result));
246 }
247
248 if let Some(error) = &result.error {
249 output.push_str(&format!("\n--- error ---\n{}\n", error));
250 }
251
252 Ok(output)
253 }
254
255 pub async fn execute_code_helper(language: &str, code: &str) -> Result<String, String> {
257 let input = json!({
258 "language": language,
259 "code": code
260 });
261 Self::execute_code(&input).await.map_err(|e| e.to_string())
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn test_get_tools() {
271 let tools = CodeExecTool::get_tools();
272 assert_eq!(tools.len(), 1);
273 assert_eq!(tools[0].name, "execute_code");
274 assert!(tools[0].requires_approval);
275 assert!(tools[0].defer_loading);
276 }
277
278 #[test]
279 fn test_execute_code_tool_definition() {
280 let tool = CodeExecTool::execute_code_tool();
281 assert_eq!(tool.name, "execute_code");
282 assert!(tool.description.contains("rhai"));
283 assert!(tool.description.contains("lua"));
284 }
285
286 #[test]
287 fn test_parse_native_language() {
288 assert_eq!(
289 CodeExecTool::parse_native_language("rhai"),
290 Some(Language::Rhai)
291 );
292 assert_eq!(
293 CodeExecTool::parse_native_language("lua"),
294 Some(Language::Lua)
295 );
296 assert_eq!(CodeExecTool::parse_native_language("RHAI"), None);
297 }
298
299 #[test]
300 fn test_supported_languages() {
301 let langs = CodeExecTool::supported_languages();
302 assert!(langs.contains(&"rhai"));
303 assert!(langs.contains(&"lua"));
304 }
305
306 #[test]
307 fn test_execute_native_rhai() {
308 let result = CodeExecTool::execute_native(Language::Rhai, "let x = 1 + 2; x", 10000, None);
309 assert!(result.is_ok());
310 let output = result.unwrap();
311 assert!(output.contains("Language: rhai (native)"));
312 assert!(output.contains("Success: true"));
313 }
314
315 #[test]
316 fn test_execute_native_lua() {
317 let result = CodeExecTool::execute_native(Language::Lua, "return 1 + 2", 10000, None);
318 assert!(result.is_ok());
319 let output = result.unwrap();
320 assert!(output.contains("Language: lua (native)"));
321 assert!(output.contains("Success: true"));
322 }
323
324 #[test]
325 fn test_execute_native_with_context() {
326 let context = json!({
327 "x": 10,
328 "y": 20
329 });
330 let result = CodeExecTool::execute_native(Language::Rhai, "x + y", 10000, Some(&context));
331 assert!(result.is_ok());
332 let output = result.unwrap();
333 assert!(output.contains("30"));
334 }
335
336 #[tokio::test]
337 async fn test_execute_unknown_tool() {
338 let context = ToolContext::default();
339 let result = CodeExecTool::execute("test-id", "unknown_tool", &json!({}), &context).await;
340 assert!(result.is_error);
341 assert!(result.content.contains("Unknown code execution tool"));
342 }
343
344 #[tokio::test]
345 async fn test_execute_code_routes_to_rhai() {
346 let context = ToolContext::default();
347 let input = json!({
348 "language": "rhai",
349 "code": "42"
350 });
351
352 let result = CodeExecTool::execute("test-id", "execute_code", &input, &context).await;
353 assert!(!result.is_error);
354 assert!(result.content.contains("Language: rhai"));
355 }
356
357 #[tokio::test]
358 async fn test_execute_unsupported_language() {
359 let context = ToolContext::default();
360 let input = json!({
361 "language": "cobol",
362 "code": "DISPLAY 'HELLO'"
363 });
364
365 let result = CodeExecTool::execute("test-id", "execute_code", &input, &context).await;
366 assert!(result.is_error);
367 }
368}