feat: 宿主机模式网络沙箱
- macOS sandbox-exec profile 支持 network_permission 参数 (restricted/full/none) - backend: 透传网络权限至 run_command / terminal_session / sub_agent / background_command - 后台 run_command 接入沙箱执行 - 自动审核模式兼容网络权限报错 markers - 运行时切换网络权限通过 pending 机制 + user 消息通知 - 提示词注入网络状态 (仅沙箱模式) - 前端权限菜单新增网络权限组 (受限/完全开放) - direct 模式下网络权限组变灰禁用 - settings.json 默认 HOST_SANDBOX_NETWORK_PERMISSION=restricted
This commit is contained in:
parent
f75e2f07a3
commit
c2d460a706
@ -58,6 +58,9 @@ HOST_EXECUTION_DIRECT_TTL_SECONDS = int(os.environ.get("HOST_EXECUTION_DIRECT_TT
|
||||
HOST_SANDBOX_MACOS_WRITABLE_PATHS = _parse_paths(
|
||||
os.environ.get("HOST_SANDBOX_MACOS_WRITABLE_PATHS", "")
|
||||
)
|
||||
HOST_SANDBOX_NETWORK_PERMISSION = os.environ.get(
|
||||
"HOST_SANDBOX_NETWORK_PERMISSION", "restricted"
|
||||
).strip().lower()
|
||||
|
||||
__all__ = [
|
||||
"MAX_TERMINALS",
|
||||
@ -87,4 +90,5 @@ __all__ = [
|
||||
"HOST_EXECUTION_MODE_DEFAULT",
|
||||
"HOST_EXECUTION_DIRECT_TTL_SECONDS",
|
||||
"HOST_SANDBOX_MACOS_WRITABLE_PATHS",
|
||||
"HOST_SANDBOX_NETWORK_PERMISSION",
|
||||
]
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
# core/main_terminal.py - 主终端(集成对话持久化)
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING
|
||||
@ -18,6 +19,7 @@ try:
|
||||
MCP_TOOLS_ENABLED,
|
||||
HOST_EXECUTION_MODE_DEFAULT,
|
||||
HOST_EXECUTION_DIRECT_TTL_SECONDS,
|
||||
HOST_SANDBOX_NETWORK_PERMISSION,
|
||||
)
|
||||
except ImportError:
|
||||
import sys
|
||||
@ -37,6 +39,7 @@ except ImportError:
|
||||
MCP_TOOLS_ENABLED,
|
||||
HOST_EXECUTION_MODE_DEFAULT,
|
||||
HOST_EXECUTION_DIRECT_TTL_SECONDS,
|
||||
HOST_SANDBOX_NETWORK_PERMISSION,
|
||||
)
|
||||
|
||||
from modules.file_manager import FileManager
|
||||
@ -207,12 +210,15 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
|
||||
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
|
||||
self.pending_network_permission: Optional[str] = None
|
||||
# 当前生效的启用 skills(用于强约束判定)
|
||||
self.enabled_skill_ids: Set[str] = set()
|
||||
self.apply_personalization_preferences()
|
||||
self._init_host_execution_mode()
|
||||
self._init_host_network_permission()
|
||||
|
||||
# 新增:自动开始新对话
|
||||
self._ensure_conversation()
|
||||
@ -257,10 +263,29 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
|
||||
if getattr(self, "sub_agent_manager", None):
|
||||
self.sub_agent_manager.set_host_execution_mode(mode)
|
||||
|
||||
def _init_host_network_permission(self):
|
||||
default_permission = str(HOST_SANDBOX_NETWORK_PERMISSION or "restricted").strip().lower()
|
||||
if default_permission not in {"restricted", "full", "none"}:
|
||||
default_permission = "restricted"
|
||||
self.host_network_permission = default_permission
|
||||
os.environ["HOST_SANDBOX_NETWORK_PERMISSION"] = default_permission
|
||||
|
||||
def get_network_permission(self) -> str:
|
||||
return getattr(self, "host_network_permission", "restricted")
|
||||
|
||||
def set_network_permission(self, mode: str) -> str:
|
||||
normalized = str(mode or "").strip().lower()
|
||||
if normalized not in {"restricted", "full", "none"}:
|
||||
raise ValueError("无效网络权限,仅支持 restricted / full / none")
|
||||
self.host_network_permission = normalized
|
||||
os.environ["HOST_SANDBOX_NETWORK_PERMISSION"] = normalized
|
||||
return normalized
|
||||
|
||||
def get_pending_runtime_modes(self) -> Dict[str, Optional[str]]:
|
||||
return {
|
||||
"permission_mode": self.pending_permission_mode,
|
||||
"execution_mode": self.pending_execution_mode,
|
||||
"network_permission": getattr(self, "pending_network_permission", None),
|
||||
}
|
||||
|
||||
def queue_permission_mode_change(self, mode: str) -> str:
|
||||
@ -283,6 +308,16 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
|
||||
})
|
||||
return normalized
|
||||
|
||||
def queue_network_permission_change(self, mode: str) -> str:
|
||||
normalized = str(mode or "").strip().lower()
|
||||
if normalized not in {"restricted", "full", "none"}:
|
||||
raise ValueError("无效网络权限,仅支持 restricted / full / none")
|
||||
self.pending_network_permission = normalized
|
||||
self._persist_runtime_mode_metadata({
|
||||
"pending_network_permission": normalized,
|
||||
})
|
||||
return normalized
|
||||
|
||||
def apply_pending_runtime_mode_changes(self) -> List[Dict[str, str]]:
|
||||
notices: List[Dict[str, str]] = []
|
||||
updates: Dict[str, Any] = {}
|
||||
@ -314,6 +349,17 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
|
||||
updates["pending_execution_mode"] = None
|
||||
self.pending_execution_mode = None
|
||||
|
||||
pending_network = getattr(self, "pending_network_permission", None)
|
||||
if pending_network:
|
||||
current_network = self.get_network_permission()
|
||||
if pending_network != current_network:
|
||||
self.set_network_permission(pending_network)
|
||||
label = {"restricted": "受限", "full": "完全开放", "none": "完全禁止"}.get(pending_network, pending_network)
|
||||
notices.append({"text": f"网络权限被用户修改为 {label}", "source": "网络权限变更"})
|
||||
updates["network_permission"] = pending_network
|
||||
updates["pending_network_permission"] = None
|
||||
self.pending_network_permission = None
|
||||
|
||||
if updates:
|
||||
self._persist_runtime_mode_metadata(updates)
|
||||
|
||||
|
||||
@ -183,16 +183,25 @@ class MainTerminalContextMixin:
|
||||
"- 若操作受系统权限限制:先提供 1 个安全替代方案并执行;若仍无法满足目标,明确请求用户切换执行环境为“完全访问权限”后重试\n"
|
||||
"- 不要通过复杂绕过手段反复尝试规避沙箱限制;若任务本质需要更高权限,应直接说明并等待用户确认"
|
||||
)
|
||||
net = getattr(self, "host_network_permission", "restricted")
|
||||
if net == "restricted":
|
||||
network_rules = "- 网络:受限(仅允许 localhost,外部网络不可达)"
|
||||
elif net == "full":
|
||||
network_rules = "- 网络:完全开放"
|
||||
else:
|
||||
network_rules = ""
|
||||
else:
|
||||
rules = (
|
||||
"- 当前为宿主机直接执行模式(完全访问权限)\n"
|
||||
"- 仅在必须时执行高权限操作,保持最小化命令范围\n"
|
||||
"- 涉及删除/覆盖/系统级变更前,先说明风险再执行"
|
||||
)
|
||||
network_rules = ""
|
||||
return template.format(
|
||||
execution_mode=mode,
|
||||
execution_mode_label=mode_label,
|
||||
rules=rules,
|
||||
network_rules=network_rules,
|
||||
)
|
||||
|
||||
def _get_or_init_frozen_mode_prompt(self, key: str, builder) -> Optional[str]:
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
@ -1448,6 +1449,13 @@ class MainTerminalToolsExecutionMixin:
|
||||
permission_mode == "readonly"
|
||||
or (permission_mode in {"approval", "auto_approval"} and not write_granted_once)
|
||||
)
|
||||
network_granted_once = bool(arguments.get("_approval_network_granted", False))
|
||||
if network_granted_once:
|
||||
network_permission = "full"
|
||||
else:
|
||||
network_permission = getattr(
|
||||
self, "host_network_permission", None
|
||||
) or os.environ.get("HOST_SANDBOX_NETWORK_PERMISSION", "restricted")
|
||||
run_in_background = bool(arguments.get("run_in_background", False))
|
||||
timeout_value = arguments.get("timeout")
|
||||
if run_in_background:
|
||||
@ -1489,6 +1497,7 @@ class MainTerminalToolsExecutionMixin:
|
||||
arguments["command"],
|
||||
timeout=timeout_value,
|
||||
sandbox_write_access=sandbox_write_access,
|
||||
network_permission=network_permission,
|
||||
)
|
||||
|
||||
# 字符数检查
|
||||
|
||||
@ -234,8 +234,15 @@ class WebTerminal(MainTerminal):
|
||||
self.set_execution_mode(execution_mode)
|
||||
except Exception:
|
||||
pass
|
||||
network_permission = str(meta.get("network_permission") or "").strip().lower()
|
||||
if network_permission in {"restricted", "full", "none"}:
|
||||
try:
|
||||
self.set_network_permission(network_permission)
|
||||
except Exception:
|
||||
pass
|
||||
self.pending_permission_mode = str(meta.get("pending_permission_mode") or "").strip().lower() or None
|
||||
self.pending_execution_mode = str(meta.get("pending_execution_mode") or "").strip().lower() or None
|
||||
self.pending_network_permission = str(meta.get("pending_network_permission") or "").strip().lower() or None
|
||||
except Exception:
|
||||
pass
|
||||
# 重置相关状态
|
||||
@ -356,8 +363,10 @@ class WebTerminal(MainTerminal):
|
||||
"model_key": getattr(self, "model_key", None),
|
||||
"permission_mode": self.get_permission_mode() if hasattr(self, "get_permission_mode") else "unrestricted",
|
||||
"execution_mode": self.get_execution_mode_state() if hasattr(self, "get_execution_mode_state") else {"mode": "sandbox"},
|
||||
"network_permission": self.get_network_permission() if hasattr(self, "get_network_permission") else "restricted",
|
||||
"pending_runtime_modes": self.get_pending_runtime_modes() if hasattr(self, "get_pending_runtime_modes") else {},
|
||||
"execution_mode_enabled": bool(self._is_host_mode()) and getattr(self, "user_role", "user") == "admin",
|
||||
"network_permission_enabled": bool(self._is_host_mode()) and getattr(self, "user_role", "user") == "admin",
|
||||
"has_images": getattr(self.context_manager, "has_images", False),
|
||||
"has_videos": getattr(self.context_manager, "has_videos", False),
|
||||
"context": {
|
||||
|
||||
@ -14,6 +14,11 @@ from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from config import MAX_RUN_COMMAND_CHARS
|
||||
from modules.host_sandbox_runner import (
|
||||
HostSandboxError,
|
||||
build_host_sandbox_plan,
|
||||
host_sandbox_enabled,
|
||||
)
|
||||
|
||||
|
||||
TERMINAL_STATUSES = {"completed", "failed", "timeout", "cancelled"}
|
||||
@ -116,6 +121,7 @@ class BackgroundCommandManager:
|
||||
"timeout": timeout_value,
|
||||
"session": session_override or getattr(terminal_ops, "container_session", None),
|
||||
"python_env": getattr(terminal_ops, "_python_env", None) or {},
|
||||
"host_execution_mode": getattr(terminal_ops, "host_execution_mode", "sandbox"),
|
||||
},
|
||||
name=f"bg-run-command-{command_id}",
|
||||
daemon=True,
|
||||
@ -182,6 +188,7 @@ class BackgroundCommandManager:
|
||||
timeout: int,
|
||||
session,
|
||||
python_env: Dict[str, str],
|
||||
host_execution_mode: str = "sandbox",
|
||||
) -> None:
|
||||
start_ts = time.time()
|
||||
process: Optional[subprocess.Popen] = None
|
||||
@ -229,18 +236,49 @@ class BackgroundCommandManager:
|
||||
use_shell = False
|
||||
|
||||
if use_shell:
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
cwd=str(work_path),
|
||||
shell=True,
|
||||
env=env,
|
||||
start_new_session=True,
|
||||
text=True,
|
||||
errors="replace",
|
||||
bufsize=1,
|
||||
)
|
||||
use_host_sandbox = host_execution_mode != "direct"
|
||||
if use_host_sandbox and host_sandbox_enabled():
|
||||
network_permission = os.environ.get("HOST_SANDBOX_NETWORK_PERMISSION", "restricted")
|
||||
plan = build_host_sandbox_plan(command, work_path, env, network_permission=network_permission)
|
||||
cmd_args = plan.command
|
||||
pass_fds = ()
|
||||
seccomp_fd = None
|
||||
if plan.seccomp_bpf_path:
|
||||
seccomp_fd = os.open(plan.seccomp_bpf_path, os.O_RDONLY)
|
||||
cmd_args = [str(seccomp_fd) if token == "__SECCOMP_FD__" else token for token in cmd_args]
|
||||
pass_fds = (seccomp_fd,)
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
cmd_args,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
cwd=plan.cwd,
|
||||
env=plan.env,
|
||||
start_new_session=True,
|
||||
text=True,
|
||||
errors="replace",
|
||||
bufsize=1,
|
||||
pass_fds=pass_fds,
|
||||
)
|
||||
finally:
|
||||
if seccomp_fd is not None:
|
||||
try:
|
||||
os.close(seccomp_fd)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
cwd=str(work_path),
|
||||
shell=True,
|
||||
env=env,
|
||||
start_new_session=True,
|
||||
text=True,
|
||||
errors="replace",
|
||||
bufsize=1,
|
||||
)
|
||||
else:
|
||||
process = subprocess.Popen(
|
||||
exec_cmd,
|
||||
|
||||
@ -22,6 +22,17 @@ class HostSandboxError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
# 宿主机网络权限档位
|
||||
NETWORK_PERMISSION_RESTRICTED = "restricted" # macOS: 仅本地回环;Linux/Windows: 暂不隔离
|
||||
NETWORK_PERMISSION_FULL = "full" # 完全开放
|
||||
NETWORK_PERMISSION_NONE = "none" # 完全禁止网络(后端保留)
|
||||
_NETWORK_PERMISSION_VALUES = {
|
||||
NETWORK_PERMISSION_RESTRICTED,
|
||||
NETWORK_PERMISSION_FULL,
|
||||
NETWORK_PERMISSION_NONE,
|
||||
}
|
||||
|
||||
|
||||
def _truthy(name: str, default: str = "1") -> bool:
|
||||
return os.environ.get(name, default).strip().lower() not in {"0", "false", "no", "off"}
|
||||
|
||||
@ -30,72 +41,126 @@ def host_sandbox_enabled() -> bool:
|
||||
return _truthy("HOST_SANDBOX_ENABLED", "1")
|
||||
|
||||
|
||||
def build_host_sandbox_plan(command: str, work_path: Path, env: Dict[str, str]) -> SandboxPlan:
|
||||
def _normalize_network_permission(value: Optional[str]) -> str:
|
||||
"""归一化网络权限值,非法值回退为 restricted。"""
|
||||
normalized = str(value or "").strip().lower()
|
||||
if normalized in _NETWORK_PERMISSION_VALUES:
|
||||
return normalized
|
||||
return NETWORK_PERMISSION_RESTRICTED
|
||||
|
||||
|
||||
def _build_macos_network_policy(network_permission: str) -> str:
|
||||
"""根据网络权限档位生成 macOS sandbox-exec 网络规则片段。"""
|
||||
permission = _normalize_network_permission(network_permission)
|
||||
if permission == NETWORK_PERMISSION_NONE:
|
||||
return ""
|
||||
if permission == NETWORK_PERMISSION_FULL:
|
||||
return "(allow network-outbound)\n(allow network-inbound)\n"
|
||||
# restricted: 仅允许本地回环出站(涵盖 127.0.0.1 / ::1 的实际效果)
|
||||
return '(allow network-outbound (remote ip "localhost:*"))\n'
|
||||
|
||||
|
||||
def build_host_sandbox_plan(
|
||||
command: str,
|
||||
work_path: Path,
|
||||
env: Dict[str, str],
|
||||
network_permission: Optional[str] = None,
|
||||
) -> SandboxPlan:
|
||||
system = platform.system()
|
||||
if system == "Darwin":
|
||||
return _build_macos_plan(command, work_path, env)
|
||||
return _build_macos_plan(command, work_path, env, network_permission)
|
||||
if system == "Linux":
|
||||
return _build_linux_plan(command, work_path, env)
|
||||
return _build_linux_plan(command, work_path, env, network_permission)
|
||||
if system == "Windows":
|
||||
return _build_windows_plan(command, work_path, env)
|
||||
return _build_windows_plan(command, work_path, env, network_permission)
|
||||
raise HostSandboxError(f"不支持的宿主机系统: {system}")
|
||||
|
||||
|
||||
def build_host_sandbox_readonly_plan(command: str, work_path: Path, env: Dict[str, str]) -> SandboxPlan:
|
||||
def build_host_sandbox_readonly_plan(
|
||||
command: str,
|
||||
work_path: Path,
|
||||
env: Dict[str, str],
|
||||
network_permission: Optional[str] = None,
|
||||
) -> SandboxPlan:
|
||||
system = platform.system()
|
||||
if system == "Darwin":
|
||||
return _build_macos_readonly_plan(command, work_path, env)
|
||||
return _build_macos_readonly_plan(command, work_path, env, network_permission)
|
||||
if system == "Linux":
|
||||
return _build_linux_readonly_plan(command, work_path, env)
|
||||
return _build_linux_readonly_plan(command, work_path, env, network_permission)
|
||||
if system == "Windows":
|
||||
return _build_windows_plan(command, work_path, env)
|
||||
raise HostSandboxError(f"不支持的宿主机系统: {system}")
|
||||
|
||||
def build_host_sandbox_shell_plan(work_path: Path, env: Dict[str, str]) -> SandboxPlan:
|
||||
system = platform.system()
|
||||
if system == "Darwin":
|
||||
return _build_macos_shell_plan(work_path, env)
|
||||
if system == "Linux":
|
||||
return _build_linux_shell_plan(work_path, env)
|
||||
if system == "Windows":
|
||||
return _build_windows_shell_plan(work_path, env)
|
||||
return _build_windows_plan(command, work_path, env, network_permission)
|
||||
raise HostSandboxError(f"不支持的宿主机系统: {system}")
|
||||
|
||||
|
||||
def _build_macos_plan(command: str, work_path: Path, env: Dict[str, str]) -> SandboxPlan:
|
||||
def build_host_sandbox_shell_plan(
|
||||
work_path: Path,
|
||||
env: Dict[str, str],
|
||||
network_permission: Optional[str] = None,
|
||||
) -> SandboxPlan:
|
||||
system = platform.system()
|
||||
if system == "Darwin":
|
||||
return _build_macos_shell_plan(work_path, env, network_permission)
|
||||
if system == "Linux":
|
||||
return _build_linux_shell_plan(work_path, env, network_permission)
|
||||
if system == "Windows":
|
||||
return _build_windows_shell_plan(work_path, env, network_permission)
|
||||
raise HostSandboxError(f"不支持的宿主机系统: {system}")
|
||||
|
||||
|
||||
def _build_macos_plan(
|
||||
command: str,
|
||||
work_path: Path,
|
||||
env: Dict[str, str],
|
||||
network_permission: Optional[str] = None,
|
||||
) -> SandboxPlan:
|
||||
sandbox_exec = shutil.which("sandbox-exec")
|
||||
if not sandbox_exec:
|
||||
raise HostSandboxError("macOS 未找到 sandbox-exec,拒绝执行宿主机命令。")
|
||||
profile = _macos_profile_for_workspace(work_path)
|
||||
profile = _macos_profile_for_workspace(work_path, network_permission)
|
||||
cmd = [sandbox_exec, "-p", profile, "/bin/bash", "-lc", command]
|
||||
return SandboxPlan(command=cmd, env=env, cwd=str(work_path))
|
||||
|
||||
|
||||
def _build_macos_readonly_plan(command: str, work_path: Path, env: Dict[str, str]) -> SandboxPlan:
|
||||
def _build_macos_readonly_plan(
|
||||
command: str,
|
||||
work_path: Path,
|
||||
env: Dict[str, str],
|
||||
network_permission: Optional[str] = None,
|
||||
) -> SandboxPlan:
|
||||
sandbox_exec = shutil.which("sandbox-exec")
|
||||
if not sandbox_exec:
|
||||
raise HostSandboxError("macOS 未找到 sandbox-exec,拒绝执行宿主机命令。")
|
||||
network_policy = _build_macos_network_policy(network_permission)
|
||||
profile = (
|
||||
'(version 1) '
|
||||
'(deny default) '
|
||||
'(allow sysctl-read) '
|
||||
'(allow process*) '
|
||||
'(allow network*) '
|
||||
f'{network_policy}'
|
||||
'(allow file-read*) '
|
||||
'(allow file-write* (literal "/dev/null"))'
|
||||
)
|
||||
cmd = [sandbox_exec, "-p", profile, "/bin/bash", "-lc", command]
|
||||
return SandboxPlan(command=cmd, env=env, cwd=str(work_path))
|
||||
|
||||
def _build_macos_shell_plan(work_path: Path, env: Dict[str, str]) -> SandboxPlan:
|
||||
|
||||
def _build_macos_shell_plan(
|
||||
work_path: Path,
|
||||
env: Dict[str, str],
|
||||
network_permission: Optional[str] = None,
|
||||
) -> SandboxPlan:
|
||||
sandbox_exec = shutil.which("sandbox-exec")
|
||||
if not sandbox_exec:
|
||||
raise HostSandboxError("macOS 未找到 sandbox-exec,拒绝启动宿主机沙箱终端。")
|
||||
profile = _macos_profile_for_workspace(work_path)
|
||||
profile = _macos_profile_for_workspace(work_path, network_permission)
|
||||
cmd = [sandbox_exec, "-p", profile, "/bin/bash", "-i"]
|
||||
return SandboxPlan(command=cmd, env=env, cwd=str(work_path))
|
||||
|
||||
def _macos_profile_for_workspace(work_path: Path) -> str:
|
||||
|
||||
def _macos_profile_for_workspace(
|
||||
work_path: Path,
|
||||
network_permission: Optional[str] = None,
|
||||
) -> str:
|
||||
workspace = str(work_path.resolve())
|
||||
writable_paths = [workspace, "/tmp", "/private/tmp", "/dev/null"]
|
||||
for raw in get_macos_writable_paths():
|
||||
@ -112,18 +177,24 @@ def _macos_profile_for_workspace(work_path: Path) -> str:
|
||||
else:
|
||||
write_rules.append(f'(subpath "{entry}")')
|
||||
write_expr = " ".join(write_rules)
|
||||
network_policy = _build_macos_network_policy(network_permission)
|
||||
return (
|
||||
'(version 1) '
|
||||
'(deny default) '
|
||||
'(allow sysctl-read) '
|
||||
'(allow process*) '
|
||||
'(allow network*) '
|
||||
f'{network_policy}'
|
||||
'(allow file-read*) '
|
||||
f'(allow file-write* {write_expr})'
|
||||
)
|
||||
|
||||
|
||||
def _build_linux_plan(command: str, work_path: Path, env: Dict[str, str]) -> SandboxPlan:
|
||||
def _build_linux_plan(
|
||||
command: str,
|
||||
work_path: Path,
|
||||
env: Dict[str, str],
|
||||
network_permission: Optional[str] = None,
|
||||
) -> SandboxPlan:
|
||||
bwrap = shutil.which("bwrap")
|
||||
if not bwrap:
|
||||
raise HostSandboxError("Linux 未找到 bubblewrap(bwrap),拒绝执行宿主机命令。")
|
||||
@ -135,10 +206,16 @@ def _build_linux_plan(command: str, work_path: Path, env: Dict[str, str]) -> San
|
||||
if not seccomp_path.exists():
|
||||
raise HostSandboxError(f"seccomp BPF 文件不存在: {seccomp_path}")
|
||||
shell_cmd = ["/bin/bash", "-lc", command]
|
||||
# network_permission 暂不参与 Linux 构建,保持现有 --share-net 行为
|
||||
return _build_linux_common_plan(work_path, env, shell_cmd, seccomp_path)
|
||||
|
||||
|
||||
def _build_linux_readonly_plan(command: str, work_path: Path, env: Dict[str, str]) -> SandboxPlan:
|
||||
def _build_linux_readonly_plan(
|
||||
command: str,
|
||||
work_path: Path,
|
||||
env: Dict[str, str],
|
||||
network_permission: Optional[str] = None,
|
||||
) -> SandboxPlan:
|
||||
bwrap = shutil.which("bwrap")
|
||||
if not bwrap:
|
||||
raise HostSandboxError("Linux 未找到 bubblewrap(bwrap),拒绝执行宿主机命令。")
|
||||
@ -151,7 +228,12 @@ def _build_linux_readonly_plan(command: str, work_path: Path, env: Dict[str, str
|
||||
shell_cmd = ["/bin/bash", "-lc", command]
|
||||
return _build_linux_common_plan(work_path, env, shell_cmd, seccomp_path, readonly=True)
|
||||
|
||||
def _build_linux_shell_plan(work_path: Path, env: Dict[str, str]) -> SandboxPlan:
|
||||
|
||||
def _build_linux_shell_plan(
|
||||
work_path: Path,
|
||||
env: Dict[str, str],
|
||||
network_permission: Optional[str] = None,
|
||||
) -> SandboxPlan:
|
||||
bwrap = shutil.which("bwrap")
|
||||
if not bwrap:
|
||||
raise HostSandboxError("Linux 未找到 bubblewrap(bwrap),拒绝启动宿主机沙箱终端。")
|
||||
@ -164,6 +246,7 @@ def _build_linux_shell_plan(work_path: Path, env: Dict[str, str]) -> SandboxPlan
|
||||
shell_cmd = ["/bin/bash", "-i"]
|
||||
return _build_linux_common_plan(work_path, env, shell_cmd, seccomp_path)
|
||||
|
||||
|
||||
def _build_linux_common_plan(
|
||||
work_path: Path,
|
||||
env: Dict[str, str],
|
||||
@ -205,7 +288,12 @@ def _build_linux_common_plan(
|
||||
return SandboxPlan(command=cmd, env=env, cwd=sandbox_root, seccomp_bpf_path=str(seccomp_path))
|
||||
|
||||
|
||||
def _build_windows_plan(command: str, work_path: Path, env: Dict[str, str]) -> SandboxPlan:
|
||||
def _build_windows_plan(
|
||||
command: str,
|
||||
work_path: Path,
|
||||
env: Dict[str, str],
|
||||
network_permission: Optional[str] = None,
|
||||
) -> SandboxPlan:
|
||||
wsl = shutil.which("wsl.exe")
|
||||
if not wsl:
|
||||
raise HostSandboxError("Windows 未找到 wsl.exe(WSL2),拒绝执行宿主机命令。")
|
||||
@ -214,7 +302,12 @@ def _build_windows_plan(command: str, work_path: Path, env: Dict[str, str]) -> S
|
||||
cmd = [wsl, "bash", "-lc", shell]
|
||||
return SandboxPlan(command=cmd, env=env, cwd=str(work_path))
|
||||
|
||||
def _build_windows_shell_plan(work_path: Path, env: Dict[str, str]) -> SandboxPlan:
|
||||
|
||||
def _build_windows_shell_plan(
|
||||
work_path: Path,
|
||||
env: Dict[str, str],
|
||||
network_permission: Optional[str] = None,
|
||||
) -> SandboxPlan:
|
||||
wsl = shutil.which("wsl.exe")
|
||||
if not wsl:
|
||||
raise HostSandboxError("Windows 未找到 wsl.exe(WSL2),拒绝启动宿主机沙箱终端。")
|
||||
|
||||
@ -247,7 +247,10 @@ class PersistentTerminal:
|
||||
env['LC_ALL'] = 'en_US.UTF-8'
|
||||
|
||||
try:
|
||||
plan = build_host_sandbox_shell_plan(self.working_dir, env)
|
||||
network_permission = os.environ.get("HOST_SANDBOX_NETWORK_PERMISSION", "restricted")
|
||||
plan = build_host_sandbox_shell_plan(
|
||||
self.working_dir, env, network_permission=network_permission
|
||||
)
|
||||
cmd_args = plan.command
|
||||
pass_fds = ()
|
||||
seccomp_fd = None
|
||||
|
||||
@ -188,7 +188,10 @@ class SubAgentManager:
|
||||
return {"success": False, "error": "宿主机子智能体执行被拒绝:HOST_SANDBOX_ENABLED=0"}
|
||||
try:
|
||||
command_str = self._join_shell_words(cmd)
|
||||
plan = build_host_sandbox_plan(command_str, self.project_path, env)
|
||||
network_permission = os.environ.get("HOST_SANDBOX_NETWORK_PERMISSION", "restricted")
|
||||
plan = build_host_sandbox_plan(
|
||||
command_str, self.project_path, env, network_permission=network_permission
|
||||
)
|
||||
launch_cmd, pass_fds, seccomp_fd = self._materialize_seccomp_fd(
|
||||
plan.command,
|
||||
plan.seccomp_bpf_path,
|
||||
|
||||
@ -18,6 +18,7 @@ try:
|
||||
OUTPUT_FORMATS,
|
||||
MAX_RUN_COMMAND_CHARS,
|
||||
TOOLBOX_TERMINAL_IDLE_SECONDS,
|
||||
HOST_SANDBOX_NETWORK_PERMISSION,
|
||||
)
|
||||
except ImportError:
|
||||
project_root = Path(__file__).resolve().parents[1]
|
||||
@ -29,10 +30,12 @@ except ImportError:
|
||||
OUTPUT_FORMATS,
|
||||
MAX_RUN_COMMAND_CHARS,
|
||||
TOOLBOX_TERMINAL_IDLE_SECONDS,
|
||||
HOST_SANDBOX_NETWORK_PERMISSION,
|
||||
)
|
||||
from modules.toolbox_container import ToolboxContainer
|
||||
from modules.host_sandbox_runner import (
|
||||
HostSandboxError,
|
||||
NETWORK_PERMISSION_RESTRICTED,
|
||||
build_host_sandbox_plan,
|
||||
build_host_sandbox_readonly_plan,
|
||||
host_sandbox_enabled,
|
||||
@ -308,6 +311,7 @@ class TerminalOperator:
|
||||
working_dir: str = None,
|
||||
timeout: int = None,
|
||||
sandbox_write_access: bool = True,
|
||||
network_permission: Optional[str] = None,
|
||||
) -> Dict:
|
||||
"""
|
||||
执行终端命令
|
||||
@ -383,6 +387,7 @@ class TerminalOperator:
|
||||
timeout,
|
||||
session_override=session_override,
|
||||
sandbox_write_access=sandbox_write_access,
|
||||
network_permission=network_permission,
|
||||
)
|
||||
else:
|
||||
# 若未绑定用户容器,则使用工具箱容器(与终端相同镜像/预装包)
|
||||
@ -425,7 +430,7 @@ class TerminalOperator:
|
||||
|
||||
# 改为一次性子进程执行,确保等待到超时或命令结束
|
||||
result_payload = result_payload if result_payload is not None else await self._run_command_subprocess(
|
||||
command, work_path, timeout, sandbox_write_access=sandbox_write_access
|
||||
command, work_path, timeout, sandbox_write_access=sandbox_write_access, network_permission=network_permission
|
||||
)
|
||||
|
||||
# 字符数检查
|
||||
@ -488,6 +493,7 @@ class TerminalOperator:
|
||||
timeout: int,
|
||||
session_override: Optional["ContainerHandle"] = None,
|
||||
sandbox_write_access: bool = True,
|
||||
network_permission: Optional[str] = None,
|
||||
) -> Dict:
|
||||
start_ts = time.time()
|
||||
try:
|
||||
@ -534,9 +540,13 @@ class TerminalOperator:
|
||||
use_host_sandbox = self.host_execution_mode != "direct"
|
||||
if use_host_sandbox and host_sandbox_enabled():
|
||||
if sandbox_write_access:
|
||||
plan = build_host_sandbox_plan(command, work_path, env)
|
||||
plan = build_host_sandbox_plan(
|
||||
command, work_path, env, network_permission=network_permission
|
||||
)
|
||||
else:
|
||||
plan = build_host_sandbox_readonly_plan(command, work_path, env)
|
||||
plan = build_host_sandbox_readonly_plan(
|
||||
command, work_path, env, network_permission=network_permission
|
||||
)
|
||||
cmd_args, pass_fds, seccomp_fd = self._materialize_seccomp_fd(
|
||||
plan.command,
|
||||
plan.seccomp_bpf_path,
|
||||
|
||||
@ -2,3 +2,4 @@
|
||||
|
||||
### 当前规则
|
||||
{rules}
|
||||
{network_rules}
|
||||
|
||||
@ -50,6 +50,7 @@ chat_bp = Blueprint('chat', __name__)
|
||||
|
||||
PERMISSION_MODE_OPTIONS = ["readonly", "approval", "auto_approval", "unrestricted"]
|
||||
EXECUTION_MODE_OPTIONS = ["sandbox", "direct"]
|
||||
NETWORK_PERMISSION_OPTIONS = ["restricted", "full"]
|
||||
|
||||
|
||||
def _dispatch_runtime_mode_notice(terminal: WebTerminal, username: str, text: str, source: str = "notify") -> None:
|
||||
@ -857,6 +858,90 @@ def update_execution_mode(terminal: WebTerminal, workspace: UserWorkspace, usern
|
||||
})
|
||||
|
||||
|
||||
@chat_bp.route('/api/network-permission', methods=['GET'])
|
||||
@api_login_required
|
||||
@with_terminal
|
||||
def get_network_permission(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||
is_host = bool(getattr(terminal, "_is_host_mode", lambda: False)())
|
||||
can_manage = is_host and getattr(terminal, "user_role", "user") == "admin"
|
||||
current = terminal.get_network_permission() if hasattr(terminal, "get_network_permission") else "restricted"
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"enabled": can_manage,
|
||||
"mode": current,
|
||||
"pending_mode": (terminal.get_pending_runtime_modes().get("network_permission") if hasattr(terminal, "get_pending_runtime_modes") else None),
|
||||
"options": NETWORK_PERMISSION_OPTIONS,
|
||||
})
|
||||
|
||||
|
||||
@chat_bp.route('/api/network-permission', methods=['POST'])
|
||||
@api_login_required
|
||||
@with_terminal
|
||||
@rate_limited("network_permission_switch", 20, 60, scope="user")
|
||||
def update_network_permission(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||
is_host = bool(getattr(terminal, "_is_host_mode", lambda: False)())
|
||||
can_manage = is_host and getattr(terminal, "user_role", "user") == "admin"
|
||||
if not can_manage:
|
||||
return jsonify({"success": False, "error": "仅宿主机管理员可切换网络权限"}), 403
|
||||
data = request.get_json() or {}
|
||||
target_mode = str(data.get("mode") or "").strip().lower()
|
||||
if target_mode not in NETWORK_PERMISSION_OPTIONS:
|
||||
return jsonify({"success": False, "error": "无效网络权限,仅支持 restricted / full"}), 400
|
||||
|
||||
is_running = False
|
||||
try:
|
||||
from .tasks import task_manager
|
||||
current_conv = getattr(getattr(terminal, "context_manager", None), "current_conversation_id", None)
|
||||
running_tasks = [
|
||||
r for r in task_manager.list_tasks(username) if r.status in {"pending", "running", "cancel_requested"}
|
||||
]
|
||||
if current_conv:
|
||||
running_tasks.sort(key=lambda r: 0 if r.conversation_id == current_conv else 1)
|
||||
is_running = bool(running_tasks)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if is_running:
|
||||
try:
|
||||
terminal.queue_network_permission_change(target_mode)
|
||||
except Exception as exc:
|
||||
return jsonify({"success": False, "error": str(exc), "message": "更新网络权限失败"}), 500
|
||||
status = terminal.get_status()
|
||||
socketio.emit('status_update', status, room=f"user_{username}")
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"mode": target_mode,
|
||||
"pending_mode": target_mode,
|
||||
"options": NETWORK_PERMISSION_OPTIONS,
|
||||
"message": "网络权限将在当前工具执行完成后生效",
|
||||
})
|
||||
|
||||
previous_mode = terminal.get_network_permission() if hasattr(terminal, "get_network_permission") else None
|
||||
try:
|
||||
applied = terminal.set_network_permission(target_mode)
|
||||
if hasattr(terminal, "pending_network_permission"):
|
||||
terminal.pending_network_permission = None
|
||||
if hasattr(terminal, "_persist_runtime_mode_metadata"):
|
||||
terminal._persist_runtime_mode_metadata({
|
||||
"network_permission": applied,
|
||||
"pending_network_permission": None,
|
||||
})
|
||||
except Exception as exc:
|
||||
return jsonify({"success": False, "error": str(exc), "message": "更新网络权限失败"}), 500
|
||||
status = terminal.get_status()
|
||||
if applied != previous_mode:
|
||||
label = {"restricted": "受限", "full": "完全开放", "none": "完全禁止"}.get(applied, applied)
|
||||
_dispatch_runtime_mode_notice(terminal, username, f"网络权限被用户修改为 {label}", source="网络权限变更")
|
||||
socketio.emit('status_update', status, room=f"user_{username}")
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"mode": applied,
|
||||
"pending_mode": None,
|
||||
"options": NETWORK_PERMISSION_OPTIONS,
|
||||
"message": "网络权限已更新并立即生效",
|
||||
})
|
||||
|
||||
|
||||
@chat_bp.route('/api/path-authorization', methods=['GET'])
|
||||
@api_login_required
|
||||
@with_terminal
|
||||
|
||||
@ -1703,9 +1703,9 @@ async def handle_task_with_sender(
|
||||
username=username,
|
||||
task_id=client_sid,
|
||||
)
|
||||
# 权限/审核方式变更通知在任务完成时不应该触发新一轮工作,
|
||||
# 只保留真正的引导消息(压缩续接等),丢弃权限/执行环境变更通知。
|
||||
runtime_skip_sources = {"权限变更", "执行环境变更", "notify"}
|
||||
# 权限/执行环境/网络权限变更通知在任务完成时不应该触发新一轮工作,
|
||||
# 只保留真正的引导消息(压缩续接等),丢弃变更通知。
|
||||
runtime_skip_sources = {"权限变更", "执行环境变更", "网络权限变更", "notify"}
|
||||
for item in (raw_items or []):
|
||||
if isinstance(item, dict):
|
||||
src = str(item.get("source") or "").strip().lower()
|
||||
|
||||
@ -163,6 +163,13 @@ def _is_permission_denied_result(result_data: Dict[str, Any]) -> bool:
|
||||
"无权限",
|
||||
"不允许",
|
||||
"access denied",
|
||||
# 网络受限常见报错
|
||||
"could not resolve host",
|
||||
"nodename nor servname provided, or not known",
|
||||
"unknown host",
|
||||
"network is unreachable",
|
||||
"no route to host",
|
||||
"socket.gaierror",
|
||||
)
|
||||
return any(marker in joined for marker in markers)
|
||||
|
||||
@ -940,6 +947,7 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
||||
|
||||
retry_arguments = dict(arguments or {})
|
||||
retry_arguments["_approval_write_granted"] = True
|
||||
retry_arguments["_approval_network_granted"] = True
|
||||
retry_tool_result = await web_terminal.handle_tool_call(function_name, retry_arguments)
|
||||
try:
|
||||
result_data = json.loads(retry_tool_result)
|
||||
|
||||
@ -303,6 +303,9 @@
|
||||
:execution-mode-enabled="executionModeEnabled"
|
||||
:current-execution-mode="currentExecutionMode"
|
||||
:execution-mode-options="executionModeOptions"
|
||||
:network-permission-enabled="networkPermissionEnabled"
|
||||
:current-network-permission="currentNetworkPermission"
|
||||
:network-permission-options="networkPermissionOptions"
|
||||
:current-context-tokens="currentContextTokens"
|
||||
:versioning-enabled="versioningEnabled"
|
||||
:runtime-queued-messages="runtimeQueuedMessages"
|
||||
@ -343,6 +346,7 @@
|
||||
@toggle-permission-menu="togglePermissionMenu"
|
||||
@change-permission-mode="changePermissionMode"
|
||||
@change-execution-mode="changeExecutionMode"
|
||||
@change-network-permission="changeNetworkPermission"
|
||||
@open-path-authorization="openPathAuthorizationDialog"
|
||||
@open-versioning-dialog="openVersioningDialog"
|
||||
@guide-runtime-message="handleGuideRuntimeMessage"
|
||||
|
||||
@ -379,6 +379,7 @@ export const conversationMethods = {
|
||||
}
|
||||
this.fetchPermissionMode();
|
||||
this.fetchExecutionMode();
|
||||
this.fetchNetworkPermission();
|
||||
await this.fetchVersioningStatus(conversationId, { silent: true });
|
||||
await this.maybePromptVersioningMismatch(result.versioning);
|
||||
this.fetchPendingToolApprovals();
|
||||
|
||||
@ -203,6 +203,16 @@ export const resourceMethods = {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (typeof status.network_permission === 'string') {
|
||||
this.currentNetworkPermission = status.network_permission;
|
||||
}
|
||||
this.networkPermissionEnabled =
|
||||
typeof status.network_permission_enabled === 'boolean' ? status.network_permission_enabled : false;
|
||||
if (typeof pendingModes.network_permission === 'string') {
|
||||
this.pendingNetworkPermission = pendingModes.network_permission;
|
||||
} else {
|
||||
this.pendingNetworkPermission = '';
|
||||
}
|
||||
if (typeof pendingModes.execution_mode === 'string') {
|
||||
this.pendingExecutionMode = pendingModes.execution_mode;
|
||||
} else {
|
||||
|
||||
@ -1859,6 +1859,59 @@ export const uiMethods = {
|
||||
}
|
||||
},
|
||||
|
||||
async changeNetworkPermission(mode) {
|
||||
const target = String(mode || '').trim().toLowerCase();
|
||||
if (!this.networkPermissionEnabled || !target) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch('/api/network-permission', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mode: target })
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok || !payload?.success) {
|
||||
throw new Error(payload?.message || payload?.error || '切换网络权限失败');
|
||||
}
|
||||
if (typeof payload.mode === 'string') {
|
||||
this.currentNetworkPermission = payload.mode;
|
||||
}
|
||||
this.pendingNetworkPermission = typeof payload.pending_mode === 'string' ? payload.pending_mode : '';
|
||||
const labelMap: Record<string, string> = { restricted: '受限', full: '完全开放' };
|
||||
this.uiPushToast({
|
||||
title: '网络权限已更新',
|
||||
message: payload?.message || `已切换为 ${labelMap[this.currentNetworkPermission] || this.currentNetworkPermission}`,
|
||||
type: this.currentNetworkPermission === 'full' ? 'warning' : 'info',
|
||||
duration: 1800
|
||||
});
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error || '切换网络权限失败');
|
||||
this.uiPushToast({
|
||||
title: '切换网络权限失败',
|
||||
message: msg,
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async fetchNetworkPermission() {
|
||||
try {
|
||||
const response = await fetch('/api/network-permission');
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok || !payload?.success) {
|
||||
return;
|
||||
}
|
||||
this.networkPermissionEnabled = !!payload.enabled;
|
||||
if (typeof payload.mode === 'string') {
|
||||
this.currentNetworkPermission = payload.mode;
|
||||
}
|
||||
this.pendingNetworkPermission = typeof payload.pending_mode === 'string' ? payload.pending_mode : '';
|
||||
} catch (_error) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
|
||||
async fetchPermissionMode() {
|
||||
try {
|
||||
const response = await fetch('/api/permission-mode');
|
||||
@ -3524,6 +3577,7 @@ export const uiMethods = {
|
||||
this.applyStatusSnapshot(statusData);
|
||||
this.fetchPermissionMode();
|
||||
this.fetchExecutionMode();
|
||||
this.fetchNetworkPermission();
|
||||
this.fetchPendingToolApprovals();
|
||||
// 立即更新配额和运行模式,避免等待其他慢接口
|
||||
this.fetchUsageQuota();
|
||||
|
||||
@ -129,6 +129,21 @@ export function dataState() {
|
||||
executionModeAutoFallbackToastId: null,
|
||||
executionModeTtlSeconds: 600,
|
||||
executionModeExpirySyncTimer: null,
|
||||
networkPermissionEnabled: false,
|
||||
currentNetworkPermission: 'restricted',
|
||||
pendingNetworkPermission: '',
|
||||
networkPermissionOptions: [
|
||||
{
|
||||
value: 'restricted',
|
||||
label: '受限',
|
||||
description: '仅允许本地回环访问,外部网络不可用'
|
||||
},
|
||||
{
|
||||
value: 'full',
|
||||
label: '完全开放',
|
||||
description: '允许所有出站和入站网络连接'
|
||||
}
|
||||
],
|
||||
pathAuthorizationDialogOpen: false,
|
||||
pathAuthorizationMode: 'writable',
|
||||
pathAuthorizationWritableDraft: '',
|
||||
|
||||
@ -375,6 +375,29 @@
|
||||
<span class="permission-switcher__item-desc">{{ option.description }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="networkPermissionEnabled"
|
||||
class="permission-switcher__group"
|
||||
:class="{ 'permission-switcher__group--disabled': currentExecutionMode === 'direct' }"
|
||||
>
|
||||
<div class="permission-switcher__group-title">网络权限</div>
|
||||
<button
|
||||
v-for="option in networkPermissionOptions"
|
||||
:key="`network-${option.value}`"
|
||||
type="button"
|
||||
class="permission-switcher__item"
|
||||
:class="{ active: option.value === currentNetworkPermission }"
|
||||
:disabled="currentExecutionMode === 'direct'"
|
||||
@click="$emit('change-network-permission', option.value)"
|
||||
>
|
||||
<span
|
||||
class="permission-switcher__item-label"
|
||||
:class="{ 'permission-switcher__item-label--warn': option.value === 'full' }"
|
||||
>{{ option.label }}</span
|
||||
>
|
||||
<span class="permission-switcher__item-desc">{{ option.description }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -459,6 +482,7 @@ const emit = defineEmits([
|
||||
'toggle-permission-menu',
|
||||
'change-permission-mode',
|
||||
'change-execution-mode',
|
||||
'change-network-permission',
|
||||
'open-versioning-dialog',
|
||||
'guide-runtime-message',
|
||||
'delete-runtime-message',
|
||||
@ -516,6 +540,9 @@ const props = defineProps<{
|
||||
executionModeEnabled?: boolean;
|
||||
currentExecutionMode?: 'sandbox' | 'direct';
|
||||
executionModeOptions?: Array<{ value: string; label: string; description: string }>;
|
||||
networkPermissionEnabled?: boolean;
|
||||
currentNetworkPermission?: 'restricted' | 'full';
|
||||
networkPermissionOptions?: Array<{ value: string; label: string; description: string }>;
|
||||
currentContextTokens: number;
|
||||
versioningEnabled?: boolean;
|
||||
runtimeQueuedMessages?: Array<{ id: string; text: string }>;
|
||||
|
||||
@ -492,12 +492,21 @@ body[data-theme='light'] {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
|
||||
> :first-child.permission-switcher__group {
|
||||
grid-row: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
.permission-switcher__group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.35;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.permission-switcher__group-title {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user