1use std::time::Instant;
5
6use async_trait::async_trait;
7use tokio::process::Command;
8
9use super::{CodeSandbox, Language, RunResult, SandboxError};
10
11const BLOCKED_PYTHON_IMPORTS: &[&str] = &[
13 "os",
14 "subprocess",
15 "sys",
16 "shutil",
17 "signal",
18 "ctypes",
19 "multiprocessing",
20 "socket",
21 "http.server",
22 "xmlrpc",
23 "pickle",
24 "shelve",
25 "importlib",
26 "code",
27 "codeop",
28 "compileall",
29 "pty",
30 "commands",
31 "pdb",
32 "webbrowser",
33];
34
35fn contains_dangerous_python_import(code: &str) -> Option<String> {
37 for line in code.lines() {
38 let trimmed = line.trim();
39 if trimmed.starts_with('#') {
40 continue;
41 }
42 let code_part = trimmed.split('#').next().unwrap_or(trimmed);
43 if code_part.contains("import") {
44 for blocked in BLOCKED_PYTHON_IMPORTS {
45 if code_part.contains(&format!("import {}", blocked))
46 || code_part.contains(&format!("from {} ", blocked))
47 || code_part.contains(&format!("from {}.", blocked))
48 || code_part.contains(&format!("from {}import", blocked))
49 {
50 return Some(blocked.to_string());
51 }
52 }
53 }
54 }
55 None
56}
57
58pub struct LocalSandbox {
60 python_path: String,
61 node_path: String,
62}
63
64impl LocalSandbox {
65 pub fn new() -> Self {
67 Self {
68 python_path: Self::find_python(),
69 node_path: "node".to_string(),
70 }
71 }
72
73 pub fn with_python_path(mut self, path: impl Into<String>) -> Self {
75 self.python_path = path.into();
76 self
77 }
78
79 pub fn with_node_path(mut self, path: impl Into<String>) -> Self {
81 self.node_path = path.into();
82 self
83 }
84
85 fn find_python() -> String {
87 for candidate in &["python3", "python"] {
88 if std::process::Command::new(candidate)
89 .arg("--version")
90 .output()
91 .is_ok()
92 {
93 return candidate.to_string();
94 }
95 }
96 "python3".to_string()
97 }
98
99 fn build_command(&self, code: &str, language: Language) -> Result<Command, SandboxError> {
101 match language {
102 Language::Python => {
103 if let Some(blocked) = contains_dangerous_python_import(code) {
104 return Err(SandboxError::Runtime(format!(
105 "Code contains dangerous import: '{}'. \
106 This module is blocked for security in local sandbox.",
107 blocked
108 )));
109 }
110 let mut cmd = Command::new(&self.python_path);
111 cmd.arg("-c").arg(code);
112 Ok(cmd)
113 }
114 Language::JavaScript => {
115 let mut cmd = Command::new(&self.node_path);
116 cmd.arg("-e").arg(code);
117 Ok(cmd)
118 }
119 Language::Rust => Err(SandboxError::UnsupportedLanguage(
120 "Rust compilation is not supported by LocalSandbox (use E2BSandbox or WasmSandbox)"
121 .to_string(),
122 )),
123 }
124 }
125}
126
127impl Default for LocalSandbox {
128 fn default() -> Self {
129 Self::new()
130 }
131}
132
133#[async_trait]
134impl CodeSandbox for LocalSandbox {
135 async fn run(
136 &self,
137 code: &str,
138 language: Language,
139 timeout_ms: u64,
140 ) -> Result<RunResult, SandboxError> {
141 let mut cmd = self.build_command(code, language)?;
142
143 let start = Instant::now();
144
145 let result =
146 tokio::time::timeout(std::time::Duration::from_millis(timeout_ms), cmd.output())
147 .await
148 .map_err(|_| SandboxError::Timeout(timeout_ms))?
149 .map_err(|e| {
150 SandboxError::Runtime(format!("failed to execute subprocess: {}", e))
151 })?;
152
153 let execution_time_ms = start.elapsed().as_millis() as u64;
154
155 let stdout = String::from_utf8_lossy(&result.stdout).to_string();
156 let stderr = String::from_utf8_lossy(&result.stderr).to_string();
157 let exit_code = result.status.code().unwrap_or(-1);
158
159 Ok(RunResult {
160 stdout,
161 stderr,
162 exit_code,
163 execution_time_ms,
164 })
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn test_local_sandbox_default() {
174 let sandbox = LocalSandbox::default();
175 assert!(!sandbox.python_path.is_empty());
176 assert_eq!(sandbox.node_path, "node");
177 }
178
179 #[test]
180 fn test_local_sandbox_custom_paths() {
181 let sandbox = LocalSandbox::new()
182 .with_python_path("/usr/bin/python3.11")
183 .with_node_path("/usr/local/bin/node");
184 assert_eq!(sandbox.python_path, "/usr/bin/python3.11");
185 assert_eq!(sandbox.node_path, "/usr/local/bin/node");
186 }
187
188 #[test]
189 fn test_rust_unsupported() {
190 let sandbox = LocalSandbox::new();
191 let result = sandbox.build_command("fn main(){}", Language::Rust);
192 assert!(result.is_err());
193 match result.unwrap_err() {
194 SandboxError::UnsupportedLanguage(msg) => {
195 assert!(msg.contains("Rust"));
196 }
197 other => panic!("expected UnsupportedLanguage, got: {:?}", other),
198 }
199 }
200
201 #[test]
202 fn test_dangerous_python_import_detection() {
203 assert!(contains_dangerous_python_import("import os").is_some());
204 assert!(contains_dangerous_python_import("from sys import path").is_some());
205 assert!(contains_dangerous_python_import("import subprocess").is_some());
206 assert!(contains_dangerous_python_import("import math").is_none());
207 assert!(contains_dangerous_python_import("import json").is_none());
208 assert!(contains_dangerous_python_import("from datetime import datetime").is_none());
209 assert!(contains_dangerous_python_import("# import os").is_none());
210 }
211
212 #[tokio::test]
213 async fn test_python_dangerous_import_blocked() {
214 let sandbox = LocalSandbox::new();
215 let result = sandbox
216 .run("import os; print(os.getcwd())", Language::Python, 10_000)
217 .await;
218 assert!(result.is_err());
219 let err = result.unwrap_err().to_string();
220 assert!(
221 err.contains("dangerous import"),
222 "Expected dangerous import error, got: {}",
223 err
224 );
225 }
226
227 #[tokio::test]
228 async fn test_python_execution_if_available() {
229 let sandbox = LocalSandbox::new();
230 let result = sandbox.run("print(1 + 2)", Language::Python, 10_000).await;
231
232 match result {
233 Ok(run_result) => {
234 if run_result.exit_code == 0 {
235 assert!(
236 run_result.stdout.trim() == "3",
237 "expected '3', got '{}'",
238 run_result.stdout.trim()
239 );
240 }
241 }
242 Err(SandboxError::Runtime(msg)) => {
243 eprintln!("Python not available (expected in some CI): {}", msg);
244 }
245 Err(other) => panic!("unexpected error: {:?}", other),
246 }
247 }
248
249 #[tokio::test]
250 async fn test_javascript_execution_if_available() {
251 let sandbox = LocalSandbox::new();
252 let result = sandbox
253 .run("console.log(1 + 2)", Language::JavaScript, 10_000)
254 .await;
255
256 match result {
257 Ok(run_result) => {
258 if run_result.exit_code == 0 {
259 assert!(
260 run_result.stdout.trim() == "3",
261 "expected '3', got '{}'",
262 run_result.stdout.trim()
263 );
264 }
265 }
266 Err(SandboxError::Runtime(msg)) => {
267 eprintln!("Node.js not available (expected in some CI): {}", msg);
268 }
269 Err(other) => panic!("unexpected error: {:?}", other),
270 }
271 }
272
273 #[tokio::test]
274 async fn test_rust_execution_unsupported() {
275 let sandbox = LocalSandbox::new();
276 let result = sandbox.run("fn main(){}", Language::Rust, 10_000).await;
277 assert!(result.is_err());
278 match result.unwrap_err() {
279 SandboxError::UnsupportedLanguage(_) => {}
280 other => panic!("expected UnsupportedLanguage, got: {:?}", other),
281 }
282 }
283
284 #[tokio::test]
285 async fn test_execution_timeout() {
286 let sandbox = LocalSandbox::new();
287 let result = sandbox
288 .run("import time; time.sleep(10)", Language::Python, 100)
289 .await;
290
291 match result {
292 Err(SandboxError::Timeout(ms)) => {
293 assert_eq!(ms, 100);
294 }
295 Ok(_) => {
296 }
298 Err(other) => panic!("expected Timeout, got: {:?}", other),
299 }
300 }
301
302 #[tokio::test]
303 async fn test_execution_time_is_recorded() {
304 let sandbox = LocalSandbox::new();
305 let result = sandbox.run("print('hi')", Language::Python, 10_000).await;
306
307 match result {
308 Ok(run_result) => {
309 assert!(run_result.execution_time_ms < 10_000);
310 }
311 Err(_) => {}
312 }
313 }
314
315 #[tokio::test]
316 async fn test_stderr_captured() {
317 let sandbox = LocalSandbox::new();
318 let result = sandbox
319 .run(
320 "import sys; print('error', file=sys.stderr)",
321 Language::Python,
322 10_000,
323 )
324 .await;
325
326 match result {
327 Ok(run_result) => {
328 if run_result.exit_code == 0 {
329 assert!(
330 run_result.stderr.contains("error"),
331 "stderr should contain 'error', got: '{}'",
332 run_result.stderr
333 );
334 }
335 }
336 Err(_) => {}
337 }
338 }
339}