AgentArena / core /sandbox.py
smlflg's picture
Initial public upload from Projekte/AgentArena
3b0d6b3 verified
Raw History Blame Contribute Delete
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 = ""
@classmethod
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
@classmethod
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()}")