From 63c7868a48e8d039d9bd5829a740f2bb48eedce4 Mon Sep 17 00:00:00 2001 From: Uday Date: Thu, 16 Jul 2026 22:46:56 +0530 Subject: [PATCH] feat: implement secure Docker sandbox for code execution and fix upstream LLM API parameter routing --- requirements.txt | 3 +- seimei/agents/code_act.py | 117 +++++++++++++++++++++++++++++++------- seimei/llm.py | 2 +- 3 files changed, 98 insertions(+), 24 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7ae79b5..437384d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ huggingface_hub duckduckgo_search requests ddgs -PyMuPDF \ No newline at end of file +PyMuPDF +docker \ No newline at end of file diff --git a/seimei/agents/code_act.py b/seimei/agents/code_act.py index 86ee233..7a259c1 100644 --- a/seimei/agents/code_act.py +++ b/seimei/agents/code_act.py @@ -1,8 +1,11 @@ from __future__ import annotations import asyncio +import os import re import subprocess +import uuid +import docker from pathlib import Path from typing import Any, Dict, List, NamedTuple, Optional, Sequence, Tuple @@ -215,30 +218,100 @@ def _run_command( python_heredoc: Optional[_PythonHeredoc], cwd: Optional[str], ) -> subprocess.CompletedProcess[str]: - if python_heredoc: - args: List[str] = [python_heredoc.executable] - if python_heredoc.has_dash: - args.append("-") - script_input = python_heredoc.script - if script_input and not script_input.endswith("\n"): - script_input += "\n" - return subprocess.run( - args, - input=script_input, - capture_output=True, - text=True, - timeout=timeout, - cwd=cwd, + try: + client = docker.from_env() + except Exception as e: + return subprocess.CompletedProcess( + args=code, + returncode=1, + stdout="", + stderr=f"Docker is not available or not running: {e}" ) - return subprocess.run( - code, - shell=True, - capture_output=True, - text=True, - timeout=timeout, - cwd=cwd, - ) + host_workspace = os.path.abspath(cwd) if cwd and os.path.exists(cwd) else None + volumes = {} + working_dir = None + + if host_workspace: + volumes[host_workspace] = {'bind': '/workspace', 'mode': 'rw'} + working_dir = '/workspace' + + temp_script_name = None + if python_heredoc and host_workspace: + temp_script_name = f".tmp_agent_{uuid.uuid4().hex}.py" + host_script_path = os.path.join(host_workspace, temp_script_name) + + with open(host_script_path, "w", encoding="utf-8") as f: + f.write(python_heredoc.script) + + container_cmd = [python_heredoc.executable, f"/workspace/{temp_script_name}"] + else: + container_cmd = ["/bin/sh", "-c", code] + + image_name = "python:3.10-slim" + container = None + + try: + try: + container = client.containers.run( + image_name, + command=container_cmd, + volumes=volumes, + working_dir=working_dir, + network_mode="none", + mem_limit="256m", + nano_cpus=1000000000, + detach=True, + ) + except docker.errors.ImageNotFound: + client.images.pull(image_name) + container = client.containers.run( + image_name, + command=container_cmd, + volumes=volumes, + working_dir=working_dir, + network_mode="none", + mem_limit="256m", + nano_cpus=1000000000, + detach=True, + ) + + result = container.wait(timeout=timeout) + returncode = result.get("StatusCode", 0) + + stdout = container.logs(stdout=True, stderr=False).decode("utf-8", errors="replace") + stderr = container.logs(stdout=False, stderr=True).decode("utf-8", errors="replace") + + except Exception as e: + if "ReadTimeout" in type(e).__name__ or "Timeout" in type(e).__name__: + if container: + try: + container.stop(timeout=1) + except Exception: + pass + raise subprocess.TimeoutExpired(cmd=code, timeout=timeout) + + returncode = 1 + stdout = "" + stderr = f"Execution failed: {e}" + if container: + try: + container.stop(timeout=1) + except Exception: + pass + finally: + if container: + try: + container.remove(force=True) + except Exception: + pass + if temp_script_name and host_workspace: + try: + os.remove(os.path.join(host_workspace, temp_script_name)) + except Exception: + pass + + return subprocess.CompletedProcess(args=code, returncode=returncode, stdout=stdout, stderr=stderr) def _normalize_command(command: str) -> str: cmd = (command or "").strip() diff --git a/seimei/llm.py b/seimei/llm.py index b6c14e6..6a7f0ef 100644 --- a/seimei/llm.py +++ b/seimei/llm.py @@ -615,7 +615,7 @@ async def chat( else: payload = { "model": self.model, - "messages": payload_msgs, + "input": payload_msgs, } payload.update(self._filter_payload(extra_params)) if self.using_kyotoai: