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:
parent
5969d04c99
commit
9618148ed3
@ -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 路径授权语义
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
]
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -123,7 +123,7 @@
|
||||
|
||||
- 命令直接在宿主机执行
|
||||
- 仅建议临时用于必须操作
|
||||
- 支持 TTL 自动回退(会话级)到 `sandbox`
|
||||
- 切换后一直生效,无自动回退机制(2026-07 已移除原 TTL 自动回退)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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: '',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user