Coverage for agentos/swarm/code_sandbox.py: 27%

154 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-06 10:59 +0800

1""" 

2v1.9.5: Isolated Code Sandbox for safe agent code generation & execution. 

3 

4Runs generated code in subprocess with timeout, memory limits, 

5test case validation, and structured error extraction for feedback loops. 

6""" 

7 

8from __future__ import annotations 

9 

10import os 

11import subprocess 

12import tempfile 

13import traceback 

14from dataclasses import dataclass, field 

15from typing import Any 

16 

17 

18@dataclass 

19class SandboxResult: 

20 """Result of a sandbox execution.""" 

21 

22 success: bool = False 

23 exit_code: int = -1 

24 stdout: str = "" 

25 stderr: str = "" 

26 exception: str = "" 

27 duration: float = 0.0 

28 max_memory_mb: float = 0.0 

29 test_results: list[dict] = field(default_factory=list) # per-test-case results 

30 all_passed: bool = False 

31 

32 def to_dict(self) -> dict: 

33 return { 

34 "success": self.success, 

35 "exit_code": self.exit_code, 

36 "stdout": self.stdout[:500], 

37 "stderr": self.stderr[:500], 

38 "exception": self.exception[:300], 

39 "duration": f"{self.duration:.2f}s", 

40 "max_memory_mb": f"{self.max_memory_mb:.1f}", 

41 "test_results": self.test_results, 

42 "all_passed": self.all_passed, 

43 } 

44 

45 

46@dataclass 

47class TestCase: 

48 """A single test case for code validation.""" 

49 

50 name: str 

51 input_args: tuple = () 

52 input_kwargs: dict = field(default_factory=dict) 

53 expected_output: Any = None 

54 expected_type: str = "" # e.g. "int", "str", "list" 

55 expected_exception: str = "" # e.g. "ValueError" 

56 weight: float = 1.0 

57 

58 def to_dict(self) -> dict: 

59 d: dict = {"name": self.name, "input_args": str(self.input_args)} 

60 if self.expected_output is not None: 

61 d["expected_output"] = str(self.expected_output)[:100] 

62 if self.expected_exception: 

63 d["expected_exception"] = self.expected_exception 

64 return d 

65 

66 

67class CodeSandbox: 

68 """Isolated execution environment for agent-generated code. 

69 

70 Usage: 

71 sandbox = CodeSandbox(timeout=30, max_memory_mb=256) 

72 result = sandbox.run( 

73 code="def add(a, b): return a + b", 

74 func_name="add", 

75 test_cases=[TestCase(name="1+2", args=(1, 2), expected=3)] 

76 ) 

77 """ 

78 

79 def __init__( 

80 self, 

81 timeout: float = 30.0, 

82 max_memory_mb: int = 256, 

83 allow_imports: bool = True, 

84 forbidden_modules: list[str] | None = None, 

85 ): 

86 self.timeout = timeout 

87 self.max_memory_mb = max_memory_mb 

88 self.allow_imports = allow_imports 

89 self.forbidden_modules = forbidden_modules or [ 

90 "os.system", "subprocess", "shutil", "socket", 

91 "requests", "urllib", "http", "ftp", 

92 ] 

93 

94 def run( 

95 self, 

96 code: str, 

97 func_name: str = "", 

98 test_cases: list[TestCase] | None = None, 

99 setup_code: str = "", 

100 ) -> SandboxResult: 

101 """Execute code in sandbox with test cases. 

102 

103 Args: 

104 code: The code to execute 

105 func_name: Name of the function to test 

106 test_cases: List of test cases to run against func_name 

107 setup_code: Setup code to run before tests (imports, fixtures) 

108 

109 Returns: 

110 SandboxResult with execution details and test outcomes 

111 """ 

112 test_cases = test_cases or [] 

113 

114 # Security check 

115 security_issues = self._check_security(code) 

116 if security_issues: 

117 return SandboxResult( 

118 success=False, 

119 stderr=f"SECURITY VIOLATION: {security_issues}", 

120 exception="Security check failed", 

121 ) 

122 

123 # Syntactic check 

124 syntax_ok, syntax_err = self._check_syntax(code) 

125 if not syntax_ok: 

126 return SandboxResult( 

127 success=False, 

128 stderr=syntax_err, 

129 exception=f"Syntax error: {syntax_err}", 

130 ) 

131 

132 # Write code to temp file and execute 

133 with tempfile.NamedTemporaryFile( 

134 mode="w", suffix=".py", delete=False, 

135 prefix="sandbox_" 

136 ) as f: 

137 script = self._build_script(code, func_name, test_cases, setup_code) 

138 f.write(script) 

139 script_path = f.name 

140 

141 try: 

142 result = self._execute_script(script_path) 

143 finally: 

144 try: 

145 os.unlink(script_path) 

146 except OSError: 

147 pass 

148 

149 return result 

150 

151 def _check_security(self, code: str) -> str: 

152 """Check for forbidden patterns.""" 

153 issues = [] 

154 code_lower = code.lower() 

155 

156 # Check forbidden modules 

157 for mod in self.forbidden_modules: 

158 import_pattern = mod.replace(".", ".") 

159 if f"import {import_pattern}" in code_lower: 

160 issues.append(f"Forbidden import: {mod}") 

161 if f"from {import_pattern}" in code_lower: 

162 issues.append(f"Forbidden import: {mod}") 

163 

164 # Check dangerous builtins 

165 dangerous = [ 

166 ("eval(", "eval() is forbidden"), 

167 ("exec(", "exec() is forbidden"), 

168 ("__import__(", "__import__() is forbidden"), 

169 ("open(", "File I/O blocked in sandbox"), 

170 ] 

171 for pattern, msg in dangerous: 

172 if pattern in code_lower: 

173 issues.append(msg) 

174 

175 return "; ".join(issues) if issues else "" 

176 

177 def _check_syntax(self, code: str) -> tuple[bool, str]: 

178 """Check Python syntax.""" 

179 try: 

180 compile(code, "<sandbox>", "exec") 

181 return True, "" 

182 except SyntaxError as e: 

183 return False, f"Line {e.lineno}: {e.msg}" 

184 

185 def _build_script( 

186 self, 

187 code: str, 

188 func_name: str, 

189 test_cases: list[TestCase], 

190 setup_code: str, 

191 ) -> str: 

192 """Build the full sandbox execution script.""" 

193 

194 tc_defs = [] 

195 tc_names = [] 

196 for tc in test_cases: 

197 tc_names.append(tc.name) 

198 tc_defs.append( 

199 f' "{tc.name}": {{' 

200 f'"args": {list(tc.input_args)}, ' 

201 f'"kwargs": {tc.input_kwargs}, ' 

202 f'"expected": {repr(tc.expected_output) if tc.expected_output is not None else None}, ' 

203 f'"expected_type": "{tc.expected_type}", ' 

204 f'"expected_exception": "{tc.expected_exception}"' 

205 f"}}" 

206 ) 

207 

208 tc_dict = ",\n".join(tc_defs) 

209 tc_list = ", ".join(f'"{n}"' for n in tc_names) 

210 

211 return f'''#!/usr/bin/env python3 

212"""Sandbox execution script — auto-generated by CodeSandbox.""" 

213import json 

214import sys 

215import time 

216import traceback 

217import os 

218 

219# Limit memory 

220try: 

221 import resource 

222 resource.setrlimit(resource.RLIMIT_AS, ({self.max_memory_mb} * 1024 * 1024, {self.max_memory_mb} * 1024 * 1024)) 

223except Exception: 

224 pass 

225 

226# Setup code 

227{setup_code} 

228 

229# User code 

230{code} 

231 

232# Test runner 

233test_cases = {{ 

234{tc_dict} 

235}} 

236 

237func_name = "{func_name}" 

238has_func = func_name and func_name in dir() 

239 

240results = [] 

241total = 0 

242passed = 0 

243 

244if not has_func and len(test_cases) == 0: 

245 print("__SANDBOX_OK__") 

246 print(json.dumps({{"status": "executed", "message": "Code executed, no tests"}})) 

247 sys.exit(0) 

248 

249if not has_func and len(test_cases) > 0: 

250 print("__SANDBOX_ERR__") 

251 print(json.dumps({{"error": f"Function '{{func_name}}' not found in code"}})) 

252 sys.exit(1) 

253 

254func = eval(func_name) 

255all_test_order = [{tc_list}] 

256 

257for tc_name in all_test_order: 

258 tc = test_cases[tc_name] 

259 total += 1 

260 start = time.time() 

261 try: 

262 output = func(*tc["args"], **tc["kwargs"]) 

263 elapsed = time.time() - start 

264 

265 expected = tc.get("expected") 

266 expected_type = tc.get("expected_type") 

267 expected_exc = tc.get("expected_exception") 

268 

269 if expected_exc: 

270 passed_ = False 

271 detail = f"Expected exception {{expected_exc}} but none raised" 

272 elif expected_type and not isinstance(output, eval(expected_type)): 

273 passed_ = False 

274 detail = f"Type mismatch: got {{type(output).__name__}}, expected {{expected_type}}" 

275 elif expected is not None and output != expected: 

276 passed_ = False 

277 detail = f"Expected {{repr(expected)}}, got {{repr(output)}}" 

278 else: 

279 passed_ = True 

280 detail = "OK" 

281 

282 if passed_: 

283 passed += 1 

284 

285 results.append({{ 

286 "name": tc_name, 

287 "passed": passed_, 

288 "output": repr(output)[:200], 

289 "expected": repr(expected)[:200], 

290 "detail": detail, 

291 "duration_ms": round(elapsed * 1000, 2), 

292 }}) 

293 except Exception as e: 

294 exc_name = type(e).__name__ 

295 expected_exc = tc.get("expected_exception", "") 

296 

297 if expected_exc and exc_name == expected_exc: 

298 passed_ = True 

299 detail = f"Expected exception {{exc_name}} raised" 

300 passed += 1 

301 else: 

302 passed_ = False 

303 detail = f"{{exc_name}}: {{str(e)[:200]}}" 

304 

305 results.append({{ 

306 "name": tc_name, 

307 "passed": passed_, 

308 "output": "", 

309 "expected": repr(tc.get("expected"))[:200], 

310 "detail": detail, 

311 "duration_ms": 0, 

312 }}) 

313 

314print("__SANDBOX_RESULTS__") 

315print(json.dumps({{ 

316 "status": "complete", 

317 "total": total, 

318 "passed": passed, 

319 "failed": total - passed, 

320 "all_passed": total > 0 and passed == total, 

321 "test_results": results, 

322}})) 

323''' 

324 

325 def _execute_script(self, script_path: str) -> SandboxResult: 

326 """Execute the sandbox script as subprocess.""" 

327 try: 

328 start = time_module() 

329 proc = subprocess.run( 

330 ["python3", script_path], 

331 capture_output=True, 

332 text=True, 

333 timeout=self.timeout, 

334 cwd="/tmp", 

335 env={ 

336 **os.environ, 

337 "PYTHONDONTWRITEBYTECODE": "1", 

338 "PYTHONPATH": "", 

339 "SANDBOX_MODE": "1", 

340 }, 

341 ) 

342 duration = time_module() - start 

343 except subprocess.TimeoutExpired as e: 

344 return SandboxResult( 

345 success=False, 

346 exit_code=-1, 

347 stderr=str(e.stdout or "") if e.stdout else "", 

348 exception=f"Timeout after {self.timeout}s", 

349 duration=self.timeout, 

350 ) 

351 except Exception as e: 

352 return SandboxResult( 

353 success=False, 

354 exception=str(e), 

355 stderr=traceback.format_exc(), 

356 ) 

357 

358 result = SandboxResult( 

359 exit_code=proc.returncode, 

360 stdout=proc.stdout, 

361 stderr=proc.stderr, 

362 duration=duration, 

363 ) 

364 

365 # Parse test results 

366 if "__SANDBOX_RESULTS__" in proc.stdout: 

367 try: 

368 lines = proc.stdout.split("\n") 

369 json_start = False 

370 json_text = "" 

371 for line in lines: 

372 if json_start: 

373 json_text += line 

374 try: 

375 data = json.loads(json_text) 

376 break 

377 except json.JSONDecodeError: 

378 continue 

379 if "__SANDBOX_RESULTS__" in line: 

380 json_start = True 

381 

382 if data: 

383 result.test_results = data.get("test_results", []) 

384 result.all_passed = data.get("all_passed", False) 

385 passed = data.get("passed", 0) 

386 total = data.get("total", 0) 

387 result.success = total > 0 and passed == total 

388 except Exception: 

389 pass 

390 

391 elif proc.returncode == 0 and not proc.stderr: 

392 result.success = True 

393 

394 # Extract meaningful error info for feedback 

395 if not result.success and result.stderr: 

396 lines = result.stderr.strip().split("\n") 

397 # Get last 3 lines (most relevant error info) 

398 result.exception = "\n".join(lines[-5:]) if len(lines) > 5 else result.stderr 

399 

400 return result 

401 

402 

403def time_module() -> float: 

404 """Get current time in seconds.""" 

405 import time as _time 

406 return _time.time() 

407 

408 

409class CodeFeedbackExtractor: 

410 """Extracts actionable feedback from sandbox failures for retry loops.""" 

411 

412 ERROR_PATTERNS = { 

413 "NameError": "Variable or function not defined. Check spelling and scope.", 

414 "TypeError": "Wrong type passed to function. Check argument types.", 

415 "ValueError": "Invalid value for operation. Check parameter constraints.", 

416 "IndexError": "List index out of range. Check bounds.", 

417 "KeyError": "Dictionary key not found. Check key existence.", 

418 "AttributeError": "Object has no such attribute. Check method/attribute name.", 

419 "ImportError": "Missing import. Add 'import X' or 'from X import Y'.", 

420 "SyntaxError": "Python syntax error. Check indentation, brackets, colons.", 

421 "ZeroDivisionError": "Division by zero. Add guard for zero denominator.", 

422 "RecursionError": "Recursion depth exceeded. Add base case or switch to iteration.", 

423 "TimeoutError": "Code timed out. Check for infinite loops or optimize.", 

424 } 

425 

426 @classmethod 

427 def extract(cls, sandbox_result: SandboxResult) -> list[str]: 

428 """Extract actionable feedback suggestions from sandbox result.""" 

429 suggestions: list[str] = [] 

430 

431 # Check for security issues 

432 if "SECURITY" in sandbox_result.stderr: 

433 suggestions.append("Code violates sandbox security rules; avoid system calls and I/O.") 

434 return suggestions 

435 

436 # Check test failures 

437 for tc in sandbox_result.test_results: 

438 if not tc.get("passed", True): 

439 detail = tc.get("detail", "") 

440 name = tc.get("name", "unknown") 

441 suggestions.append(f"Test '{name}' failed: {detail}") 

442 

443 # Check error patterns 

444 for error_type, suggestion in cls.ERROR_PATTERNS.items(): 

445 if error_type in sandbox_result.stderr or error_type in sandbox_result.exception: 

446 if suggestion not in suggestions: 

447 suggestions.append(suggestion) 

448 

449 # Add specific output mismatch advice 

450 if not sandbox_result.success and not suggestions: 

451 suggestions.append("Code failed to execute. Check logic and edge cases.") 

452 if sandbox_result.stderr: 

453 suggestions.append(f"Error: {sandbox_result.stderr.strip()[:200]}") 

454 

455 return suggestions