refactor(execution): 移除执行环境 TTL 自动回退机制

- 后端:移除 host_execution_direct_until、_refresh_execution_mode_by_ttl
  及 HOST_EXECUTION_DIRECT_TTL_SECONDS 配置,切换执行环境后一直生效
- 前端:移除到期定时同步、回退检测、自动回退 toast 及权限自动降级逻辑
- 文档:AGENTS.md 与 host_sandbox_and_permission_model.md 同步更新
This commit is contained in:
JOJO 2026-07-21 17:12:57 +08:00
parent 5969d04c99
commit 9618148ed3
9 changed files with 3 additions and 159 deletions

View File

@ -330,8 +330,8 @@ AI 执行以下流程时,每一步都要向用户说明在做什么:
### 10.2 执行环境 ### 10.2 执行环境
- `sandbox`(默认):使用 OS 沙箱执行macOS: `sandbox-exec`Linux: `bwrap + seccomp`Windows: `WSL2` - `sandbox`(默认):使用 OS 沙箱执行macOS: `sandbox-exec`Linux: `bwrap + seccomp`Windows: `WSL2`
- `direct`:宿主机直接执行(高风险,仅建议短时使用 - `direct`:宿主机直接执行(高风险)
- 宿主机下支持会话级 TTL 自动回退 `direct -> sandbox` - 切换后一直生效无自动回退机制2026-07 已移除原 TTL 自动回退)
### 10.3 路径授权语义 ### 10.3 路径授权语义

View File

@ -62,7 +62,6 @@ def _load_dotenv():
_LEGACY_MAP = { _LEGACY_MAP = {
"terminal.sandbox_mode": "TERMINAL_SANDBOX_MODE", "terminal.sandbox_mode": "TERMINAL_SANDBOX_MODE",
"terminal.execution_mode_default": "HOST_EXECUTION_MODE_DEFAULT", "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.macos_writable_paths": "HOST_SANDBOX_MACOS_WRITABLE_PATHS",
"terminal.max_active_containers": "MAX_ACTIVE_USER_CONTAINERS", "terminal.max_active_containers": "MAX_ACTIVE_USER_CONTAINERS",
"terminal.project_max_storage_mb": "PROJECT_MAX_STORAGE_MB", "terminal.project_max_storage_mb": "PROJECT_MAX_STORAGE_MB",

View File

@ -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")) 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")) 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_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( HOST_SANDBOX_MACOS_WRITABLE_PATHS = _parse_paths(
os.environ.get("HOST_SANDBOX_MACOS_WRITABLE_PATHS", "") os.environ.get("HOST_SANDBOX_MACOS_WRITABLE_PATHS", "")
) )
@ -88,7 +87,6 @@ __all__ = [
"TOOLBOX_TERMINAL_IDLE_SECONDS", "TOOLBOX_TERMINAL_IDLE_SECONDS",
"MAX_ACTIVE_USER_CONTAINERS", "MAX_ACTIVE_USER_CONTAINERS",
"HOST_EXECUTION_MODE_DEFAULT", "HOST_EXECUTION_MODE_DEFAULT",
"HOST_EXECUTION_DIRECT_TTL_SECONDS",
"HOST_SANDBOX_MACOS_WRITABLE_PATHS", "HOST_SANDBOX_MACOS_WRITABLE_PATHS",
"HOST_SANDBOX_NETWORK_PERMISSION", "HOST_SANDBOX_NETWORK_PERMISSION",
] ]

View File

@ -18,7 +18,6 @@ try:
CUSTOM_TOOLS_ENABLED, CUSTOM_TOOLS_ENABLED,
MCP_TOOLS_ENABLED, MCP_TOOLS_ENABLED,
HOST_EXECUTION_MODE_DEFAULT, HOST_EXECUTION_MODE_DEFAULT,
HOST_EXECUTION_DIRECT_TTL_SECONDS,
HOST_SANDBOX_NETWORK_PERMISSION, HOST_SANDBOX_NETWORK_PERMISSION,
) )
except ImportError: except ImportError:
@ -38,7 +37,6 @@ except ImportError:
CUSTOM_TOOLS_ENABLED, CUSTOM_TOOLS_ENABLED,
MCP_TOOLS_ENABLED, MCP_TOOLS_ENABLED,
HOST_EXECUTION_MODE_DEFAULT, HOST_EXECUTION_MODE_DEFAULT,
HOST_EXECUTION_DIRECT_TTL_SECONDS,
HOST_SANDBOX_NETWORK_PERMISSION, HOST_SANDBOX_NETWORK_PERMISSION,
) )
@ -209,7 +207,6 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
self.default_permission_mode: str = "unrestricted" self.default_permission_mode: str = "unrestricted"
self.current_permission_mode: str = "unrestricted" self.current_permission_mode: str = "unrestricted"
self.host_execution_mode: str = "sandbox" self.host_execution_mode: str = "sandbox"
self.host_execution_direct_until: Optional[float] = None
self.host_network_permission: str = "restricted" self.host_network_permission: str = "restricted"
self.pending_permission_mode: Optional[str] = None self.pending_permission_mode: Optional[str] = None
self.pending_execution_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): def _init_host_execution_mode(self):
default_mode = str(HOST_EXECUTION_MODE_DEFAULT or "sandbox").strip().lower() 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_mode = "direct" if default_mode == "direct" else "sandbox"
self.host_execution_direct_until = None
self._apply_execution_mode_to_runtime() self._apply_execution_mode_to_runtime()
def _apply_execution_mode_to_runtime(self): def _apply_execution_mode_to_runtime(self):
@ -380,29 +376,14 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
except Exception: except Exception:
pass 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: def get_execution_mode(self) -> str:
self._refresh_execution_mode_by_ttl()
return "direct" if self.host_execution_mode == "direct" else "sandbox" return "direct" if self.host_execution_mode == "direct" else "sandbox"
def get_execution_mode_state(self) -> Dict: def get_execution_mode_state(self) -> Dict:
mode = self.get_execution_mode() mode = self.get_execution_mode()
ttl_seconds = int(HOST_EXECUTION_DIRECT_TTL_SECONDS)
return { return {
"mode": mode, "mode": mode,
"default_mode": "direct" if str(HOST_EXECUTION_MODE_DEFAULT).lower() == "direct" else "sandbox", "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: def set_execution_mode(self, mode: str) -> Dict:
@ -410,11 +391,6 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
if normalized not in {"sandbox", "direct"}: if normalized not in {"sandbox", "direct"}:
raise ValueError("无效执行环境,仅支持 sandbox / direct") raise ValueError("无效执行环境,仅支持 sandbox / direct")
self.host_execution_mode = normalized 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() self._apply_execution_mode_to_runtime()
return self.get_execution_mode_state() return self.get_execution_mode_state()

View File

@ -778,8 +778,6 @@ class MainTerminalToolsExecutionMixin:
"""处理工具调用(添加参数预检查和改进错误处理)""" """处理工具调用(添加参数预检查和改进错误处理)"""
logger.info("[handle_tool_call] 工具调用开始: tool_name=%s, arguments=%s", tool_name, arguments) logger.info("[handle_tool_call] 工具调用开始: tool_name=%s, arguments=%s", tool_name, arguments)
try: try:
if hasattr(self, "_refresh_execution_mode_by_ttl"):
self._refresh_execution_mode_by_ttl()
if hasattr(self, "_apply_execution_mode_to_runtime"): if hasattr(self, "_apply_execution_mode_to_runtime"):
self._apply_execution_mode_to_runtime() self._apply_execution_mode_to_runtime()
except Exception: except Exception:

View File

@ -123,7 +123,7 @@
- 命令直接在宿主机执行 - 命令直接在宿主机执行
- 仅建议临时用于必须操作 - 仅建议临时用于必须操作
- 支持 TTL 自动回退(会话级)到 `sandbox` - 切换后一直生效无自动回退机制2026-07 已移除原 TTL 自动回退)
--- ---

View File

@ -127,8 +127,6 @@ export const resourceMethods = {
}, },
applyStatusSnapshot(status) { applyStatusSnapshot(status) {
const prevExecutionMode = this.currentExecutionMode;
const prevDirectUntil = this.executionModeDirectUntil;
this.resourceApplyStatusSnapshot(status); this.resourceApplyStatusSnapshot(status);
// 模型/思考模式/推理强度的权威来源规则: // 模型/思考模式/推理强度的权威来源规则:
// - 非空对话:以对话 meta 为准enterConversation bootstrap 恢复), // - 非空对话:以对话 meta 为准enterConversation bootstrap 恢复),
@ -168,54 +166,8 @@ export const resourceMethods = {
if (typeof status.execution_mode.mode === 'string') { if (typeof status.execution_mode.mode === 'string') {
this.currentExecutionMode = status.execution_mode.mode; 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 = this.executionModeEnabled =
typeof status.execution_mode_enabled === 'boolean' ? status.execution_mode_enabled : true; 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') { if (typeof status.network_permission === 'string') {
this.currentNetworkPermission = status.network_permission; this.currentNetworkPermission = status.network_permission;

View File

@ -129,10 +129,6 @@ export const permissionMethods = {
this.currentExecutionMode = state.mode; this.currentExecutionMode = state.mode;
} }
this.pendingExecutionMode = ''; 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({ this.uiPushToast({
title: '执行环境已更新', title: '执行环境已更新',
message: payload?.message || '已立即生效', message: payload?.message || '已立即生效',
@ -234,8 +230,6 @@ export const permissionMethods = {
}, },
async fetchExecutionMode() { async fetchExecutionMode() {
try { try {
const prevExecutionMode = this.currentExecutionMode;
const prevDirectUntil = this.executionModeDirectUntil;
const query = this.currentConversationId const query = this.currentConversationId
? `?conversation_id=${encodeURIComponent(this.currentConversationId)}` ? `?conversation_id=${encodeURIComponent(this.currentConversationId)}`
: ''; : '';
@ -250,79 +244,10 @@ export const permissionMethods = {
this.currentExecutionMode = state.mode; this.currentExecutionMode = state.mode;
} }
this.pendingExecutionMode = typeof payload.pending_mode === 'string' ? payload.pending_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) { } catch (_error) {
// ignore // 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() { async openPathAuthorizationDialog() {
if (!this.executionModeEnabled) return; if (!this.executionModeEnabled) return;
this.pathAuthorizationDialogOpen = true; this.pathAuthorizationDialogOpen = true;

View File

@ -129,10 +129,6 @@ export function dataState() {
executionModeEnabled: false, executionModeEnabled: false,
currentExecutionMode: 'sandbox', currentExecutionMode: 'sandbox',
pendingExecutionMode: '', pendingExecutionMode: '',
executionModeDirectUntil: null,
executionModeAutoFallbackToastId: null,
executionModeTtlSeconds: 600,
executionModeExpirySyncTimer: null,
networkPermissionEnabled: false, networkPermissionEnabled: false,
currentNetworkPermission: 'restricted', currentNetworkPermission: 'restricted',
pendingNetworkPermission: '', pendingNetworkPermission: '',