"""Docker 隔离沙箱:Code Interpreter 在一次性容器里执行不可信代码。 隔离要点(安全默认): - network_disabled:禁网(无外联 / 不能回连)。 - user=65534(nobody) + cap_drop=ALL + no-new-privileges:非 root、丢全部能力、禁提权。 - read_only 根文件系统 + 仅 /tmp 给小块 tmpfs:不能写镜像、不留痕。 - mem_limit + memswap=mem(禁 swap)+ nano_cpus + pids_limit:限内存/CPU/进程数。 - python -I(隔离模式,忽略环境与用户 site)+ wait 超时即 kill + 一次性 remove。 Docker 不可用时优雅降级(available()=False),不阻断服务。 """ from __future__ import annotations import asyncio import logging log = logging.getLogger("mcp_py") class CodeInterpreter: """Docker 隔离沙箱 · Code Interpreter。""" def __init__(self) -> None: self._client = None self._init_err = "未初始化" try: import docker # 延迟导入:未装 docker SDK 时不影响其它工具 self._client = docker.from_env() self._client.ping() self._init_err = "" log.info("[interpreter] Docker 就绪") except Exception as e: # noqa: BLE001 self._init_err = str(e) log.warning("[interpreter] Docker 不可用,代码执行降级: %s", e) def available(self) -> bool: return self._client is not None async def execute( self, code: str, *, image: str = "python:3.11-slim", mem: str = "256m", cpu: float = 0.5, timeout: int = 10, pids: int = 64, ) -> dict: """在一次性隔离容器中执行代码,返回 {ok,stdout,stderr,exit,degraded}。""" if self._client is None: return {"ok": False, "stdout": "", "stderr": f"Docker 不可用:{self._init_err}", "exit": -1, "degraded": True} # Docker SDK 为阻塞式,丢线程池避免卡事件循环。 return await asyncio.to_thread(self._run_blocking, code, image, mem, cpu, timeout, pids) def _run_blocking(self, code: str, image: str, mem: str, cpu: float, timeout: int, pids: int) -> dict: container = None try: container = self._client.containers.run( image, ["python", "-I", "-c", code], detach=True, network_disabled=True, mem_limit=mem, memswap_limit=mem, # = mem → 禁用 swap nano_cpus=int(cpu * 1e9), pids_limit=pids, read_only=True, tmpfs={"/tmp": "size=16m"}, cap_drop=["ALL"], security_opt=["no-new-privileges"], user="65534:65534", # nobody stdin_open=False, tty=False, ) timed_out = False try: res = container.wait(timeout=timeout) exit_code = int(res.get("StatusCode", -1)) except Exception: # noqa: BLE001 容器 wait 超时(requests ReadTimeout) try: container.kill() except Exception: # noqa: BLE001 pass exit_code, timed_out = -1, True stdout = container.logs(stdout=True, stderr=False).decode("utf-8", "replace") stderr = container.logs(stdout=False, stderr=True).decode("utf-8", "replace") if timed_out: stderr = (stderr + f"\n[超时 {timeout}s,已终止]").strip() return {"ok": exit_code == 0 and not timed_out, "stdout": stdout, "stderr": stderr, "exit": exit_code, "degraded": False} except Exception as e: # noqa: BLE001 return {"ok": False, "stdout": "", "stderr": f"执行失败:{e}", "exit": -1, "degraded": False} finally: if container is not None: try: container.remove(force=True) except Exception: # noqa: BLE001 pass