feat: 逐步增加docker 支持

This commit is contained in:
ViperEkura 2026-03-29 12:42:07 +08:00
parent 7da142fccb
commit 9b7468ea4e
3 changed files with 212 additions and 24 deletions

View File

@ -28,6 +28,12 @@ class CodeExecutionConfig:
"""Code execution settings.""" """Code execution settings."""
default_strictness: str = "standard" default_strictness: str = "standard"
extra_allowed_modules: Dict = field(default_factory=dict) extra_allowed_modules: Dict = field(default_factory=dict)
backend: str = "subprocess" # subprocess or docker
docker_image: str = "python:3.12-slim"
docker_network: str = "none"
docker_user: str = "nobody"
docker_memory_limit: Optional[str] = None
docker_cpu_shares: Optional[int] = None
@dataclass @dataclass
@ -88,6 +94,12 @@ def _parse_config(raw: dict) -> AppConfig:
code_execution = CodeExecutionConfig( code_execution = CodeExecutionConfig(
default_strictness=ce_raw.get("default_strictness", "standard"), default_strictness=ce_raw.get("default_strictness", "standard"),
extra_allowed_modules=ce_raw.get("extra_allowed_modules", {}), extra_allowed_modules=ce_raw.get("extra_allowed_modules", {}),
backend=ce_raw.get("backend", "subprocess"),
docker_image=ce_raw.get("docker_image", "python:3.12-slim"),
docker_network=ce_raw.get("docker_network", "none"),
docker_user=ce_raw.get("docker_user", "nobody"),
docker_memory_limit=ce_raw.get("docker_memory_limit"),
docker_cpu_shares=ce_raw.get("docker_cpu_shares"),
) )
return AppConfig( return AppConfig(

View File

@ -8,6 +8,7 @@ from typing import Dict, List, Set
from backend.tools.factory import tool from backend.tools.factory import tool
from backend.config import config from backend.config import config
from backend.tools.docker_executor import DockerExecutor
# Strictness profiles configuration # Strictness profiles configuration
@ -181,7 +182,26 @@ def execute_python(arguments: dict) -> dict:
"error": f"Blocked functions: {', '.join(dangerous_calls)}. These functions are not allowed in '{strictness}' mode." "error": f"Blocked functions: {', '.join(dangerous_calls)}. These functions are not allowed in '{strictness}' mode."
} }
# Execute in isolated subprocess # Choose execution backend
backend = config.code_execution.backend
if backend == "docker":
# Use Docker executor
executor = DockerExecutor(
image=config.code_execution.docker_image,
network=config.code_execution.docker_network,
user=config.code_execution.docker_user,
memory_limit=config.code_execution.docker_memory_limit,
cpu_shares=config.code_execution.docker_cpu_shares,
)
result = executor.execute(
code=code,
timeout=timeout,
strictness=strictness,
)
# Docker executor already returns the same dict structure
return result
else:
# Default subprocess backend
try: try:
result = subprocess.run( result = subprocess.run(
[sys.executable, "-c", _build_safe_code(code, blocked_builtins, allowlist_modules)], [sys.executable, "-c", _build_safe_code(code, blocked_builtins, allowlist_modules)],

View File

@ -0,0 +1,156 @@
"""Docker-based code execution with isolation."""
import subprocess
import tempfile
from typing import Optional, Dict, Any
from pathlib import Path
from backend.config import config
class DockerExecutor:
"""Execute Python code in isolated Docker containers."""
def __init__(
self,
image: str = "python:3.12-slim",
network: str = "none",
user: str = "nobody",
workdir: str = "/workspace",
memory_limit: Optional[str] = None,
cpu_shares: Optional[int] = None,
):
self.image = image
self.network = network
self.user = user
self.workdir = workdir
self.memory_limit = memory_limit
self.cpu_shares = cpu_shares
def execute(
self,
code: str,
timeout: int,
strictness: str,
extra_env: Optional[Dict[str, str]] = None,
mount_src: Optional[str] = None,
mount_dst: Optional[str] = None,
) -> Dict[str, Any]:
"""
Execute Python code in a Docker container.
Args:
code: Python code to execute.
timeout: Maximum execution time in seconds.
strictness: Strictness level (lenient/standard/strict) for logging.
extra_env: Additional environment variables.
mount_src: Host path to mount into container (optional).
mount_dst: Container mount path (defaults to workdir).
Returns:
Dictionary with keys:
success: bool
output: str if success else empty
error: str if not success else empty
container_id: str for debugging
"""
# Create temporary file with code inside a temporary directory
# so we can mount it into container
with tempfile.TemporaryDirectory() as tmpdir:
code_path = Path(tmpdir) / "code.py"
code_path.write_text(code, encoding="utf-8")
# Build docker run command
cmd = [
"docker", "run",
"--rm",
f"--network={self.network}",
f"--user={self.user}",
f"--workdir={self.workdir}",
f"--env=PYTHONIOENCODING=utf-8",
]
# Add memory limit if specified
if self.memory_limit:
cmd.append(f"--memory={self.memory_limit}")
# Add CPU shares if specified
if self.cpu_shares:
cmd.append(f"--cpu-shares={self.cpu_shares}")
# Add timeout via --stop-timeout (seconds before SIGKILL)
# Docker's timeout is different; we'll use subprocess timeout instead.
# We'll rely on subprocess timeout, but also set --stop-timeout as backup.
stop_timeout = timeout + 2 # give 2 seconds grace
cmd.append(f"--stop-timeout={stop_timeout}")
# Mount the temporary directory as /workspace (read-only)
cmd.extend(["-v", f"{tmpdir}:{self.workdir}:ro"])
# Additional mount if provided
if mount_src and mount_dst:
cmd.extend(["-v", f"{mount_src}:{mount_dst}:ro"])
# Add environment variables
env = extra_env or {}
for k, v in env.items():
cmd.extend(["-e", f"{k}={v}"])
# Finally, image and command to run
cmd.append(self.image)
cmd.extend(["python", "-c", code])
# Execute docker run with timeout
try:
result = subprocess.run(
cmd,
capture_output=True,
timeout=timeout,
encoding="utf-8",
errors="ignore",
)
if result.returncode == 0:
return {
"success": True,
"output": result.stdout,
"error": "",
"container_id": "", # not available with --rm
"strictness": strictness,
"timeout": timeout,
}
else:
return {
"success": False,
"output": "",
"error": result.stderr or f"Container exited with code {result.returncode}",
"container_id": "",
"strictness": strictness,
"timeout": timeout,
}
except subprocess.TimeoutExpired:
return {
"success": False,
"output": "",
"error": f"Execution timeout ({timeout}s limit in '{strictness}' mode)",
"container_id": "",
"strictness": strictness,
"timeout": timeout,
}
except Exception as e:
return {
"success": False,
"output": "",
"error": f"Docker execution error: {str(e)}",
"container_id": "",
"strictness": strictness,
"timeout": timeout,
}
# Singleton instance
_default_executor = DockerExecutor()
def execute_in_docker(code: str, timeout: int, strictness: str, **kwargs) -> Dict[str, Any]:
"""Convenience function using default executor."""
return _default_executor.execute(code, timeout, strictness, **kwargs)