From 9618148ed39884ed00b903c183ba2d5af3519cc6 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Tue, 21 Jul 2026 17:12:57 +0800 Subject: [PATCH] =?UTF-8?q?refactor(execution):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E7=8E=AF=E5=A2=83=20TTL=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=9B=9E=E9=80=80=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端:移除 host_execution_direct_until、_refresh_execution_mode_by_ttl 及 HOST_EXECUTION_DIRECT_TTL_SECONDS 配置,切换执行环境后一直生效 - 前端:移除到期定时同步、回退检测、自动回退 toast 及权限自动降级逻辑 - 文档:AGENTS.md 与 host_sandbox_and_permission_model.md 同步更新 --- AGENTS.md | 4 +- config/__init__.py | 1 - config/terminal.py | 2 - core/main_terminal.py | 24 ------- core/main_terminal_parts/tools_execution.py | 2 - docs/host_sandbox_and_permission_model.md | 2 +- static/src/app/methods/resources.ts | 48 ------------- static/src/app/methods/ui/permission.ts | 75 --------------------- static/src/app/state.ts | 4 -- 9 files changed, 3 insertions(+), 159 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3f91573a..fca03ee8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -330,8 +330,8 @@ AI 执行以下流程时,每一步都要向用户说明在做什么: ### 10.2 执行环境 - `sandbox`(默认):使用 OS 沙箱执行(macOS: `sandbox-exec`,Linux: `bwrap + seccomp`,Windows: `WSL2`) -- `direct`:宿主机直接执行(高风险,仅建议短时使用) -- 宿主机下支持会话级 TTL 自动回退 `direct -> sandbox` +- `direct`:宿主机直接执行(高风险) +- 切换后一直生效,无自动回退机制(2026-07 已移除原 TTL 自动回退) ### 10.3 路径授权语义 diff --git a/config/__init__.py b/config/__init__.py index 98f68e70..81adf547 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -62,7 +62,6 @@ def _load_dotenv(): _LEGACY_MAP = { "terminal.sandbox_mode": "TERMINAL_SANDBOX_MODE", "terminal.execution_mode_default": "HOST_EXECUTION_MODE_DEFAULT", - "terminal.direct_ttl_seconds": "HOST_EXECUTION_DIRECT_TTL_SECONDS", "terminal.macos_writable_paths": "HOST_SANDBOX_MACOS_WRITABLE_PATHS", "terminal.max_active_containers": "MAX_ACTIVE_USER_CONTAINERS", "terminal.project_max_storage_mb": "PROJECT_MAX_STORAGE_MB", diff --git a/config/terminal.py b/config/terminal.py index 7f3e3592..75e38c1d 100644 --- a/config/terminal.py +++ b/config/terminal.py @@ -54,7 +54,6 @@ 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")) HOST_EXECUTION_MODE_DEFAULT = os.environ.get("HOST_EXECUTION_MODE_DEFAULT", "sandbox").strip().lower() -HOST_EXECUTION_DIRECT_TTL_SECONDS = int(os.environ.get("HOST_EXECUTION_DIRECT_TTL_SECONDS", "600")) HOST_SANDBOX_MACOS_WRITABLE_PATHS = _parse_paths( os.environ.get("HOST_SANDBOX_MACOS_WRITABLE_PATHS", "") ) @@ -88,7 +87,6 @@ __all__ = [ "TOOLBOX_TERMINAL_IDLE_SECONDS", "MAX_ACTIVE_USER_CONTAINERS", "HOST_EXECUTION_MODE_DEFAULT", - "HOST_EXECUTION_DIRECT_TTL_SECONDS", "HOST_SANDBOX_MACOS_WRITABLE_PATHS", "HOST_SANDBOX_NETWORK_PERMISSION", ] diff --git a/core/main_terminal.py b/core/main_terminal.py index 68c21414..f7eadd05 100644 --- a/core/main_terminal.py +++ b/core/main_terminal.py @@ -18,7 +18,6 @@ try: CUSTOM_TOOLS_ENABLED, MCP_TOOLS_ENABLED, HOST_EXECUTION_MODE_DEFAULT, - HOST_EXECUTION_DIRECT_TTL_SECONDS, HOST_SANDBOX_NETWORK_PERMISSION, ) except ImportError: @@ -38,7 +37,6 @@ except ImportError: CUSTOM_TOOLS_ENABLED, MCP_TOOLS_ENABLED, HOST_EXECUTION_MODE_DEFAULT, - HOST_EXECUTION_DIRECT_TTL_SECONDS, HOST_SANDBOX_NETWORK_PERMISSION, ) @@ -209,7 +207,6 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi self.default_permission_mode: str = "unrestricted" self.current_permission_mode: str = "unrestricted" self.host_execution_mode: str = "sandbox" - self.host_execution_direct_until: Optional[float] = None self.host_network_permission: str = "restricted" self.pending_permission_mode: Optional[str] = None self.pending_execution_mode: Optional[str] = None @@ -251,7 +248,6 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi def _init_host_execution_mode(self): default_mode = str(HOST_EXECUTION_MODE_DEFAULT or "sandbox").strip().lower() self.host_execution_mode = "direct" if default_mode == "direct" else "sandbox" - self.host_execution_direct_until = None self._apply_execution_mode_to_runtime() def _apply_execution_mode_to_runtime(self): @@ -380,29 +376,14 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi except Exception: pass - def _refresh_execution_mode_by_ttl(self): - if self.host_execution_mode != "direct": - return - until = self.host_execution_direct_until - if not until: - return - if time.time() >= float(until): - self.host_execution_mode = "sandbox" - self.host_execution_direct_until = None - self._apply_execution_mode_to_runtime() - def get_execution_mode(self) -> str: - self._refresh_execution_mode_by_ttl() return "direct" if self.host_execution_mode == "direct" else "sandbox" def get_execution_mode_state(self) -> Dict: mode = self.get_execution_mode() - ttl_seconds = int(HOST_EXECUTION_DIRECT_TTL_SECONDS) return { "mode": mode, "default_mode": "direct" if str(HOST_EXECUTION_MODE_DEFAULT).lower() == "direct" else "sandbox", - "ttl_seconds": ttl_seconds, - "direct_until": self.host_execution_direct_until, } def set_execution_mode(self, mode: str) -> Dict: @@ -410,11 +391,6 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi if normalized not in {"sandbox", "direct"}: raise ValueError("无效执行环境,仅支持 sandbox / direct") self.host_execution_mode = normalized - ttl_seconds = int(HOST_EXECUTION_DIRECT_TTL_SECONDS) - if normalized == "direct": - self.host_execution_direct_until = None if ttl_seconds in (0, -1) else (time.time() + max(1, ttl_seconds)) - else: - self.host_execution_direct_until = None self._apply_execution_mode_to_runtime() return self.get_execution_mode_state() diff --git a/core/main_terminal_parts/tools_execution.py b/core/main_terminal_parts/tools_execution.py index e4e64f77..605191d4 100644 --- a/core/main_terminal_parts/tools_execution.py +++ b/core/main_terminal_parts/tools_execution.py @@ -778,8 +778,6 @@ class MainTerminalToolsExecutionMixin: """处理工具调用(添加参数预检查和改进错误处理)""" logger.info("[handle_tool_call] 工具调用开始: tool_name=%s, arguments=%s", tool_name, arguments) try: - if hasattr(self, "_refresh_execution_mode_by_ttl"): - self._refresh_execution_mode_by_ttl() if hasattr(self, "_apply_execution_mode_to_runtime"): self._apply_execution_mode_to_runtime() except Exception: diff --git a/docs/host_sandbox_and_permission_model.md b/docs/host_sandbox_and_permission_model.md index 22e56219..9cb6df4a 100644 --- a/docs/host_sandbox_and_permission_model.md +++ b/docs/host_sandbox_and_permission_model.md @@ -123,7 +123,7 @@ - 命令直接在宿主机执行 - 仅建议临时用于必须操作 -- 支持 TTL 自动回退(会话级)到 `sandbox` +- 切换后一直生效,无自动回退机制(2026-07 已移除原 TTL 自动回退) --- diff --git a/static/src/app/methods/resources.ts b/static/src/app/methods/resources.ts index 8384fd09..8559c839 100644 --- a/static/src/app/methods/resources.ts +++ b/static/src/app/methods/resources.ts @@ -127,8 +127,6 @@ export const resourceMethods = { }, applyStatusSnapshot(status) { - const prevExecutionMode = this.currentExecutionMode; - const prevDirectUntil = this.executionModeDirectUntil; this.resourceApplyStatusSnapshot(status); // 模型/思考模式/推理强度的权威来源规则: // - 非空对话:以对话 meta 为准(enterConversation bootstrap 恢复), @@ -168,54 +166,8 @@ export const resourceMethods = { if (typeof status.execution_mode.mode === 'string') { this.currentExecutionMode = status.execution_mode.mode; } - if (typeof status.execution_mode.ttl_seconds === 'number') { - this.executionModeTtlSeconds = status.execution_mode.ttl_seconds; - } - this.executionModeDirectUntil = status.execution_mode.direct_until ?? null; this.executionModeEnabled = typeof status.execution_mode_enabled === 'boolean' ? status.execution_mode_enabled : true; - if (typeof this.scheduleExecutionModeExpirySync === 'function') { - this.scheduleExecutionModeExpirySync(this.executionModeDirectUntil); - } - const currentMode = this.currentExecutionMode; - const parseDirectUntilMs = (value: any): number => { - if (!value) return 0; - if (typeof value === 'number') { - return value > 1e12 ? value : value * 1000; - } - if (typeof value === 'string') { - const parsed = Date.parse(value); - return Number.isNaN(parsed) ? 0 : parsed; - } - return 0; - }; - const prevDirectUntilMs = parseDirectUntilMs(prevDirectUntil); - const fallbackLikelyByExpiry = !!prevDirectUntilMs && Date.now() >= prevDirectUntilMs - 1200; - const directExpiredToSandbox = - prevExecutionMode === 'direct' && currentMode === 'sandbox' && fallbackLikelyByExpiry; - if (directExpiredToSandbox) { - if (this.currentPermissionMode === 'unrestricted') { - if (typeof pendingModes.permission_mode === 'string' && pendingModes.permission_mode) { - this.currentPermissionMode = pendingModes.permission_mode; - } else { - this.currentPermissionMode = 'approval'; - } - } - if (typeof this.fetchPermissionMode === 'function') { - Promise.resolve(this.fetchPermissionMode()).catch(() => {}); - } - try { - if (this.executionModeAutoFallbackToastId) { - this.uiDismissToast(this.executionModeAutoFallbackToastId); - } - } catch (_err) {} - this.executionModeAutoFallbackToastId = this.uiPushToast({ - title: '执行环境已自动回退', - message: '完全访问权限已到期,已自动切回受限权限并切换为沙箱执行环境。', - type: 'warning', - duration: null - }); - } } if (typeof status.network_permission === 'string') { this.currentNetworkPermission = status.network_permission; diff --git a/static/src/app/methods/ui/permission.ts b/static/src/app/methods/ui/permission.ts index e04957a3..6e418385 100644 --- a/static/src/app/methods/ui/permission.ts +++ b/static/src/app/methods/ui/permission.ts @@ -129,10 +129,6 @@ export const permissionMethods = { this.currentExecutionMode = state.mode; } this.pendingExecutionMode = ''; - this.executionModeDirectUntil = state.direct_until ?? null; - this.executionModeTtlSeconds = - typeof state.ttl_seconds === 'number' ? state.ttl_seconds : this.executionModeTtlSeconds; - this.scheduleExecutionModeExpirySync(this.executionModeDirectUntil); this.uiPushToast({ title: '执行环境已更新', message: payload?.message || '已立即生效', @@ -234,8 +230,6 @@ export const permissionMethods = { }, async fetchExecutionMode() { try { - const prevExecutionMode = this.currentExecutionMode; - const prevDirectUntil = this.executionModeDirectUntil; const query = this.currentConversationId ? `?conversation_id=${encodeURIComponent(this.currentConversationId)}` : ''; @@ -250,79 +244,10 @@ export const permissionMethods = { this.currentExecutionMode = state.mode; } this.pendingExecutionMode = typeof payload.pending_mode === 'string' ? payload.pending_mode : ''; - this.executionModeDirectUntil = state.direct_until ?? null; - if (typeof state.ttl_seconds === 'number') { - this.executionModeTtlSeconds = state.ttl_seconds; - } - this.scheduleExecutionModeExpirySync(this.executionModeDirectUntil); - const parseDirectUntilMs = (value) => { - if (!value) return 0; - if (typeof value === 'number') { - return value > 1e12 ? value : value * 1000; - } - if (typeof value === 'string') { - const parsed = Date.parse(value); - return Number.isNaN(parsed) ? 0 : parsed; - } - return 0; - }; - const prevDirectUntilMs = parseDirectUntilMs(prevDirectUntil); - const fallbackLikelyByExpiry = !!prevDirectUntilMs && Date.now() >= prevDirectUntilMs - 1200; - const directExpiredToSandbox = - prevExecutionMode === 'direct' && - this.currentExecutionMode === 'sandbox' && - fallbackLikelyByExpiry; - if (directExpiredToSandbox) { - if (this.currentPermissionMode === 'unrestricted') { - this.currentPermissionMode = 'approval'; - } - Promise.resolve(this.fetchPermissionMode()).catch(() => {}); - try { - if (this.executionModeAutoFallbackToastId) { - this.uiDismissToast(this.executionModeAutoFallbackToastId); - } - } catch (_err) {} - this.executionModeAutoFallbackToastId = this.uiPushToast({ - title: '执行环境已自动回退', - message: '完全访问权限已到期,已自动切回受限权限并切换为沙箱执行环境。', - type: 'warning', - duration: null - }); - } } catch (_error) { // ignore } }, - clearExecutionModeExpirySyncTimer() { - if (this.executionModeExpirySyncTimer) { - clearTimeout(this.executionModeExpirySyncTimer); - this.executionModeExpirySyncTimer = null; - } - }, - scheduleExecutionModeExpirySync(directUntil) { - this.clearExecutionModeExpirySyncTimer(); - if (!this.executionModeEnabled || this.currentExecutionMode !== 'direct' || !directUntil) { - return; - } - let untilMs = 0; - if (typeof directUntil === 'number') { - untilMs = directUntil > 1e12 ? directUntil : directUntil * 1000; - } else if (typeof directUntil === 'string') { - const parsed = Date.parse(directUntil); - if (!Number.isNaN(parsed)) { - untilMs = parsed; - } - } - if (!untilMs) { - return; - } - const delayMs = Math.max(250, untilMs - Date.now() + 250); - this.executionModeExpirySyncTimer = window.setTimeout(async () => { - this.executionModeExpirySyncTimer = null; - await this.fetchExecutionMode(); - await this.fetchPermissionMode(); - }, delayMs); - }, async openPathAuthorizationDialog() { if (!this.executionModeEnabled) return; this.pathAuthorizationDialogOpen = true; diff --git a/static/src/app/state.ts b/static/src/app/state.ts index ef772aca..7a20972d 100644 --- a/static/src/app/state.ts +++ b/static/src/app/state.ts @@ -129,10 +129,6 @@ export function dataState() { executionModeEnabled: false, currentExecutionMode: 'sandbox', pendingExecutionMode: '', - executionModeDirectUntil: null, - executionModeAutoFallbackToastId: null, - executionModeTtlSeconds: 600, - executionModeExpirySyncTimer: null, networkPermissionEnabled: false, currentNetworkPermission: 'restricted', pendingNetworkPermission: '',