Coverage for agentos/sandbox/__init__.py: 0%

255 statements  

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

1""" 

2AgentOS v1.14.6 — Self-Evolution Safe Sandbox. 

3 

4Docker-based isolated execution environment for agent self-improvement. 

5Allows agents to generate, test, and iterate on code/tools without 

6risking the host system. 

7 

8Features: 

9- Docker container isolation (network/filesystem/process) 

10- Resource limits (CPU, memory, disk, timeout) 

11- Execution result capture (stdout, stderr, exit code) 

12- Code safety validation (dangerous import blocklist) 

13- Snapshot/rollback (Docker commit-based) 

14- Rate limiting to prevent runaway loops 

15- Audit trail for all executed code 

16 

17Security layers: 

18 1. Docker namespace isolation 

19 2. Seccomp profile (restricted syscalls) 

20 3. Network disabled by default 

21 4. Read-only rootfs option 

22 5. tmpfs for writable scratch space 

23 6. Resource cgroups limits 

24 

25Inspired by: E2B, Open Interpreter sandbox, Modal 

26""" 

27 

28from __future__ import annotations 

29 

30import hashlib 

31import logging 

32import re 

33import subprocess 

34import tempfile 

35import time 

36import uuid 

37from dataclasses import dataclass, field 

38from enum import StrEnum 

39from pathlib import Path 

40 

41logger = logging.getLogger(__name__) 

42 

43 

44# ── Types ─────────────────────────────────── 

45 

46 

47class SandboxStatus(StrEnum): 

48 CREATED = "created" 

49 RUNNING = "running" 

50 COMPLETED = "completed" 

51 TIMEOUT = "timeout" 

52 ERROR = "error" 

53 KILLED = "killed" 

54 

55 

56class Language(StrEnum): 

57 PYTHON = "python" 

58 BASH = "bash" 

59 NODE = "node" 

60 

61 

62@dataclass 

63class SandboxConfig: 

64 """沙箱配置。""" 

65 

66 image: str = "python:3.11-slim" # Docker 镜像 

67 language: Language = Language.PYTHON 

68 timeout_s: float = 30.0 # 执行超时 

69 memory_mb: int = 512 # 内存限制 (兼容测试) 

70 max_cpu_cores: float = 1.0 # CPU 限制 

71 max_disk_mb: int = 100 # 磁盘限制 

72 network_enabled: bool = False # 网络访问(默认关闭) 

73 read_only_rootfs: bool = True # 只读根文件系统 

74 allow_write: bool = True # 是否允许写入 scratch 空间 

75 

76 max_memory_mb = memory_mb # 别名,指向同一默认值 

77 

78 def __post_init__(self): 

79 self.max_memory_mb = self.memory_mb 

80 

81 # Paths 

82 work_dir: str = "/sandbox" # 工作目录 

83 scratch_dir: str = "/tmp/scratch" # 可写临时空间 

84 

85 # Safety 

86 dangerous_imports: list[str] = field( 

87 default_factory=lambda: [ 

88 "os.system", 

89 "subprocess", 

90 "shutil.rmtree", 

91 "__import__('os')", 

92 "eval(", 

93 "exec(", 

94 "compile(", 

95 "pty", 

96 "ctypes", 

97 ] 

98 ) 

99 

100 def to_docker_args(self, container_name: str) -> list[str]: 

101 """生成 docker run 参数。""" 

102 args = [ 

103 "docker", 

104 "run", 

105 "--rm", 

106 "--name", 

107 container_name, 

108 "--cpus", 

109 str(self.max_cpu_cores), 

110 "--memory=" f"{self.max_memory_mb}m", 

111 "--storage-opt", 

112 f"size={self.max_disk_mb}m", 

113 "--workdir", 

114 self.work_dir, 

115 ] 

116 

117 if not self.network_enabled: 

118 args.append("--network=none") 

119 

120 if self.read_only_rootfs: 

121 args.extend(["--read-only"]) 

122 # tmpfs for writable scratch 

123 args.extend( 

124 [ 

125 "--tmpfs", 

126 "/tmp:exec,size=200m", 

127 "--tmpfs", 

128 f"{self.scratch_dir}:exec,size=100m", 

129 ] 

130 ) 

131 

132 return args 

133 

134 

135@dataclass 

136class SandboxResult: 

137 """沙箱执行结果。""" 

138 

139 execution_id: str = field(default_factory=lambda: f"exec-{uuid.uuid4().hex[:8]}") 

140 status: SandboxStatus = SandboxStatus.CREATED 

141 stdout: str = "" 

142 stderr: str = "" 

143 exit_code: int = -1 

144 elapsed_s: float = 0.0 

145 error: str = "" 

146 truncated: bool = False # 输出是否被截断 

147 

148 max_output_bytes: int = 1024 * 100 # 100KB max output 

149 

150 def to_dict(self) -> dict: 

151 return { 

152 "execution_id": self.execution_id, 

153 "status": self.status.value, 

154 "stdout_preview": self.stdout[:500], 

155 "stderr_preview": self.stderr[:500], 

156 "exit_code": self.exit_code, 

157 "elapsed_s": self.elapsed_s, 

158 "error": self.error, 

159 } 

160 

161 

162# ── Code Validator ────────────────────────── 

163 

164 

165class CodeValidator: 

166 """代码安全校验器。""" 

167 

168 # Python 危险模式 

169 PY_DANGEROUS_PATTERNS = [ 

170 r"os\.system\s*\(", 

171 r"subprocess\.(call|run|Popen|check_output)\s*\(", 

172 r"shutil\.rmtree\s*\(", 

173 r"__import__\s*\(\s*['\"]os['\"]", 

174 r"eval\s*\(", 

175 r"exec\s*\(", 

176 r"compile\s*\(", 

177 r"importlib\.import_module\s*\(", 

178 r"ctypes\.", 

179 r"os\.remove\s*\(", 

180 r"os\.unlink\s*\(", 

181 r"os\.rmdir\s*\(", 

182 r"socket\.", 

183 r"requests\.(get|post|put|delete|patch)", 

184 r"urllib\.", 

185 r"open\s*\([^)]*['\"]w", 

186 r"pty\.", 

187 r"multiprocessing\.", 

188 ] 

189 

190 # Bash 危险模式 

191 SH_DANGEROUS_PATTERNS = [ 

192 r"rm\s+-rf\s+/", 

193 r"mkfs\.", 

194 r"dd\s+if=", 

195 r">\s*/dev/", 

196 r"chmod\s+777", 

197 r"wget\s+", 

198 r"curl\s+", 

199 r"nc\s+", 

200 r"telnet\s+", 

201 ] 

202 

203 @classmethod 

204 def validate_python(cls, code: str) -> tuple[bool, list[str]]: 

205 """校验 Python 代码安全性。""" 

206 violations = [] 

207 for pattern in cls.PY_DANGEROUS_PATTERNS: 

208 if re.search(pattern, code, re.IGNORECASE): 

209 violations.append(f"Dangerous pattern: {pattern}") 

210 return len(violations) == 0, violations 

211 

212 @classmethod 

213 def validate_bash(cls, code: str) -> tuple[bool, list[str]]: 

214 """校验 Bash 代码安全性。""" 

215 violations = [] 

216 for pattern in cls.SH_DANGEROUS_PATTERNS: 

217 if re.search(pattern, code, re.IGNORECASE): 

218 violations.append(f"Dangerous pattern: {pattern}") 

219 return len(violations) == 0, violations 

220 

221 @classmethod 

222 def validate(cls, code: str, language: Language) -> tuple[bool, list[str]]: 

223 if language == Language.PYTHON: 

224 return cls.validate_python(code) 

225 elif language == Language.BASH: 

226 return cls.validate_bash(code) 

227 return True, [] 

228 

229 

230# ── Docker Sandbox ────────────────────────── 

231 

232 

233class DockerSandbox: 

234 """Docker 沙箱执行器。 

235 

236 Usage: 

237 sandbox = DockerSandbox(SandboxConfig()) 

238 result = sandbox.run("print('hello world')") 

239 print(result.stdout) # "hello world" 

240 """ 

241 

242 def __init__(self, config: SandboxConfig | None = None): 

243 self._config = config or SandboxConfig() 

244 self._validator = CodeValidator() 

245 self._execution_count: int = 0 

246 self._rate_limit_window: float = 60.0 # 1 minute 

247 self._max_executions_per_window: int = 100 

248 self._execution_timestamps: list[float] = [] 

249 

250 def run(self, code: str, language: Language | None = None) -> SandboxResult: 

251 """在沙箱中执行代码。""" 

252 lang = language or self._config.language 

253 result = SandboxResult() 

254 

255 # Rate limiting 

256 if not self._check_rate_limit(): 

257 result.status = SandboxStatus.ERROR 

258 result.error = "Rate limit exceeded. Max 100 executions/minute." 

259 return result 

260 

261 # Validate code 

262 safe, violations = self._validator.validate(code, lang) 

263 if not safe: 

264 result.status = SandboxStatus.ERROR 

265 result.error = f"Code validation failed: {', '.join(violations)}" 

266 return result 

267 

268 # Check Docker availability 

269 if not self._docker_available(): 

270 result.status = SandboxStatus.ERROR 

271 result.error = "Docker is not available on this system" 

272 return result 

273 

274 # Execute 

275 container_name = f"agentos-sandbox-{result.execution_id}" 

276 start = time.time() 

277 

278 try: 

279 if lang == Language.PYTHON: 

280 result = self._run_python(code, container_name, result) 

281 elif lang == Language.BASH: 

282 result = self._run_bash(code, container_name, result) 

283 else: 

284 result.status = SandboxStatus.ERROR 

285 result.error = f"Unsupported language: {lang}" 

286 except Exception as e: 

287 result.status = SandboxStatus.ERROR 

288 result.error = str(e) 

289 finally: 

290 # Cleanup just in case 

291 self._cleanup(container_name) 

292 

293 result.elapsed_s = time.time() - start 

294 self._execution_count += 1 

295 self._execution_timestamps.append(time.time()) 

296 

297 return result 

298 

299 def run_batch( 

300 self, 

301 code_blocks: list[str], 

302 language: Language | None = None, 

303 ) -> list[SandboxResult]: 

304 """批量执行代码块。""" 

305 return [self.run(code, language) for code in code_blocks] 

306 

307 def _run_python(self, code: str, container_name: str, result: SandboxResult) -> SandboxResult: 

308 """执行 Python 代码。""" 

309 # Write code to temp file 

310 code_hash = hashlib.sha256(code.encode()).hexdigest()[:12] 

311 tmp_path = Path(tempfile.gettempdir()) / f"agentos_sb_{code_hash}.py" 

312 tmp_path.write_text(code, encoding="utf-8") 

313 

314 args = self._config.to_docker_args(container_name) 

315 args.extend( 

316 [ 

317 "-v", 

318 f"{tmp_path}:/sandbox/script.py:ro", 

319 self._config.image, 

320 "timeout", 

321 str(int(self._config.timeout_s)), 

322 "python", 

323 "-u", 

324 "/sandbox/script.py", 

325 ] 

326 ) 

327 

328 try: 

329 proc = subprocess.run( 

330 args, 

331 capture_output=True, 

332 timeout=self._config.timeout_s + 5, 

333 text=True, 

334 ) 

335 

336 result.exit_code = proc.returncode 

337 result.stdout = proc.stdout[: result.max_output_bytes] 

338 result.stderr = proc.stderr[: result.max_output_bytes] 

339 

340 if len(proc.stdout) > result.max_output_bytes: 

341 result.truncated = True 

342 

343 if proc.returncode == 124: # timeout kill 

344 result.status = SandboxStatus.TIMEOUT 

345 elif proc.returncode == 137: # OOM kill 

346 result.status = SandboxStatus.KILLED 

347 result.error = "Out of memory" 

348 elif proc.returncode != 0: 

349 result.status = SandboxStatus.COMPLETED 

350 else: 

351 result.status = SandboxStatus.COMPLETED 

352 

353 except subprocess.TimeoutExpired: 

354 result.status = SandboxStatus.TIMEOUT 

355 result.error = f"Host timeout after {self._config.timeout_s + 5}s" 

356 self._cleanup(container_name) 

357 finally: 

358 # Clean temp file 

359 try: 

360 tmp_path.unlink() 

361 except Exception: 

362 pass 

363 

364 return result 

365 

366 def _run_bash(self, code: str, container_name: str, result: SandboxResult) -> SandboxResult: 

367 """执行 Bash 脚本。""" 

368 args = self._config.to_docker_args(container_name) 

369 args.extend( 

370 [ 

371 self._config.image, 

372 "timeout", 

373 str(int(self._config.timeout_s)), 

374 "bash", 

375 "-c", 

376 code, 

377 ] 

378 ) 

379 

380 try: 

381 proc = subprocess.run( 

382 args, 

383 capture_output=True, 

384 timeout=self._config.timeout_s + 5, 

385 text=True, 

386 ) 

387 result.exit_code = proc.returncode 

388 result.stdout = proc.stdout[: result.max_output_bytes] 

389 result.stderr = proc.stderr[: result.max_output_bytes] 

390 result.status = ( 

391 SandboxStatus.COMPLETED if proc.returncode == 0 else SandboxStatus.COMPLETED 

392 ) 

393 

394 except subprocess.TimeoutExpired: 

395 result.status = SandboxStatus.TIMEOUT 

396 self._cleanup(container_name) 

397 

398 return result 

399 

400 def _docker_available(self) -> bool: 

401 """检查 Docker 是否可用。""" 

402 try: 

403 subprocess.run( 

404 ["docker", "info"], 

405 capture_output=True, 

406 timeout=5, 

407 ) 

408 return True 

409 except Exception: 

410 logger.warning("Docker is not available") 

411 return False 

412 

413 def _cleanup(self, container_name: str) -> None: 

414 """清理容器。""" 

415 try: 

416 subprocess.run( 

417 ["docker", "rm", "-f", container_name], 

418 capture_output=True, 

419 timeout=5, 

420 ) 

421 except Exception: 

422 pass 

423 

424 def _check_rate_limit(self) -> bool: 

425 """检查速率限制。""" 

426 now = time.time() 

427 cutoff = now - self._rate_limit_window 

428 self._execution_timestamps = [t for t in self._execution_timestamps if t > cutoff] 

429 return len(self._execution_timestamps) < self._max_executions_per_window 

430 

431 

432# ── Evolution Runner ──────────────────────── 

433 

434 

435@dataclass 

436class EvolutionStep: 

437 """自进化的一步。""" 

438 

439 step_id: str = field(default_factory=lambda: f"ev-{uuid.uuid4().hex[:8]}") 

440 iteration: int = 0 

441 prompt: str = "" # 指导 Agent 生成代码的 prompt 

442 generated_code: str = "" 

443 test_code: str = "" 

444 result: SandboxResult | None = None 

445 test_result: SandboxResult | None = None 

446 score: float = 0.0 

447 accepted: bool = False 

448 error: str = "" 

449 

450 

451class SelfEvolutionRunner: 

452 """自进化执行器。 

453 

454 让 Agent 在安全沙箱中迭代生成、测试、改进代码。 

455 每次迭代自动评分,保留最优版本。 

456 

457 Usage: 

458 runner = SelfEvolutionRunner(sandbox) 

459 result = await runner.evolve( 

460 prompt="Write a function that sorts a list with quicksort", 

461 test_cases=["assert quicksort([3,1,2]) == [1,2,3]"], 

462 max_iterations=5, 

463 ) 

464 """ 

465 

466 def __init__(self, sandbox: DockerSandbox | None = None): 

467 self._sandbox = sandbox or DockerSandbox() 

468 self._evolution_history: list[EvolutionStep] = [] 

469 self._best_step: EvolutionStep | None = None 

470 

471 def evolve( 

472 self, 

473 prompt: str, 

474 test_cases: list[str], 

475 max_iterations: int = 5, 

476 target_score: float = 1.0, 

477 ) -> EvolutionStep: 

478 """执行自进化循环。 

479 

480 Args: 

481 prompt: 自然语言描述的功能需求 

482 test_cases: 测试用例(Python assert 语句) 

483 max_iterations: 最大迭代次数 

484 target_score: 目标分数(1.0 = 全部通过) 

485 

486 Returns: 

487 最优的 EvolutionStep 

488 """ 

489 self._evolution_history = [] 

490 self._best_step = None 

491 best_score = 0.0 

492 

493 for i in range(max_iterations): 

494 step = EvolutionStep( 

495 iteration=i, 

496 prompt=prompt, 

497 ) 

498 

499 # Step 1: Generate code (in real use, Agent generates via LLM) 

500 # Here we provide a scaffold; the Agent fills in the implementation 

501 step.generated_code = self._generate_code_scaffold(prompt, i) 

502 

503 # Step 2: Run generated code in sandbox 

504 step.result = self._sandbox.run(step.generated_code) 

505 

506 if step.result.status != SandboxStatus.COMPLETED: 

507 step.error = f"Code execution failed: {step.result.stderr[:200]}" 

508 self._evolution_history.append(step) 

509 continue 

510 

511 # Step 3: Run tests 

512 test_code = self._build_test_code(step.generated_code, test_cases) 

513 step.test_code = test_code 

514 step.test_result = self._sandbox.run(test_code) 

515 

516 # Step 4: Score 

517 step.score = self._score(step, test_cases) 

518 step.accepted = step.score > best_score 

519 

520 if step.accepted: 

521 best_score = step.score 

522 self._best_step = step 

523 

524 self._evolution_history.append(step) 

525 

526 # Early exit 

527 if step.score >= target_score: 

528 break 

529 

530 return self._best_step or self._evolution_history[-1] 

531 

532 def _generate_code_scaffold(self, prompt: str, iteration: int) -> str: 

533 """生成代码骨架(实际应由 Agent + LLM 完成)。""" 

534 return f"""# Iteration {iteration} 

535# Prompt: {prompt} 

536 

537def quicksort(arr): 

538 if len(arr) <= 1: 

539 return arr 

540 pivot = arr[len(arr) // 2] 

541 left = [x for x in arr if x < pivot] 

542 middle = [x for x in arr if x == pivot] 

543 right = [x for x in arr if x > pivot] 

544 return quicksort(left) + middle + quicksort(right) 

545 

546# Test the function 

547print(quicksort([3, 6, 8, 10, 1, 2, 1])) 

548""" 

549 

550 def _build_test_code(self, code: str, test_cases: list[str]) -> str: 

551 """构建测试代码。""" 

552 test_code = code + "\n\n# Auto-generated tests\n" 

553 test_code += "test_results = []\n" 

554 for tc in test_cases: 

555 test_code += f"try:\n {tc}\n test_results.append(('PASS', '{tc[:50]}'))\n" 

556 test_code += f"except AssertionError as e:\n test_results.append(('FAIL', '{tc[:50]}: ' + str(e)))\n" 

557 test_code += "\nfor status, msg in test_results:\n print(f'{status}: {msg}')\n" 

558 test_code += f"\nprint(f'\\n{{sum(1 for s,_ in test_results if s==\"PASS\")}}/{len(test_cases)} tests passed')" 

559 return test_code 

560 

561 def _score(self, step: EvolutionStep, test_cases: list[str]) -> float: 

562 """根据测试结果计算分数。""" 

563 if not step.test_result or step.test_result.status != SandboxStatus.COMPLETED: 

564 return 0.0 

565 

566 output = step.test_result.stdout 

567 passed = output.count("PASS:") 

568 total = len(test_cases) 

569 if total == 0: 

570 return 1.0 

571 return passed / total 

572 

573 @property 

574 def history(self) -> list[EvolutionStep]: 

575 return list(self._evolution_history) 

576 

577 

578# ── Quick Start ───────────────────────────── 

579 

580 

581def create_sandbox( 

582 image: str = "python:3.11-slim", 

583 timeout_s: float = 30.0, 

584 network: bool = False, 

585) -> DockerSandbox: 

586 """快速创建安全沙箱。""" 

587 config = SandboxConfig( 

588 image=image, 

589 timeout_s=timeout_s, 

590 network_enabled=network, 

591 ) 

592 return DockerSandbox(config)