From 8bfaaa565949c60d72ea6f5e605e30a8d983b56d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 16:38:19 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D6=E5=A4=84=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E6=BC=8F=E6=B4=9E=E4=B8=8E=E9=80=BB=E8=BE=91=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - agents/utils.py: docker_exec 捕获 TimeoutExpired,防止调用方崩溃 - ui/server.py: get_figure 端点添加路径穿越防护(resolve + 前缀校验) - ui/server.py: pip install 端点改用 shlex.quote 防 shell 注入,包名正则白名单 - sandbox/loop.py: 成功时 iterations 计数改为 iteration+1(与失败路径一致) - sandbox/healer.py: 超过最大迭代次数返回 is_logic=False,不再误标为逻辑错误 - agents/conversation_mgr.py: 会话标题 .strip() 防止空白字符标题 https://claude.ai/code/session_01Ff2zH5qmZkJ4kCQAKh1TXx --- agents/conversation_mgr.py | 4 ++-- agents/utils.py | 21 ++++++++++++--------- sandbox/healer.py | 8 ++++---- sandbox/loop.py | 4 ++-- ui/server.py | 16 ++++++++++++---- 5 files changed, 32 insertions(+), 21 deletions(-) diff --git a/agents/conversation_mgr.py b/agents/conversation_mgr.py index dad1fb2..ec5f4e6 100644 --- a/agents/conversation_mgr.py +++ b/agents/conversation_mgr.py @@ -121,7 +121,7 @@ def rename_session(session_id: str, title: str) -> bool: s = data.get("sessions", {}).get(session_id) if not s: return False - s["title"] = title[:60] or "新对话" + s["title"] = title[:60].strip() or "新对话" s["updated_at"] = _now() _save_all(data) return True @@ -155,7 +155,7 @@ def append_messages(session_id: str, new_messages: list[dict]) -> dict | None: if s.get("title", "新对话") == "新对话": for m in msgs: if m.get("role") == "user" and isinstance(m.get("content"), str): - s["title"] = m["content"][:28] or "新对话" + s["title"] = m["content"][:28].strip() or "新对话" break # rolling trim if len(msgs) > _MAX_MESSAGES_PER_SESSION: diff --git a/agents/utils.py b/agents/utils.py index 01a3fb3..2f18806 100644 --- a/agents/utils.py +++ b/agents/utils.py @@ -41,15 +41,18 @@ def docker_cp(host_path: str, container: str, container_path: str) -> None: def docker_exec(container: str, cmd: str, timeout: int = 300) -> tuple[int, str, str]: """Execute shell command in container and return (code, stdout, stderr).""" - result = subprocess.run( - ["docker", "exec", container, "sh", "-c", cmd], - capture_output=True, - text=True, - encoding="utf-8", - errors="replace", - timeout=timeout, - ) - return result.returncode, result.stdout, result.stderr + try: + result = subprocess.run( + ["docker", "exec", container, "sh", "-c", cmd], + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=timeout, + ) + return result.returncode, result.stdout, result.stderr + except subprocess.TimeoutExpired: + return -1, "", f"Command timed out after {timeout}s" def vol_host() -> Path: diff --git a/sandbox/healer.py b/sandbox/healer.py index c7ab804..f5c6429 100644 --- a/sandbox/healer.py +++ b/sandbox/healer.py @@ -46,14 +46,14 @@ def heal(script_path: str, stderr: str, iteration: int) -> tuple[str, bool]: """ 修复脚本。 返回 (fixed_code_or_empty, is_logic_err)。 - 如果是逻辑错误或超过最大迭代次数,返回 ("", True)。 + 如果是逻辑错误返回 ("", True);超过最大迭代次数返回 ("", False)。 """ - if iteration >= MAX_ITER: - return "", True - if is_logic_error(stderr): return "", True + if iteration >= MAX_ITER: + return "", False + code = Path(script_path).read_text(encoding="utf-8") tb = extract_traceback(stderr) diff --git a/sandbox/loop.py b/sandbox/loop.py index 0cfe7e8..eabb28c 100644 --- a/sandbox/loop.py +++ b/sandbox/loop.py @@ -50,8 +50,8 @@ def execute_with_healing(script_name: str) -> dict: if exit_code == 0: artifacts = archive_artifacts() - _update_ctx({"status": "success", "iterations": iteration, "last_error": None}) - return {"status": "success", "artifacts": artifacts, "iterations": iteration} + _update_ctx({"status": "success", "iterations": iteration + 1, "last_error": None}) + return {"status": "success", "artifacts": artifacts, "iterations": iteration + 1} fixed_code, is_logic = heal(script_host_path, stderr, iteration) diff --git a/ui/server.py b/ui/server.py index 5dde893..b1c9723 100644 --- a/ui/server.py +++ b/ui/server.py @@ -30,6 +30,7 @@ import json import os import re +import shlex import subprocess import sys import time @@ -633,9 +634,16 @@ async def list_files(): @app.get("/api/figures/{filename}") async def get_figure(filename: str): + safe_roots = [FIGURES_DIR.resolve(), (PAPER_DIR / "figures").resolve()] for p in [FIGURES_DIR / filename, PAPER_DIR / "figures" / filename]: - if p.exists() and p.is_file(): - return FileResponse(str(p)) + try: + resolved = p.resolve() + except Exception: + continue + if not any(str(resolved).startswith(str(root)) for root in safe_roots): + raise HTTPException(400, "Invalid filename") + if resolved.exists() and resolved.is_file(): + return FileResponse(str(resolved)) raise HTTPException(404, f"Figure not found: {filename}") @@ -1351,7 +1359,7 @@ async def pip_install_endpoint(request: Request): """Install a Python package inside the sandbox container (or locally).""" body = await request.json() package: str = body.get("package", "").strip() - if not package or any(c in package for c in [";", "&", "|", "`", "$"]): + if not package or not re.match(r'^[A-Za-z0-9_.\-\[\]]+$', package): raise HTTPException(400, "Invalid package name") loop = asyncio.get_event_loop() @@ -1361,7 +1369,7 @@ def _run_pip(): from sandbox.runner import docker_exec, container_name exit_code, stdout, stderr = docker_exec( container_name(), - f"pip install {package} --quiet", + f"pip install {shlex.quote(package)} --quiet", timeout=120, ) return {"success": exit_code == 0, "output": (stdout + stderr).strip()}