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 执行环境
- `sandbox`(默认):使用 OS 沙箱执行macOS: `sandbox-exec`Linux: `bwrap + seccomp`Windows: `WSL2`
- `direct`:宿主机直接执行(高风险,仅建议短时使用
- 宿主机下支持会话级 TTL 自动回退 `direct -> sandbox`
- `direct`:宿主机直接执行(高风险)
- 切换后一直生效无自动回退机制2026-07 已移除原 TTL 自动回退)
### 10.3 路径授权语义

View File

@ -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",

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"))
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",
]

View File

@ -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()

View File

@ -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:

View File

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

View File

@ -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;

View File

@ -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;

View File

@ -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: '',