feat(permission): 只读权限联动执行环境锁定沙箱
- set_permission_mode 进入 readonly 时强制切 sandbox(存 pre_readonly_execution_mode 供切离恢复) - set_execution_mode 新增 readonly 锁(拦 direct,与 plan 锁并列) - apply_pending_runtime_mode_changes 执行环境应用加 try/except 防御 - POST /api/permission-mode 响应带 execution state - 前端执行环境组 readonly 时禁用+锁定标记,权限切换后同步执行环境
This commit is contained in:
parent
ec294e2545
commit
8d10effd56
@ -345,20 +345,27 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
|
|||||||
if pending_execution:
|
if pending_execution:
|
||||||
current_exec = self.get_execution_mode()
|
current_exec = self.get_execution_mode()
|
||||||
if pending_execution != current_exec:
|
if pending_execution != current_exec:
|
||||||
self.set_execution_mode(pending_execution)
|
applied = False
|
||||||
if hasattr(self, "build_runtime_mode_switch_notice"):
|
try:
|
||||||
notice_text = self.build_runtime_mode_switch_notice("execution_mode", pending_execution)
|
self.set_execution_mode(pending_execution)
|
||||||
elif hasattr(self, "_build_execution_mode_switch_notice"):
|
applied = True
|
||||||
notice_text = self._build_execution_mode_switch_notice(pending_execution)
|
except Exception:
|
||||||
else:
|
# 锁定拒绝(plan / readonly 下禁止 direct):丢弃该 pending,保持沙箱
|
||||||
notice_text = f"执行环境被用户修改为 {pending_execution}"
|
applied = False
|
||||||
notices.append({
|
if applied:
|
||||||
"text": notice_text,
|
if hasattr(self, "build_runtime_mode_switch_notice"):
|
||||||
"source": "execution_change",
|
notice_text = self.build_runtime_mode_switch_notice("execution_mode", pending_execution)
|
||||||
"kind": "execution_mode",
|
elif hasattr(self, "_build_execution_mode_switch_notice"):
|
||||||
"mode": pending_execution,
|
notice_text = self._build_execution_mode_switch_notice(pending_execution)
|
||||||
})
|
else:
|
||||||
updates["execution_mode"] = pending_execution
|
notice_text = f"执行环境被用户修改为 {pending_execution}"
|
||||||
|
notices.append({
|
||||||
|
"text": notice_text,
|
||||||
|
"source": "execution_change",
|
||||||
|
"kind": "execution_mode",
|
||||||
|
"mode": pending_execution,
|
||||||
|
})
|
||||||
|
updates["execution_mode"] = pending_execution
|
||||||
updates["pending_execution_mode"] = None
|
updates["pending_execution_mode"] = None
|
||||||
self.pending_execution_mode = None
|
self.pending_execution_mode = None
|
||||||
|
|
||||||
@ -424,6 +431,15 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
|
|||||||
raise ValueError("计划模式下执行环境锁定为沙箱,请先切换运行模式")
|
raise ValueError("计划模式下执行环境锁定为沙箱,请先切换运行模式")
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
# 只读模式下执行环境同样锁死为沙箱(与 plan 锁并列):
|
||||||
|
# 切到 readonly 权限时由 set_permission_mode 联动强制沙箱,
|
||||||
|
# 这里拦直接调用(API/队列)防止只读期间切回 direct 绕过 OS 沙箱硬限制。
|
||||||
|
if normalized == "direct" and getattr(self, "get_permission_mode", None):
|
||||||
|
try:
|
||||||
|
if self.get_permission_mode() == "readonly":
|
||||||
|
raise ValueError("只读模式下执行环境锁定为沙箱,请先切换权限模式")
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
self.host_execution_mode = normalized
|
self.host_execution_mode = normalized
|
||||||
self._apply_execution_mode_to_runtime()
|
self._apply_execution_mode_to_runtime()
|
||||||
return self.get_execution_mode_state()
|
return self.get_execution_mode_state()
|
||||||
|
|||||||
@ -358,8 +358,14 @@ class MainTerminalToolsPolicyMixin:
|
|||||||
raise ValueError("计划模式下权限模式锁定为只读,请先切换运行模式")
|
raise ValueError("计划模式下权限模式锁定为只读,请先切换运行模式")
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
previous = self.get_permission_mode()
|
||||||
|
entering_readonly = normalized == "readonly" and previous != "readonly"
|
||||||
|
leaving_readonly = previous == "readonly" and normalized != "readonly"
|
||||||
self.current_permission_mode = normalized
|
self.current_permission_mode = normalized
|
||||||
if not persist:
|
if not persist:
|
||||||
|
# 只读联动不依赖持久化:运行中 pending 切换路径(apply_pending_runtime_mode_changes)
|
||||||
|
# 也要内存级强制沙箱,只是不落 metadata(无记录则切离时保持沙箱,安全默认)。
|
||||||
|
self._apply_readonly_execution_mode_link(entering_readonly, leaving_readonly, persist=False)
|
||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
conv_id = conversation_id or getattr(getattr(self, "context_manager", None), "current_conversation_id", None)
|
conv_id = conversation_id or getattr(getattr(self, "context_manager", None), "current_conversation_id", None)
|
||||||
@ -372,8 +378,57 @@ class MainTerminalToolsPolicyMixin:
|
|||||||
self.context_manager.conversation_metadata["permission_mode"] = normalized
|
self.context_manager.conversation_metadata["permission_mode"] = normalized
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
self._apply_readonly_execution_mode_link(entering_readonly, leaving_readonly, persist=True)
|
||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
|
def _apply_readonly_execution_mode_link(self, entering: bool, leaving: bool, *, persist: bool) -> None:
|
||||||
|
"""只读权限 ⇄ 执行环境联动:进入 readonly 强制切沙箱,切离恢复进入前执行环境。
|
||||||
|
|
||||||
|
与 plan ⇄ readonly+sandbox 双锁逻辑对称:只读权限在宿主机依赖 OS 沙箱硬限制,
|
||||||
|
direct(完全访问)下无沙箱,只读形同虚设,必须一并锁回沙箱。
|
||||||
|
进入 readonly 时若执行环境为 direct,先存 pre_readonly_execution_mode 供切离时恢复;
|
||||||
|
切离 readonly 时仅当有明确进入前记录才恢复(无记录保持 sandbox,安全默认)。
|
||||||
|
"""
|
||||||
|
if entering:
|
||||||
|
try:
|
||||||
|
if hasattr(self, "get_execution_mode") and self.get_execution_mode() == "direct":
|
||||||
|
if persist and hasattr(self, "_persist_runtime_mode_metadata"):
|
||||||
|
try:
|
||||||
|
self._persist_runtime_mode_metadata({"pre_readonly_execution_mode": "direct"})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if hasattr(self, "set_execution_mode"):
|
||||||
|
self.set_execution_mode("sandbox")
|
||||||
|
if persist and hasattr(self, "_persist_runtime_mode_metadata"):
|
||||||
|
try:
|
||||||
|
self._persist_runtime_mode_metadata({"execution_mode": "sandbox"})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
if leaving:
|
||||||
|
try:
|
||||||
|
meta = getattr(getattr(self, "context_manager", None), "conversation_metadata", None) or {}
|
||||||
|
pre_exec = str(meta.get("pre_readonly_execution_mode") or "").strip().lower()
|
||||||
|
except Exception:
|
||||||
|
pre_exec = ""
|
||||||
|
if pre_exec == "direct" and hasattr(self, "set_execution_mode"):
|
||||||
|
try:
|
||||||
|
self.set_execution_mode("direct")
|
||||||
|
if persist and hasattr(self, "_persist_runtime_mode_metadata"):
|
||||||
|
try:
|
||||||
|
self._persist_runtime_mode_metadata({"execution_mode": "direct", "pre_readonly_execution_mode": None})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif persist and hasattr(self, "_persist_runtime_mode_metadata"):
|
||||||
|
try:
|
||||||
|
self._persist_runtime_mode_metadata({"pre_readonly_execution_mode": None})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def set_tool_category_enabled(self, category: str, enabled: bool) -> None:
|
def set_tool_category_enabled(self, category: str, enabled: bool) -> None:
|
||||||
"""设置工具类别的启用状态 / Toggle tool category enablement."""
|
"""设置工具类别的启用状态 / Toggle tool category enablement."""
|
||||||
categories = self.tool_categories_map
|
categories = self.tool_categories_map
|
||||||
|
|||||||
@ -195,6 +195,7 @@ def update_permission_mode(terminal: WebTerminal, workspace: UserWorkspace, user
|
|||||||
"pending_mode": target_mode,
|
"pending_mode": target_mode,
|
||||||
"options": PERMISSION_MODE_OPTIONS,
|
"options": PERMISSION_MODE_OPTIONS,
|
||||||
"conversation_id": getattr(terminal.context_manager, "current_conversation_id", None),
|
"conversation_id": getattr(terminal.context_manager, "current_conversation_id", None),
|
||||||
|
"state": (terminal.get_execution_mode_state() if hasattr(terminal, "get_execution_mode_state") else None),
|
||||||
"message": "权限模式将在当前工具执行完成后生效",
|
"message": "权限模式将在当前工具执行完成后生效",
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -226,6 +227,7 @@ def update_permission_mode(terminal: WebTerminal, workspace: UserWorkspace, user
|
|||||||
"pending_mode": None,
|
"pending_mode": None,
|
||||||
"options": PERMISSION_MODE_OPTIONS,
|
"options": PERMISSION_MODE_OPTIONS,
|
||||||
"conversation_id": getattr(terminal.context_manager, "current_conversation_id", None),
|
"conversation_id": getattr(terminal.context_manager, "current_conversation_id", None),
|
||||||
|
"state": (terminal.get_execution_mode_state() if hasattr(terminal, "get_execution_mode_state") else None),
|
||||||
"message": "权限模式已更新并立即生效",
|
"message": "权限模式已更新并立即生效",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -85,6 +85,11 @@ export const permissionMethods = {
|
|||||||
if (typeof payload?.mode === 'string') {
|
if (typeof payload?.mode === 'string') {
|
||||||
this.currentPermissionMode = payload.mode;
|
this.currentPermissionMode = payload.mode;
|
||||||
}
|
}
|
||||||
|
// readonly 联动:后端在切到只读时会强制执行环境切到沙箱,同步前端显示
|
||||||
|
const execState = payload?.state || {};
|
||||||
|
if (typeof execState.mode === 'string') {
|
||||||
|
this.currentExecutionMode = execState.mode;
|
||||||
|
}
|
||||||
this.pendingPermissionMode = '';
|
this.pendingPermissionMode = '';
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
title: '权限已更新',
|
title: '权限已更新',
|
||||||
|
|||||||
@ -494,11 +494,11 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="permission-switcher__btn"
|
class="permission-switcher__btn"
|
||||||
:disabled="!isConnected"
|
:disabled="!isConnected"
|
||||||
:title="permissionLockedByPlan ? '计划模式下权限与执行环境已锁定,网络权限仍可调整' : ''"
|
:title="permissionLockedByPlan ? '计划模式下权限与执行环境已锁定,网络权限仍可调整' : (executionLockedByReadonly ? '只读模式下执行环境已锁定为沙箱' : '')"
|
||||||
@click="$emit('toggle-permission-menu')"
|
@click="$emit('toggle-permission-menu')"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
v-if="permissionLockedByPlan"
|
v-if="permissionLockedByPlan || executionLockedByReadonly"
|
||||||
class="permission-switcher__lock"
|
class="permission-switcher__lock"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -551,10 +551,12 @@
|
|||||||
<div
|
<div
|
||||||
v-if="executionModeEnabled"
|
v-if="executionModeEnabled"
|
||||||
class="permission-switcher__group"
|
class="permission-switcher__group"
|
||||||
:class="{ 'permission-switcher__group--disabled': permissionLockedByPlan }"
|
:class="{ 'permission-switcher__group--disabled': executionModeLocked }"
|
||||||
>
|
>
|
||||||
<div class="permission-switcher__group-title">
|
<div class="permission-switcher__group-title">
|
||||||
执行环境<span v-if="permissionLockedByPlan" class="permission-switcher__group-lock">计划模式锁定</span>
|
执行环境
|
||||||
|
<span v-if="permissionLockedByPlan" class="permission-switcher__group-lock">计划模式锁定</span>
|
||||||
|
<span v-else-if="executionLockedByReadonly" class="permission-switcher__group-lock">只读模式锁定</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-for="option in executionModeOptions"
|
v-for="option in executionModeOptions"
|
||||||
@ -562,7 +564,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="permission-switcher__item"
|
class="permission-switcher__item"
|
||||||
:class="{ active: option.value === currentExecutionMode }"
|
:class="{ active: option.value === currentExecutionMode }"
|
||||||
:disabled="permissionLockedByPlan"
|
:disabled="executionModeLocked"
|
||||||
@click="$emit('change-execution-mode', option.value)"
|
@click="$emit('change-execution-mode', option.value)"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -2960,6 +2962,14 @@ const workModeLabel = computed(() => {
|
|||||||
/** 计划模式下权限被锁定为只读(UI 禁用 + 后端强制) */
|
/** 计划模式下权限被锁定为只读(UI 禁用 + 后端强制) */
|
||||||
const permissionLockedByPlan = computed(() => props.currentWorkMode === 'plan');
|
const permissionLockedByPlan = computed(() => props.currentWorkMode === 'plan');
|
||||||
|
|
||||||
|
/** 只读模式下执行环境锁定为沙箱(UI 禁用 + 后端强制) */
|
||||||
|
const executionLockedByReadonly = computed(() => props.currentPermissionMode === 'readonly');
|
||||||
|
|
||||||
|
/** 执行环境组禁用条件:plan 全锁 或 readonly 锁执行环境(权限组仅受 plan 锁) */
|
||||||
|
const executionModeLocked = computed(
|
||||||
|
() => permissionLockedByPlan.value || executionLockedByReadonly.value
|
||||||
|
);
|
||||||
|
|
||||||
const currentExecutionShortLabel = computed(() => {
|
const currentExecutionShortLabel = computed(() => {
|
||||||
const mode = String(props.currentExecutionMode || '');
|
const mode = String(props.currentExecutionMode || '');
|
||||||
if (mode === 'direct') return '完全访问';
|
if (mode === 'direct') return '完全访问';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user