Download core/sandbox.py from smlflg/AgentArena: direct link, hf CLI and curl.
- Browser
- Download file 7.59 kB
-
https://huggingface.co/smlflg/AgentArena/resolve/main/core/sandbox.py
- Command line
-
hf download hf://smlflg/AgentArena/core/sandbox.py
-
curl -L -o sandbox.py https://huggingface.co/smlflg/AgentArena/resolve/main/core/sandbox.py
7.59 kB
| import subprocess | |
| import os | |
| import json | |
| import shlex | |
| import tempfile | |
| import config | |
| class Sandbox: | |
| def __init__(self, container_id: str): | |
| self.container_id = container_id | |
| self.patch_applied: bool = False | |
| self.patch_output: str = "" | |
| def create(cls, task) -> "Sandbox": | |
| """Create container, clone repo, create git checkpoint.""" | |
| cmd = [ | |
| "docker", "run", "-d", | |
| f"--network={config.SANDBOX_NETWORK}", | |
| f"--memory={config.SANDBOX_MEMORY_LIMIT}", | |
| "-w", "/workspace", | |
| ] | |
| # Mount host path for file:// repo URLs so git clone works inside container | |
| repo_url = task.repo_url | |
| if repo_url.startswith("file://"): | |
| host_path = repo_url[7:] # strip file:// | |
| cmd.extend(["-v", f"{host_path}:{host_path}:ro"]) | |
| cmd.extend([config.SANDBOX_IMAGE, "sleep", "infinity"]) | |
| result = subprocess.run(cmd, capture_output=True, text=True) | |
| if result.returncode != 0: | |
| raise RuntimeError(f"Failed to create container: {result.stderr.strip()}") | |
| container_id = result.stdout.strip() | |
| sandbox = cls(container_id) | |
| try: | |
| # Install git in the container (apt-get needs update first on slim images) | |
| sandbox.exec("apt-get update -qq && apt-get install -y -qq git >/dev/null 2>&1 || apk add git 2>/dev/null || true", timeout=120) | |
| # Allow cloning from mounted volumes with different ownership | |
| sandbox.exec("git config --global safe.directory '*'", timeout=5) | |
| # Install pytest (needed by most tasks' success_cmd) | |
| sandbox.exec("pip install -q pytest >/dev/null 2>&1 || true", timeout=60) | |
| # Clone repo | |
| repo_ref = task.repo_ref or "main" | |
| rc, stdout, stderr = sandbox.exec( | |
| f"git clone --depth=1 -b {shlex.quote(repo_ref)} {shlex.quote(repo_url)} .", | |
| timeout=120, | |
| ) | |
| if rc != 0: | |
| raise RuntimeError(f"Failed to clone repo: {stderr.strip()}") | |
| # Create checkpoint tag | |
| rc, _, stderr = sandbox.exec("git tag arena-checkpoint", timeout=30) | |
| if rc != 0: | |
| raise RuntimeError(f"Failed to create git checkpoint: {stderr.strip()}") | |
| except Exception: | |
| sandbox.destroy() | |
| raise | |
| return sandbox | |
| def create_judge_sandbox(cls, task, diff_text: str) -> "Sandbox": | |
| """Create an isolated judge container: fresh clone + apply diff. | |
| This container is separate from the agent container so the agent | |
| cannot manipulate pytest, test files, or git history seen by the judge. | |
| """ | |
| cmd = [ | |
| "docker", "run", "-d", | |
| f"--network={config.SANDBOX_NETWORK}", | |
| f"--memory={config.SANDBOX_MEMORY_LIMIT}", | |
| "-w", "/workspace", | |
| ] | |
| repo_url = task.repo_url | |
| if repo_url.startswith("file://"): | |
| host_path = repo_url[7:] | |
| cmd.extend(["-v", f"{host_path}:{host_path}:ro"]) | |
| cmd.extend([config.SANDBOX_IMAGE, "sleep", "infinity"]) | |
| result = subprocess.run(cmd, capture_output=True, text=True) | |
| if result.returncode != 0: | |
| raise RuntimeError(f"Failed to create judge container: {result.stderr.strip()}") | |
| container_id = result.stdout.strip() | |
| sandbox = cls(container_id) | |
| try: | |
| sandbox.exec("apt-get update -qq && apt-get install -y -qq git patch >/dev/null 2>&1 || apk add git patch 2>/dev/null || true", timeout=120) | |
| sandbox.exec("git config --global safe.directory '*'", timeout=5) | |
| sandbox.exec("pip install -q pytest >/dev/null 2>&1 || true", timeout=60) | |
| repo_ref = task.repo_ref or "main" | |
| rc, stdout, stderr = sandbox.exec( | |
| f"git clone --depth=1 -b {shlex.quote(repo_ref)} {shlex.quote(repo_url)} .", | |
| timeout=120, | |
| ) | |
| if rc != 0: | |
| raise RuntimeError(f"Judge container: failed to clone repo: {stderr.strip()}") | |
| # Apply the agent's diff so the judge sees exactly the agent's changes | |
| # in a fresh environment the agent has never touched. | |
| if diff_text and diff_text.strip(): | |
| with tempfile.NamedTemporaryFile( | |
| mode="w", suffix=".patch", delete=False | |
| ) as tmp: | |
| tmp.write(diff_text) | |
| tmp_path = tmp.name | |
| try: | |
| result2 = subprocess.run( | |
| ["docker", "cp", tmp_path, f"{container_id}:/tmp/agent.patch"], | |
| capture_output=True, text=True, timeout=10, | |
| ) | |
| if result2.returncode != 0: | |
| raise RuntimeError(f"Judge container: failed to copy patch: {result2.stderr.strip()}") | |
| rc2, out2, err2 = sandbox.exec( | |
| "git apply --whitespace=fix /tmp/agent.patch 2>&1 || patch -p1 < /tmp/agent.patch 2>&1", | |
| timeout=30, | |
| ) | |
| sandbox.patch_applied = rc2 == 0 | |
| sandbox.patch_output = (out2 + err2).strip()[:500] if rc2 != 0 else "" | |
| finally: | |
| os.unlink(tmp_path) | |
| except Exception: | |
| sandbox.destroy() | |
| raise | |
| return sandbox | |
| def exec(self, cmd: str, timeout: int = 300) -> tuple[int, str, str]: | |
| """Execute command in container. Returns (returncode, stdout, stderr).""" | |
| try: | |
| result = subprocess.run( | |
| ["docker", "exec", self.container_id, "sh", "-c", cmd], | |
| capture_output=True, | |
| text=True, | |
| timeout=timeout, | |
| ) | |
| return result.returncode, result.stdout, result.stderr | |
| except subprocess.TimeoutExpired: | |
| return 124, "", f"Command timed out after {timeout}s" | |
| except Exception as e: | |
| return -1, "", str(e) | |
| def get_diff(self) -> str: | |
| """Get git diff from checkpoint to current state (committed + uncommitted).""" | |
| # Committed changes | |
| rc, committed, _ = self.exec("git diff arena-checkpoint..HEAD", timeout=30) | |
| # Working tree (uncommitted) changes | |
| rc2, uncommitted, _ = self.exec("git diff arena-checkpoint", timeout=30) | |
| # Return whichever has more content (uncommitted includes committed + staged) | |
| if len(uncommitted) >= len(committed): | |
| return uncommitted | |
| return committed | |
| def reset(self): | |
| """Reset to checkpoint.""" | |
| self.exec("git checkout arena-checkpoint -- . && git clean -fd", timeout=60) | |
| def destroy(self): | |
| """Remove container.""" | |
| try: | |
| subprocess.run( | |
| ["docker", "rm", "-f", self.container_id], | |
| capture_output=True, | |
| text=True, | |
| timeout=30, | |
| ) | |
| except Exception: | |
| pass # Best-effort cleanup | |
| def copy_file_out(self, container_path: str, local_path: str): | |
| """Copy file from container to host.""" | |
| result = subprocess.run( | |
| ["docker", "cp", f"{self.container_id}:{container_path}", local_path], | |
| capture_output=True, | |
| text=True, | |
| timeout=30, | |
| ) | |
| if result.returncode != 0: | |
| raise RuntimeError(f"docker cp failed: {result.stderr.strip()}") | |