1mod local;
15
16#[cfg(feature = "sandbox-e2b")]
17mod e2b;
18
19#[cfg(feature = "sandbox-wasm")]
20mod wasm;
21
22pub use local::LocalSandbox;
23
24#[cfg(feature = "sandbox-e2b")]
25pub use e2b::E2BSandbox;
26
27#[cfg(feature = "sandbox-wasm")]
28pub use wasm::WasmSandbox;
29
30use async_trait::async_trait;
31use serde::{Deserialize, Serialize};
32
33use lc_core::tools::{BaseTool, ToolError};
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37#[serde(rename_all = "lowercase")]
38pub enum Language {
39 Python,
40 JavaScript,
41 Rust,
42}
43
44impl std::fmt::Display for Language {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 Language::Python => write!(f, "python"),
48 Language::JavaScript => write!(f, "javascript"),
49 Language::Rust => write!(f, "rust"),
50 }
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct RunResult {
57 pub stdout: String,
59 pub stderr: String,
61 pub exit_code: i32,
63 pub execution_time_ms: u64,
65}
66
67#[derive(Debug, thiserror::Error)]
69pub enum SandboxError {
70 #[error("execution timeout after {0}ms")]
72 Timeout(u64),
73
74 #[error("sandbox error: {0}")]
76 Runtime(String),
77
78 #[error("language not supported: {0}")]
80 UnsupportedLanguage(String),
81}
82
83#[async_trait]
85pub trait CodeSandbox: Send + Sync {
86 async fn run(
88 &self,
89 code: &str,
90 language: Language,
91 timeout_ms: u64,
92 ) -> Result<RunResult, SandboxError>;
93}
94
95#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
97struct SandboxInput {
98 code: String,
100}
101
102pub struct SandboxTool<S: CodeSandbox> {
104 sandbox: S,
105 language: Language,
106 timeout_ms: u64,
107}
108
109impl<S: CodeSandbox> SandboxTool<S> {
110 pub fn new(sandbox: S, language: Language) -> Self {
112 Self {
113 sandbox,
114 language,
115 timeout_ms: 30_000,
116 }
117 }
118
119 pub fn with_timeout(mut self, ms: u64) -> Self {
121 self.timeout_ms = ms;
122 self
123 }
124}
125
126#[async_trait]
127impl<S: CodeSandbox + 'static> BaseTool for SandboxTool<S> {
128 fn name(&self) -> &str {
129 "code_interpreter"
130 }
131
132 fn description(&self) -> &str {
133 "Execute code in a sandboxed environment. \
134 Input JSON: {\"code\": \"...\"}. \
135 Returns stdout, stderr, exit_code, and execution_time_ms. \
136 Supported languages depend on the sandbox backend."
137 }
138
139 async fn run(&self, input: String) -> Result<String, ToolError> {
140 let parsed: SandboxInput = serde_json::from_str(&input)
141 .map_err(|e| ToolError::InvalidInput(format!("JSON parse error: {}", e)))?;
142
143 if parsed.code.trim().is_empty() {
144 return Err(ToolError::InvalidInput(
145 "code must not be empty".to_string(),
146 ));
147 }
148
149 let result = self
150 .sandbox
151 .run(&parsed.code, self.language, self.timeout_ms)
152 .await
153 .map_err(|e| match e {
154 SandboxError::Timeout(ms) => ToolError::Timeout(ms / 1000),
155 SandboxError::Runtime(msg) => ToolError::ExecutionFailed(msg),
156 SandboxError::UnsupportedLanguage(lang) => {
157 ToolError::InvalidInput(format!("unsupported language: {}", lang))
158 }
159 })?;
160
161 serde_json::to_string_pretty(&result)
162 .map_err(|e| ToolError::ExecutionFailed(format!("failed to serialize result: {}", e)))
163 }
164
165 fn args_schema(&self) -> Option<serde_json::Value> {
166 use schemars::schema_for;
167 serde_json::to_value(schema_for!(SandboxInput)).ok()
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn test_language_display() {
177 assert_eq!(Language::Python.to_string(), "python");
178 assert_eq!(Language::JavaScript.to_string(), "javascript");
179 assert_eq!(Language::Rust.to_string(), "rust");
180 }
181
182 #[test]
183 fn test_language_serde() {
184 let json = serde_json::to_string(&Language::Python).unwrap();
185 assert_eq!(json, "\"python\"");
186
187 let lang: Language = serde_json::from_str("\"javascript\"").unwrap();
188 assert_eq!(lang, Language::JavaScript);
189 }
190
191 #[test]
192 fn test_run_result_serialization() {
193 let result = RunResult {
194 stdout: "hello\n".to_string(),
195 stderr: String::new(),
196 exit_code: 0,
197 execution_time_ms: 42,
198 };
199 let json = serde_json::to_string(&result).unwrap();
200 let parsed: RunResult = serde_json::from_str(&json).unwrap();
201 assert_eq!(parsed.stdout, "hello\n");
202 assert_eq!(parsed.exit_code, 0);
203 assert_eq!(parsed.execution_time_ms, 42);
204 }
205
206 #[test]
207 fn test_sandbox_error_display() {
208 let err = SandboxError::Timeout(5000);
209 assert!(err.to_string().contains("5000ms"));
210
211 let err = SandboxError::Runtime("crashed".to_string());
212 assert!(err.to_string().contains("crashed"));
213
214 let err = SandboxError::UnsupportedLanguage("brainfuck".to_string());
215 assert!(err.to_string().contains("brainfuck"));
216 }
217
218 struct MockSandbox;
219
220 #[async_trait]
221 impl CodeSandbox for MockSandbox {
222 async fn run(
223 &self,
224 code: &str,
225 _language: Language,
226 _timeout_ms: u64,
227 ) -> Result<RunResult, SandboxError> {
228 Ok(RunResult {
229 stdout: format!("executed: {}", code),
230 stderr: String::new(),
231 exit_code: 0,
232 execution_time_ms: 1,
233 })
234 }
235 }
236
237 #[tokio::test]
238 async fn test_sandbox_tool_name_and_description() {
239 let tool = SandboxTool::new(MockSandbox, Language::Python);
240 assert_eq!(tool.name(), "code_interpreter");
241 assert!(tool.description().contains("sandbox"));
242 }
243
244 #[tokio::test]
245 async fn test_sandbox_tool_args_schema() {
246 let tool = SandboxTool::new(MockSandbox, Language::Python);
247 let schema = tool.args_schema();
248 assert!(schema.is_some());
249 let schema = schema.unwrap();
250 assert!(schema["properties"]["code"].is_object());
251 }
252
253 #[tokio::test]
254 async fn test_sandbox_tool_run_success() {
255 let tool = SandboxTool::new(MockSandbox, Language::Python);
256 let result = tool.run(r#"{"code": "print(1+1)"}"#.to_string()).await;
257 assert!(result.is_ok());
258 let output: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
259 assert_eq!(output["exit_code"], 0);
260 assert!(output["stdout"].as_str().unwrap().contains("executed"));
261 }
262
263 #[tokio::test]
264 async fn test_sandbox_tool_run_empty_code() {
265 let tool = SandboxTool::new(MockSandbox, Language::Python);
266 let result = tool.run(r#"{"code": " "}"#.to_string()).await;
267 assert!(result.is_err());
268 let err = result.unwrap_err().to_string();
269 assert!(err.contains("empty"));
270 }
271
272 #[tokio::test]
273 async fn test_sandbox_tool_run_invalid_json() {
274 let tool = SandboxTool::new(MockSandbox, Language::Python);
275 let result = tool.run("not json".to_string()).await;
276 assert!(result.is_err());
277 }
278
279 struct TimeoutSandbox;
280
281 #[async_trait]
282 impl CodeSandbox for TimeoutSandbox {
283 async fn run(
284 &self,
285 _code: &str,
286 _language: Language,
287 timeout_ms: u64,
288 ) -> Result<RunResult, SandboxError> {
289 Err(SandboxError::Timeout(timeout_ms))
290 }
291 }
292
293 #[tokio::test]
294 async fn test_sandbox_tool_timeout_maps_to_tool_error() {
295 let tool = SandboxTool::new(TimeoutSandbox, Language::Python).with_timeout(5000);
296 let result = tool
297 .run(r#"{"code": "while True: pass"}"#.to_string())
298 .await;
299 assert!(result.is_err());
300 let err = result.unwrap_err();
301 match err {
302 ToolError::Timeout(secs) => assert_eq!(secs, 5),
303 other => panic!("expected Timeout error, got: {:?}", other),
304 }
305 }
306
307 struct UnsupportedSandbox;
308
309 #[async_trait]
310 impl CodeSandbox for UnsupportedSandbox {
311 async fn run(
312 &self,
313 _code: &str,
314 language: Language,
315 _timeout_ms: u64,
316 ) -> Result<RunResult, SandboxError> {
317 Err(SandboxError::UnsupportedLanguage(language.to_string()))
318 }
319 }
320
321 #[tokio::test]
322 async fn test_sandbox_tool_unsupported_language() {
323 let tool = SandboxTool::new(UnsupportedSandbox, Language::Rust);
324 let result = tool.run(r#"{"code": "fn main(){}"}"#.to_string()).await;
325 assert!(result.is_err());
326 }
327}