From 9b7468ea4e93117dd43905bfa773e27dfc4b33f8 Mon Sep 17 00:00:00 2001 From: ViperEkura <3081035982@qq.com> Date: Sun, 29 Mar 2026 12:42:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=80=90=E6=AD=A5=E5=A2=9E=E5=8A=A0doc?= =?UTF-8?q?ker=20=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/config.py | 12 +++ backend/tools/builtin/code.py | 68 +++++++++----- backend/tools/docker_executor.py | 156 +++++++++++++++++++++++++++++++ 3 files changed, 212 insertions(+), 24 deletions(-) create mode 100644 backend/tools/docker_executor.py diff --git a/backend/config.py b/backend/config.py index 92ab78f..edccacb 100644 --- a/backend/config.py +++ b/backend/config.py @@ -28,6 +28,12 @@ class CodeExecutionConfig: """Code execution settings.""" default_strictness: str = "standard" 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 @@ -88,6 +94,12 @@ def _parse_config(raw: dict) -> AppConfig: code_execution = CodeExecutionConfig( default_strictness=ce_raw.get("default_strictness", "standard"), 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( diff --git a/backend/tools/builtin/code.py b/backend/tools/builtin/code.py index fcf52d3..f1be5e3 100644 --- a/backend/tools/builtin/code.py +++ b/backend/tools/builtin/code.py @@ -8,6 +8,7 @@ from typing import Dict, List, Set from backend.tools.factory import tool from backend.config import config +from backend.tools.docker_executor import DockerExecutor # Strictness profiles configuration @@ -181,33 +182,52 @@ def execute_python(arguments: dict) -> dict: "error": f"Blocked functions: {', '.join(dangerous_calls)}. These functions are not allowed in '{strictness}' mode." } - # Execute in isolated subprocess - try: - result = subprocess.run( - [sys.executable, "-c", _build_safe_code(code, blocked_builtins, allowlist_modules)], - capture_output=True, - timeout=timeout, - cwd=tempfile.gettempdir(), - encoding="utf-8", - env={ # Clear environment variables - "PYTHONIOENCODING": "utf-8", - } + # 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: + result = subprocess.run( + [sys.executable, "-c", _build_safe_code(code, blocked_builtins, allowlist_modules)], + capture_output=True, + timeout=timeout, + cwd=tempfile.gettempdir(), + encoding="utf-8", + env={ # Clear environment variables + "PYTHONIOENCODING": "utf-8", + } + ) - if result.returncode == 0: - return { - "success": True, - "output": result.stdout, - "strictness": strictness, - "timeout": timeout - } - else: - return {"success": False, "error": result.stderr or "Execution failed"} + if result.returncode == 0: + return { + "success": True, + "output": result.stdout, + "strictness": strictness, + "timeout": timeout + } + else: + return {"success": False, "error": result.stderr or "Execution failed"} - except subprocess.TimeoutExpired: - return {"success": False, "error": f"Execution timeout ({timeout}s limit in '{strictness}' mode)"} - except Exception as e: - return {"success": False, "error": f"Execution error: {str(e)}"} + except subprocess.TimeoutExpired: + return {"success": False, "error": f"Execution timeout ({timeout}s limit in '{strictness}' mode)"} + except Exception as e: + return {"success": False, "error": f"Execution error: {str(e)}"} def _build_safe_code(code: str, blocked_builtins: Set[str], diff --git a/backend/tools/docker_executor.py b/backend/tools/docker_executor.py new file mode 100644 index 0000000..d9cfe5a --- /dev/null +++ b/backend/tools/docker_executor.py @@ -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) \ No newline at end of file