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
|
DISABLE_LENGTH_CHECK = True
|
||||||
|
|
||||||
class MainTerminalToolsExecutionMixin:
|
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
|
@staticmethod
|
||||||
def _mcp_disabled_message() -> str:
|
def _mcp_disabled_message() -> str:
|
||||||
return "当前为docker模式,MCP仅支持宿主机模式"
|
return "当前为docker模式,MCP仅支持宿主机模式"
|
||||||
@ -332,9 +384,30 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
|
|
||||||
if mode == "approval":
|
if mode == "approval":
|
||||||
if tool_name == "run_command":
|
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}
|
return {"allowed": True, "mode": mode, "requires_approval": False}
|
||||||
if tool_name == "terminal_input":
|
if tool_name == "terminal_input":
|
||||||
command_text = args.get("command") or args.get("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):
|
if self._is_readonly_run_command_allowed(command_text):
|
||||||
return {"allowed": True, "mode": mode, "requires_approval": False}
|
return {"allowed": True, "mode": mode, "requires_approval": False}
|
||||||
if tool_name in self._APPROVAL_REQUIRED_TOOLS:
|
if tool_name in self._APPROVAL_REQUIRED_TOOLS:
|
||||||
@ -343,6 +416,8 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
|
|
||||||
if mode == "auto_approval":
|
if mode == "auto_approval":
|
||||||
if tool_name in {"write_file", "edit_file"}:
|
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 ""
|
path = args.get("file_path") or args.get("path") or ""
|
||||||
try:
|
try:
|
||||||
valid, _, full_path = self.file_manager._validate_path(str(path))
|
valid, _, full_path = self.file_manager._validate_path(str(path))
|
||||||
@ -355,9 +430,30 @@ class MainTerminalToolsExecutionMixin:
|
|||||||
pass
|
pass
|
||||||
return {"allowed": True, "mode": mode, "requires_approval": True}
|
return {"allowed": True, "mode": mode, "requires_approval": True}
|
||||||
if tool_name == "run_command":
|
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}
|
return {"allowed": True, "mode": mode, "requires_approval": False}
|
||||||
if tool_name == "terminal_input":
|
if tool_name == "terminal_input":
|
||||||
command_text = args.get("command") or args.get("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):
|
if self._is_readonly_run_command_allowed(command_text):
|
||||||
return {"allowed": True, "mode": mode, "requires_approval": False}
|
return {"allowed": True, "mode": mode, "requires_approval": False}
|
||||||
if tool_name in self._APPROVAL_REQUIRED_TOOLS:
|
if tool_name in self._APPROVAL_REQUIRED_TOOLS:
|
||||||
|
|||||||
@ -13,6 +13,7 @@ def build_auto_approval_payload(
|
|||||||
recent_tool_actions: List[Dict[str, Any]],
|
recent_tool_actions: List[Dict[str, Any]],
|
||||||
function_name: str,
|
function_name: str,
|
||||||
arguments: Dict[str, Any],
|
arguments: Dict[str, Any],
|
||||||
|
risk_markers: Optional[List[str]] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
history = list(getattr(getattr(web_terminal, "context_manager", None), "conversation_history", []) or [])
|
history = list(getattr(getattr(web_terminal, "context_manager", None), "conversation_history", []) or [])
|
||||||
user_input = ""
|
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)}")
|
lines.append(f"{idx}. {row.get('tool_name')}: {json.dumps(row.get('arguments') or {}, ensure_ascii=False)}")
|
||||||
else:
|
else:
|
||||||
lines.append("(无)")
|
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 += [
|
lines += [
|
||||||
"请审核当前操作:",
|
"请审核当前操作:",
|
||||||
f"{function_name}: {json.dumps(arguments or {}, ensure_ascii=False)}",
|
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]],
|
recent_tool_actions: List[Dict[str, Any]],
|
||||||
function_name: str,
|
function_name: str,
|
||||||
arguments: Dict[str, Any],
|
arguments: Dict[str, Any],
|
||||||
|
risk_markers: Optional[List[str]],
|
||||||
sender,
|
sender,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
def _progress(evt: Dict[str, Any]) -> None:
|
def _progress(evt: Dict[str, Any]) -> None:
|
||||||
@ -73,6 +80,7 @@ async def run_auto_approval(
|
|||||||
recent_tool_actions=recent_tool_actions,
|
recent_tool_actions=recent_tool_actions,
|
||||||
function_name=function_name,
|
function_name=function_name,
|
||||||
arguments=arguments,
|
arguments=arguments,
|
||||||
|
risk_markers=risk_markers,
|
||||||
)
|
)
|
||||||
agent = ApprovalAgent(web_terminal=web_terminal)
|
agent = ApprovalAgent(web_terminal=web_terminal)
|
||||||
result = await agent.review(payload_text=payload, progress_cb=_progress, cancel_check=_manual_takeover)
|
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": "人工接管失败"}
|
out = _manual_takeover() or {"decision": "rejected", "reason": "人工接管失败"}
|
||||||
_progress({"stage": "done", "message": "自动审批结束", "decision": (out or {}).get("decision")})
|
_progress({"stage": "done", "message": "自动审批结束", "decision": (out or {}).get("decision")})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|||||||
@ -497,6 +497,7 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
|||||||
recent_tool_actions=previous_tool_actions,
|
recent_tool_actions=previous_tool_actions,
|
||||||
function_name=function_name,
|
function_name=function_name,
|
||||||
arguments=arguments,
|
arguments=arguments,
|
||||||
|
risk_markers=permission_eval.get("risk_markers") if isinstance(permission_eval, dict) else None,
|
||||||
sender=sender,
|
sender=sender,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -731,6 +732,7 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie
|
|||||||
recent_tool_actions=previous_tool_actions,
|
recent_tool_actions=previous_tool_actions,
|
||||||
function_name=function_name,
|
function_name=function_name,
|
||||||
arguments=arguments,
|
arguments=arguments,
|
||||||
|
risk_markers=permission_eval.get("risk_markers") if isinstance(permission_eval, dict) else None,
|
||||||
sender=sender,
|
sender=sender,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -183,7 +183,11 @@
|
|||||||
<span>权限:{{ currentPermissionLabel }}</span>
|
<span>权限:{{ currentPermissionLabel }}</span>
|
||||||
<span class="permission-switcher__caret" :class="{ open: permissionMenuOpen }">›</span>
|
<span class="permission-switcher__caret" :class="{ open: permissionMenuOpen }">›</span>
|
||||||
</button>
|
</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">
|
||||||
<div class="permission-switcher__group-title">权限</div>
|
<div class="permission-switcher__group-title">权限</div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -943,6 +943,7 @@ body[data-theme='dark'] {
|
|||||||
/* 手机端审批面板样式 - 与电脑端保持一致 */
|
/* 手机端审批面板样式 - 与电脑端保持一致 */
|
||||||
.mobile-approval-overlay {
|
.mobile-approval-overlay {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
justify-content: center !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-approval-overlay .mobile-panel-sheet--approval {
|
.mobile-approval-overlay .mobile-panel-sheet--approval {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user