# modules/terminal_ops.py - 终端操作模块(修复Python命令检测) import os import sys import asyncio import subprocess import shutil import time import signal import re from pathlib import Path from typing import Dict, Optional, Tuple, TYPE_CHECKING from types import SimpleNamespace try: from config import ( TERMINAL_COMMAND_TIMEOUT, FORBIDDEN_COMMANDS, OUTPUT_FORMATS, MAX_RUN_COMMAND_CHARS, TOOLBOX_TERMINAL_IDLE_SECONDS, HOST_SANDBOX_NETWORK_PERMISSION, ) except ImportError: project_root = Path(__file__).resolve().parents[1] if str(project_root) not in sys.path: sys.path.insert(0, str(project_root)) from config import ( TERMINAL_COMMAND_TIMEOUT, FORBIDDEN_COMMANDS, OUTPUT_FORMATS, MAX_RUN_COMMAND_CHARS, TOOLBOX_TERMINAL_IDLE_SECONDS, HOST_SANDBOX_NETWORK_PERMISSION, ) from modules.toolbox_container import ToolboxContainer from modules.host_sandbox_runner import ( HostSandboxError, NETWORK_PERMISSION_RESTRICTED, build_host_sandbox_plan, build_host_sandbox_readonly_plan, host_sandbox_enabled, ) if TYPE_CHECKING: from modules.user_container_manager import ContainerHandle from modules.terminal_manager import TerminalManager class PythonMixin: """TerminalOperator python 能力 mixin。""" def _detect_python_runtime(self) -> str: """ 自动检测可用的 Python 命令,优先使用预装虚拟环境。 1) 优先查找 AGENT_TOOLBOX_VENV /opt/agent-venv / 当前进程 VIRTUAL_ENV。 2) 找不到预装虚拟环境时,回退到系统可执行文件。 """ preferred = self._detect_preinstalled_python() if preferred: # 记录虚拟环境相关环境变量,供宿主机子进程复用 self._python_env = self._build_python_env(preferred) return preferred return self._detect_system_python() @staticmethod def _derive_pip_from_python(python_path: str) -> str: """ 根据 python 可执行文件推导匹配的 pip,可避免“python 在 venv、pip 却指向系统”。 若同目录下找不到 pip3/pip,则回退为 ` -m pip`。 """ try: bin_dir = Path(python_path).resolve().parent for name in ("pip3", "pip"): cand = bin_dir / name if cand.exists() and os.access(cand, os.X_OK): return str(cand) except Exception: pass return f"{python_path} -m pip" def _detect_preinstalled_python(self) -> Optional[str]: """尝试定位预装虚拟环境的 python 可执行文件。""" # 允许显式指定可执行文件(优先级最高) explicit_python = os.environ.get("AGENT_PYTHON_BIN") if explicit_python: p = Path(explicit_python).expanduser() if p.exists() and os.access(p, os.X_OK) and not self._is_blocked_python_candidate(str(p)): try: return str(p.resolve()) except Exception: return str(p) candidates = [] if sys.platform == "darwin": # 优先使用非 shim 的系统 Python 路径(避免 /usr/bin/python3 xcode shim) mac_python_candidates = [ "/Library/Developer/CommandLineTools/usr/bin/python3", "/opt/homebrew/bin/python3", "/usr/local/bin/python3", ] for raw_py in mac_python_candidates: p = Path(raw_py) if p.exists() and os.access(p, os.X_OK) and not self._is_blocked_python_candidate(str(p)): try: return str(p.resolve()) except Exception: return str(p) # 环境变量优先(Dockerfile 已设置 AGENT_TOOLBOX_VENV=/opt/agent-venv) env_venv = os.environ.get("AGENT_TOOLBOX_VENV") if env_venv: candidates.append(env_venv) # 默认预装路径 candidates.append("/opt/agent-venv") # 当前进程若已激活虚拟环境,也纳入候选 current_venv = os.environ.get("VIRTUAL_ENV") if current_venv: candidates.append(current_venv) # 去重并依次检查 seen = set() for raw in candidates: if not raw or raw in seen: continue seen.add(raw) root = Path(raw).expanduser() bin_dir = root / ("Scripts" if sys.platform == "win32" else "bin") for name in ("python3", "python"): candidate = bin_dir / name if candidate.exists() and os.access(candidate, os.X_OK): return str(candidate.resolve()) return None def _detect_system_python(self) -> str: """在系统 PATH 中探测可用的 Python3 可执行文件。""" commands_to_try = ["python", "py", "python3"] if sys.platform == "win32" else ["python3", "python"] for cmd in commands_to_try: resolved = shutil.which(cmd) if not resolved: continue if self._is_blocked_python_candidate(resolved): continue try: result = subprocess.run( [resolved, "--version"], capture_output=True, text=True, timeout=5 ) if result.returncode == 0: output = result.stdout + result.stderr if "Python 3" in output or "Python 2" not in output: return resolved except Exception: continue fallback = "python" if sys.platform == "win32" else "python3" fallback_resolved = shutil.which(fallback) or fallback if sys.platform == "darwin" and self._is_blocked_python_candidate(fallback_resolved): raise RuntimeError( "未找到可用的 Python3 可执行文件:检测到 /usr/bin/python3 (xcode shim) 在沙箱中不可用。" "请设置 AGENT_PYTHON_BIN 或安装非 shim 的 python3。" ) return fallback def _is_blocked_python_candidate(self, candidate: str) -> bool: """过滤已知在当前沙箱下不可靠的 Python 候选。""" if sys.platform != "darwin": return False try: resolved = str(Path(candidate).expanduser().resolve()) except Exception: resolved = candidate # macOS 的 /usr/bin/python3 常为 xcode shim,会调用 xcodebuild 并触发 /dev/null 拦截 if resolved == "/usr/bin/python3": return True return False def _build_python_env(self, python_path: str) -> Dict[str, str]: """构造与预装虚拟环境匹配的环境变量(仅作用于宿主机子进程)。""" env: Dict[str, str] = {} try: py_path = Path(python_path).resolve() bin_dir = py_path.parent venv_dir = bin_dir.parent env["VIRTUAL_ENV"] = str(venv_dir) current_path = os.environ.get("PATH", "") # 避免重复添加 prefix = str(bin_dir) if current_path.startswith(prefix + os.pathsep) or current_path == prefix: env["PATH"] = current_path else: env["PATH"] = f"{prefix}{os.pathsep}{current_path}" if current_path else prefix except Exception: pass return env