From 2ec226b5eb8f30945e4292e341645e547925ca47 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Mon, 11 May 2026 22:56:10 +0800 Subject: [PATCH] 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. --- config/docker_risk_markers.json | 35 +++++++ core/main_terminal_parts/tools_execution.py | 96 +++++++++++++++++++ modules/auto_approval_service.py | 9 +- server/chat_flow_tool_loop.py | 2 + static/src/components/input/InputComposer.vue | 6 +- .../styles/components/input/_composer.scss | 1 + 6 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 config/docker_risk_markers.json diff --git a/config/docker_risk_markers.json b/config/docker_risk_markers.json new file mode 100644 index 0000000..907d625 --- /dev/null +++ b/config/docker_risk_markers.json @@ -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" + ] +} diff --git a/core/main_terminal_parts/tools_execution.py b/core/main_terminal_parts/tools_execution.py index f116ba0..a5ed1c5 100644 --- a/core/main_terminal_parts/tools_execution.py +++ b/core/main_terminal_parts/tools_execution.py @@ -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: diff --git a/modules/auto_approval_service.py b/modules/auto_approval_service.py index 702acd0..e07e67a 100644 --- a/modules/auto_approval_service.py +++ b/modules/auto_approval_service.py @@ -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 - diff --git a/server/chat_flow_tool_loop.py b/server/chat_flow_tool_loop.py index 5681155..fa65629 100644 --- a/server/chat_flow_tool_loop.py +++ b/server/chat_flow_tool_loop.py @@ -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: diff --git a/static/src/components/input/InputComposer.vue b/static/src/components/input/InputComposer.vue index d0a5601..b3debc7 100644 --- a/static/src/components/input/InputComposer.vue +++ b/static/src/components/input/InputComposer.vue @@ -183,7 +183,11 @@ 权限:{{ currentPermissionLabel }} -