fix(permission,docker): enforce docker-specific approval flow and polish permission menu layout
- Add docker risk markers config (config/docker_risk_markers.json) and load it in tools permission evaluation. - In docker mode, pre-evaluate run_command/terminal_input: readonly-safe and no-risk -> direct run; otherwise require approval/auto-approval. - Keep docker write_file/edit_file behavior aligned with agreed policy: approval requires review, auto_approval passes directly. - Pass risk markers into auto approval payload and include current working directory context for approval agent. - Fix permission dropdown layout: only use split two-column menu when execution mode section is enabled; remove empty right blank area in docker mode. - Center mobile approval overlay to prevent sheet overflow on small screens.
This commit is contained in:
parent
8d665ad6de
commit
2ec226b5eb
35
config/docker_risk_markers.json
Normal file
35
config/docker_risk_markers.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"risk_markers": [
|
||||
"rm",
|
||||
"rmdir",
|
||||
"mv",
|
||||
"cp",
|
||||
"dd",
|
||||
"chmod",
|
||||
"chown",
|
||||
"chgrp",
|
||||
"sudo",
|
||||
"su",
|
||||
"passwd",
|
||||
"useradd",
|
||||
"userdel",
|
||||
"curl",
|
||||
"wget",
|
||||
"nc",
|
||||
"ncat",
|
||||
"scp",
|
||||
"rsync",
|
||||
".env",
|
||||
".ssh",
|
||||
"id_rsa",
|
||||
"id_ed25519",
|
||||
".aws",
|
||||
"token",
|
||||
"secret",
|
||||
">/",
|
||||
">>",
|
||||
"| sh",
|
||||
"| bash",
|
||||
"| zsh"
|
||||
]
|
||||
}
|
||||
@ -97,6 +97,58 @@ logger = setup_logger(__name__)
|
||||
DISABLE_LENGTH_CHECK = True
|
||||
|
||||
class MainTerminalToolsExecutionMixin:
|
||||
_DEFAULT_DOCKER_RISK_MARKERS = {
|
||||
"rm", "rmdir", "mv", "cp", "dd", "chmod", "chown", "chgrp",
|
||||
"sudo", "su", "passwd", "useradd", "userdel",
|
||||
"curl", "wget", "nc", "ncat", "scp", "rsync",
|
||||
".env", ".ssh", "id_rsa", "id_ed25519", ".aws", "token", "secret",
|
||||
">/", ">>", "| sh", "| bash", "| zsh",
|
||||
}
|
||||
|
||||
def _is_docker_runtime(self) -> bool:
|
||||
session = getattr(self, "container_session", None)
|
||||
return bool(session and getattr(session, "mode", None) == "docker")
|
||||
|
||||
def _extract_command_risk_markers(self, command: Any) -> List[str]:
|
||||
cmd = str(command or "")
|
||||
lowered = cmd.lower()
|
||||
markers = self._load_docker_risk_markers()
|
||||
hits: List[str] = []
|
||||
for marker in markers:
|
||||
if marker in lowered:
|
||||
hits.append(marker)
|
||||
# 去重并保持顺序
|
||||
dedup: List[str] = []
|
||||
seen = set()
|
||||
for m in hits:
|
||||
if m in seen:
|
||||
continue
|
||||
seen.add(m)
|
||||
dedup.append(m)
|
||||
return dedup
|
||||
|
||||
def _load_docker_risk_markers(self) -> List[str]:
|
||||
cache = getattr(self, "_docker_risk_markers_cache", None)
|
||||
if isinstance(cache, list) and cache:
|
||||
return cache
|
||||
config_path = Path(__file__).resolve().parents[2] / "config" / "docker_risk_markers.json"
|
||||
markers: List[str] = []
|
||||
try:
|
||||
if config_path.exists():
|
||||
data = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
raw = data.get("risk_markers") if isinstance(data, dict) else None
|
||||
if isinstance(raw, list):
|
||||
for item in raw:
|
||||
text = str(item or "").strip().lower()
|
||||
if text:
|
||||
markers.append(text)
|
||||
except Exception:
|
||||
markers = []
|
||||
if not markers:
|
||||
markers = sorted(self._DEFAULT_DOCKER_RISK_MARKERS)
|
||||
self._docker_risk_markers_cache = markers
|
||||
return markers
|
||||
|
||||
@staticmethod
|
||||
def _mcp_disabled_message() -> str:
|
||||
return "当前为docker模式,MCP仅支持宿主机模式"
|
||||
@ -332,9 +384,30 @@ class MainTerminalToolsExecutionMixin:
|
||||
|
||||
if mode == "approval":
|
||||
if tool_name == "run_command":
|
||||
if self._is_docker_runtime():
|
||||
command_text = args.get("command")
|
||||
readonly_ok = self._is_readonly_run_command_allowed(command_text)
|
||||
risk_markers = self._extract_command_risk_markers(command_text)
|
||||
requires = (not readonly_ok) or bool(risk_markers)
|
||||
return {
|
||||
"allowed": True,
|
||||
"mode": mode,
|
||||
"requires_approval": requires,
|
||||
"risk_markers": risk_markers,
|
||||
}
|
||||
return {"allowed": True, "mode": mode, "requires_approval": False}
|
||||
if tool_name == "terminal_input":
|
||||
command_text = args.get("command") or args.get("input")
|
||||
if self._is_docker_runtime():
|
||||
readonly_ok = self._is_readonly_run_command_allowed(command_text)
|
||||
risk_markers = self._extract_command_risk_markers(command_text)
|
||||
requires = (not readonly_ok) or bool(risk_markers)
|
||||
return {
|
||||
"allowed": True,
|
||||
"mode": mode,
|
||||
"requires_approval": requires,
|
||||
"risk_markers": risk_markers,
|
||||
}
|
||||
if self._is_readonly_run_command_allowed(command_text):
|
||||
return {"allowed": True, "mode": mode, "requires_approval": False}
|
||||
if tool_name in self._APPROVAL_REQUIRED_TOOLS:
|
||||
@ -343,6 +416,8 @@ class MainTerminalToolsExecutionMixin:
|
||||
|
||||
if mode == "auto_approval":
|
||||
if tool_name in {"write_file", "edit_file"}:
|
||||
if self._is_docker_runtime():
|
||||
return {"allowed": True, "mode": mode, "requires_approval": False}
|
||||
path = args.get("file_path") or args.get("path") or ""
|
||||
try:
|
||||
valid, _, full_path = self.file_manager._validate_path(str(path))
|
||||
@ -355,9 +430,30 @@ class MainTerminalToolsExecutionMixin:
|
||||
pass
|
||||
return {"allowed": True, "mode": mode, "requires_approval": True}
|
||||
if tool_name == "run_command":
|
||||
if self._is_docker_runtime():
|
||||
command_text = args.get("command")
|
||||
readonly_ok = self._is_readonly_run_command_allowed(command_text)
|
||||
risk_markers = self._extract_command_risk_markers(command_text)
|
||||
requires = (not readonly_ok) or bool(risk_markers)
|
||||
return {
|
||||
"allowed": True,
|
||||
"mode": mode,
|
||||
"requires_approval": requires,
|
||||
"risk_markers": risk_markers,
|
||||
}
|
||||
return {"allowed": True, "mode": mode, "requires_approval": False}
|
||||
if tool_name == "terminal_input":
|
||||
command_text = args.get("command") or args.get("input")
|
||||
if self._is_docker_runtime():
|
||||
readonly_ok = self._is_readonly_run_command_allowed(command_text)
|
||||
risk_markers = self._extract_command_risk_markers(command_text)
|
||||
requires = (not readonly_ok) or bool(risk_markers)
|
||||
return {
|
||||
"allowed": True,
|
||||
"mode": mode,
|
||||
"requires_approval": requires,
|
||||
"risk_markers": risk_markers,
|
||||
}
|
||||
if self._is_readonly_run_command_allowed(command_text):
|
||||
return {"allowed": True, "mode": mode, "requires_approval": False}
|
||||
if tool_name in self._APPROVAL_REQUIRED_TOOLS:
|
||||
|
||||
@ -13,6 +13,7 @@ def build_auto_approval_payload(
|
||||
recent_tool_actions: List[Dict[str, Any]],
|
||||
function_name: str,
|
||||
arguments: Dict[str, Any],
|
||||
risk_markers: Optional[List[str]] = None,
|
||||
) -> str:
|
||||
history = list(getattr(getattr(web_terminal, "context_manager", None), "conversation_history", []) or [])
|
||||
user_input = ""
|
||||
@ -38,6 +39,11 @@ def build_auto_approval_payload(
|
||||
lines.append(f"{idx}. {row.get('tool_name')}: {json.dumps(row.get('arguments') or {}, ensure_ascii=False)}")
|
||||
else:
|
||||
lines.append("(无)")
|
||||
lines += [
|
||||
f"当前工作目录:{('/workspace' if bool(getattr(getattr(web_terminal, 'container_session', None), 'mode', None) == 'docker') else str(getattr(getattr(web_terminal, 'context_manager', None), 'project_path', '')))}",
|
||||
]
|
||||
if risk_markers:
|
||||
lines.append(f"当前操作中触发风险特征的内容:{', '.join([str(x) for x in risk_markers if str(x).strip()])}")
|
||||
lines += [
|
||||
"请审核当前操作:",
|
||||
f"{function_name}: {json.dumps(arguments or {}, ensure_ascii=False)}",
|
||||
@ -55,6 +61,7 @@ async def run_auto_approval(
|
||||
recent_tool_actions: List[Dict[str, Any]],
|
||||
function_name: str,
|
||||
arguments: Dict[str, Any],
|
||||
risk_markers: Optional[List[str]],
|
||||
sender,
|
||||
) -> Dict[str, Any]:
|
||||
def _progress(evt: Dict[str, Any]) -> None:
|
||||
@ -73,6 +80,7 @@ async def run_auto_approval(
|
||||
recent_tool_actions=recent_tool_actions,
|
||||
function_name=function_name,
|
||||
arguments=arguments,
|
||||
risk_markers=risk_markers,
|
||||
)
|
||||
agent = ApprovalAgent(web_terminal=web_terminal)
|
||||
result = await agent.review(payload_text=payload, progress_cb=_progress, cancel_check=_manual_takeover)
|
||||
@ -89,4 +97,3 @@ async def run_auto_approval(
|
||||
out = _manual_takeover() or {"decision": "rejected", "reason": "人工接管失败"}
|
||||
_progress({"stage": "done", "message": "自动审批结束", "decision": (out or {}).get("decision")})
|
||||
return out
|
||||
|
||||
|
||||
@ -497,6 +497,7 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
||||
recent_tool_actions=previous_tool_actions,
|
||||
function_name=function_name,
|
||||
arguments=arguments,
|
||||
risk_markers=permission_eval.get("risk_markers") if isinstance(permission_eval, dict) else None,
|
||||
sender=sender,
|
||||
)
|
||||
else:
|
||||
@ -731,6 +732,7 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
||||
recent_tool_actions=previous_tool_actions,
|
||||
function_name=function_name,
|
||||
arguments=arguments,
|
||||
risk_markers=permission_eval.get("risk_markers") if isinstance(permission_eval, dict) else None,
|
||||
sender=sender,
|
||||
)
|
||||
else:
|
||||
|
||||
@ -183,7 +183,11 @@
|
||||
<span>权限:{{ currentPermissionLabel }}</span>
|
||||
<span class="permission-switcher__caret" :class="{ open: permissionMenuOpen }">›</span>
|
||||
</button>
|
||||
<div v-if="permissionMenuOpen" class="permission-switcher__menu permission-switcher__menu--split">
|
||||
<div
|
||||
v-if="permissionMenuOpen"
|
||||
class="permission-switcher__menu"
|
||||
:class="{ 'permission-switcher__menu--split': executionModeEnabled }"
|
||||
>
|
||||
<div class="permission-switcher__group">
|
||||
<div class="permission-switcher__group-title">权限</div>
|
||||
<button
|
||||
|
||||
@ -943,6 +943,7 @@ body[data-theme='dark'] {
|
||||
/* 手机端审批面板样式 - 与电脑端保持一致 */
|
||||
.mobile-approval-overlay {
|
||||
pointer-events: auto;
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.mobile-approval-overlay .mobile-panel-sheet--approval {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user