Skip to main content

brainwires_tool_builtins/
code_exec.rs

1//! Code Execution Tool - Sandboxed code execution via embedded interpreters
2//!
3//! Provides a unified `execute_code` tool that supports multiple languages:
4//!
5//! ## Default (Native Interpreters via brainwires-tools interpreters module crate)
6//! - **Rhai**: Lightweight Rust scripting (always available)
7//! - **Lua**: Lua 5.4 via mlua (always available)
8//! - **JavaScript**: ES2022+ via Boa engine (with feature flag)
9//!
10//! Requires the `interpreters` feature flag.
11
12use 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
20/// Default execution timeout in milliseconds
21const DEFAULT_TIMEOUT_MS: u64 = 10_000;
22
23/// Maximum allowed timeout in milliseconds
24const MAX_TIMEOUT_MS: u64 = 60_000;
25
26/// Code execution tool with native interpreters
27pub struct CodeExecTool;
28
29impl CodeExecTool {
30    /// Get all code execution tool definitions
31    pub fn get_tools() -> Vec<Tool> {
32        vec![Self::execute_code_tool()]
33    }
34
35    /// Execute code tool definition
36    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    /// Generate language description based on available features
83    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    /// Generate tool description
92    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    /// Execute a code execution tool
109    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    /// Execute code implementation - routes to appropriate backend
133    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, &params.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    /// Parse language string to native Language enum
165    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    /// Get list of supported native languages
175    fn supported_languages() -> Vec<&'static str> {
176        vec!["rhai", "lua", "javascript"]
177    }
178
179    /// Execute code using native interpreter
180    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    /// Execute code (for orchestrator) - simplified interface
256    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}