diff --git a/config/terminal.py b/config/terminal.py index 2fb3b95..04e42a1 100644 --- a/config/terminal.py +++ b/config/terminal.py @@ -40,6 +40,7 @@ TERMINAL_SANDBOX_ENV = { if key.startswith(_env_prefix) } TERMINAL_SANDBOX_REQUIRE = os.environ.get("TERMINAL_SANDBOX_REQUIRE", "0") not in {"0", "false", "False"} +LINUX_SAFETY = os.environ.get("LINUX_SAFETY", "0") not in {"0", "false", "False"} TOOLBOX_TERMINAL_IDLE_SECONDS = int(os.environ.get("TOOLBOX_TERMINAL_IDLE_SECONDS", "900")) MAX_ACTIVE_USER_CONTAINERS = int(os.environ.get("MAX_ACTIVE_USER_CONTAINERS", "8")) @@ -65,6 +66,7 @@ __all__ = [ "TERMINAL_SANDBOX_NAME_PREFIX", "TERMINAL_SANDBOX_ENV", "TERMINAL_SANDBOX_REQUIRE", + "LINUX_SAFETY", "TOOLBOX_TERMINAL_IDLE_SECONDS", "MAX_ACTIVE_USER_CONTAINERS", ] diff --git a/core/main_terminal.py b/core/main_terminal.py index 48bd2e1..e537882 100644 --- a/core/main_terminal.py +++ b/core/main_terminal.py @@ -210,7 +210,15 @@ class MainTerminal: else: self.container_mount_path = TERMINAL_SANDBOX_MOUNT_PATH or "/workspace" + def _is_host_mode(self) -> bool: + """判定当前是否运行在宿主机模式,用于豁免配额等限制。""" + if self.container_session and getattr(self.container_session, "mode", None) != "docker": + return True + return (TERMINAL_SANDBOX_MODE or "").lower() == "host" + def record_model_call(self, is_thinking: bool): + if self._is_host_mode(): + return True, {} tracker = getattr(self, "usage_tracker", None) if not tracker: return True, {} @@ -224,6 +232,8 @@ class MainTerminal: return True, {} def record_search_call(self): + if self._is_host_mode(): + return True, {} tracker = getattr(self, "usage_tracker", None) if not tracker: return True, {} diff --git a/modules/user_container_manager.py b/modules/user_container_manager.py index 29be35f..3b775fe 100644 --- a/modules/user_container_manager.py +++ b/modules/user_container_manager.py @@ -29,6 +29,7 @@ from config import ( TERMINAL_SANDBOX_NETWORK, TERMINAL_SANDBOX_REQUIRE, LOGS_DIR, + LINUX_SAFETY, ) from modules.container_monitor import collect_stats, inspect_state @@ -110,7 +111,13 @@ class UserContainerManager: # ------------------------------------------------------------------ # Public API # ------------------------------------------------------------------ - def ensure_container(self, username: str, workspace_path: str, container_key: Optional[str] = None) -> ContainerHandle: + def ensure_container( + self, + username: str, + workspace_path: str, + container_key: Optional[str] = None, + preferred_mode: Optional[str] = None, + ) -> ContainerHandle: """为指定“容器键”确保一个容器。 - username:业务用户名(用于日志/权限) @@ -122,10 +129,21 @@ class UserContainerManager: workspace = str(Path(workspace_path).expanduser().resolve()) Path(workspace).mkdir(parents=True, exist_ok=True) + mode = (preferred_mode or self.sandbox_mode or "host").lower() + # 安全模式:当 LINUX_SAFETY 开启且要求 host 时,强制回退 docker + if mode == "host" and LINUX_SAFETY: + mode = "docker" + with self._lock: handle = self._containers.get(key) if handle: - if handle.mode == "docker" and not self._is_container_running(handle): + # 模式不同或 docker 挂掉时重建 + if handle.mode != mode: + self._containers.pop(key, None) + if handle.mode == "docker": + self._kill_container(handle.container_name, handle.sandbox_bin) + handle = None + elif handle.mode == "docker" and not self._is_container_running(handle): self._containers.pop(key, None) self._kill_container(handle.container_name, handle.sandbox_bin) handle = None @@ -138,7 +156,7 @@ class UserContainerManager: raise RuntimeError("资源繁忙:容器配额已用尽,请稍候再试。") # Important: create container using the cache key so each workspace gets its own container name. - handle = self._create_handle(key, workspace) + handle = self._create_handle(key, workspace, mode) self._containers[key] = handle return handle @@ -262,8 +280,8 @@ class UserContainerManager: existing = 1 if username in self._containers else 0 return (len(self._containers) - existing) < self.max_containers - def _create_handle(self, username: str, workspace: str) -> ContainerHandle: - if self.sandbox_mode != "docker": + def _create_handle(self, username: str, workspace: str, mode: str) -> ContainerHandle: + if mode != "docker": return self._host_handle(username, workspace) docker_path = shutil.which(self.sandbox_bin or "docker") diff --git a/server/admin.py b/server/admin.py index 78b5db0..8289b36 100644 --- a/server/admin.py +++ b/server/admin.py @@ -210,7 +210,7 @@ def admin_dashboard_snapshot_api(): if uname: handles = state.container_manager.list_containers() if uname not in handles: - state.container_manager.ensure_container(uname, str(state.user_manager.ensure_user_workspace(uname).project_path)) + state.container_manager.ensure_container(uname, str(state.user_manager.ensure_user_workspace(uname).project_path), preferred_mode="docker") except Exception as ensure_exc: logging.getLogger(__name__).warning("ensure_container for admin failed: %s", ensure_exc) diff --git a/server/app_legacy.py b/server/app_legacy.py index 3ba2f78..7a3e3dc 100644 --- a/server/app_legacy.py +++ b/server/app_legacy.py @@ -849,7 +849,7 @@ def get_user_resources(username: Optional[str] = None) -> Tuple[Optional[WebTerm return None, None record = get_current_user_record() workspace = user_manager.ensure_user_workspace(username) - container_handle = container_manager.ensure_container(username, str(workspace.project_path)) + container_handle = container_manager.ensure_container(username, str(workspace.project_path), preferred_mode="docker") usage_tracker = get_or_create_usage_tracker(username, workspace) terminal = user_terminals.get(username) if not terminal: diff --git a/server/auth.py b/server/auth.py index 78734cc..3fc34e6 100644 --- a/server/auth.py +++ b/server/auth.py @@ -11,6 +11,7 @@ from config import ( DATA_DIR, LOGS_DIR, UPLOAD_QUARANTINE_SUBDIR, + LINUX_SAFETY, ) from .auth_helpers import login_required, api_login_required, get_current_user_record, get_current_username @@ -36,6 +37,12 @@ def issue_csrf_token(): return response +@auth_bp.route('/api/host-mode-enabled', methods=['GET']) +def host_mode_enabled(): + enabled = (TERMINAL_SANDBOX_MODE or "").lower() == "host" and not LINUX_SAFETY + return jsonify({"success": True, "enabled": enabled}) + + @auth_bp.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'GET': @@ -92,7 +99,7 @@ def login(): session.permanent = True clear_failures("login", identifier=client_ip) try: - state.container_manager.ensure_container(record.username, str(workspace.project_path)) + state.container_manager.ensure_container(record.username, str(workspace.project_path), preferred_mode="docker") except RuntimeError as exc: session.clear() return jsonify({"success": False, "error": str(exc), "code": "resource_busy"}), 503 @@ -136,7 +143,7 @@ def host_login(): # 预先创建宿主机模式的终端/容器句柄(host 模式不会启动 Docker) try: - state.container_manager.ensure_container("host", str(host_path), container_key="host") + state.container_manager.ensure_container("host", str(host_path), container_key="host", preferred_mode="host") except RuntimeError as exc: session.clear() return jsonify({"success": False, "error": str(exc)}), 503 diff --git a/server/context.py b/server/context.py index 0e8782b..ad5d5a6 100644 --- a/server/context.py +++ b/server/context.py @@ -83,8 +83,8 @@ def get_user_resources(username: Optional[str] = None, workspace_id: Optional[st workspace.workspace_id = "host" term_key = "host" - container_handle = state.container_manager.ensure_container("host", str(project_path), container_key=term_key) - usage_tracker = get_or_create_usage_tracker("host", workspace) + container_handle = state.container_manager.ensure_container("host", str(project_path), container_key=term_key, preferred_mode="host") + usage_tracker = None # 宿主机模式不计配额 terminal = state.user_terminals.get(term_key) if not terminal: run_mode = session.get('run_mode') if has_request_context() else None @@ -138,7 +138,7 @@ def get_user_resources(username: Optional[str] = None, workspace_id: Optional[st except Exception: pass term_key = _make_terminal_key(username, getattr(workspace, "workspace_id", None) if is_api_user else None) - container_handle = state.container_manager.ensure_container(username, str(workspace.project_path), container_key=term_key) + container_handle = state.container_manager.ensure_container(username, str(workspace.project_path), container_key=term_key, preferred_mode="docker") usage_tracker = None if is_api_user else get_or_create_usage_tracker(username, workspace) terminal = state.user_terminals.get(term_key) if not terminal: diff --git a/static/login.html b/static/login.html index 9af662c..9ef434b 100644 --- a/static/login.html +++ b/static/login.html @@ -115,7 +115,7 @@ - +