agent-Specialization/modules/auto_approval_service.py
JOJO 8d665ad6de feat(permission): add auto-approval mode with approval agent, UI flow, and docs sync
Implemented a new permission mode  between  and , including backend policy, runtime behavior, frontend display, and documentation updates.

Backend & policy changes:

- Added  to permission mode validation and persistence paths.

- Updated tool permission evaluation: workspace-local  direct pass, out-of-workspace requires approval.

- Kept  readonly-first flow and added auto-approval decision path when permission denial occurs.

- Added approval reason/decider support in tool approval manager.

- Improved rejection tool-content format to natural language: '工具调用被拒绝\n原因:...'

- Fixed permission-mode sync edge cases on conversation create/load and restart-first-switch behavior.

Approval agent subsystem:

- Added  and .

- Added lightweight auto-approval config at  (name/url/key/model/extra_params + timeouts).

- Added optional transcript debug switch in code and transcript output under logs/approval_agent.

- Aligned transcript saving toward cumulative messages format and captured reasoning/content/tool_calls/tool sequence.

Frontend changes:

- Added  option to personalization and permission menus.

- Reworked approval panel auto-review display into a dedicated block with simplified lines: start, command, final approve/reject + reason.

- Added delayed sidebar auto-close (10s) after approval resolved.

- Added stricter permission-switch verification and rollback on mismatch/error.

Docs updated:

- Synced AGENTS.md, README.md, and docs/host_sandbox_and_permission_model.md with new permission mode, approval-agent behavior, config, and current constraints (including docker caveat).
2026-05-11 17:55:27 +08:00

93 lines
3.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import json
from typing import Any, Dict, List, Optional
from modules.approval_agent import ApprovalAgent
from server.state import tool_approval_manager
def build_auto_approval_payload(
*,
web_terminal: Any,
recent_tool_actions: List[Dict[str, Any]],
function_name: str,
arguments: Dict[str, Any],
) -> str:
history = list(getattr(getattr(web_terminal, "context_manager", None), "conversation_history", []) or [])
user_input = ""
for msg in reversed(history):
if not isinstance(msg, dict) or msg.get("role") != "user":
continue
metadata = msg.get("metadata") or {}
if bool(metadata.get("is_auto_generated")):
continue
content = msg.get("content")
if isinstance(content, str) and content.strip():
user_input = content.strip()
break
recent = recent_tool_actions[-3:] if isinstance(recent_tool_actions, list) else []
lines = [
"请按照流程审批:",
f"用户输入:{user_input or '(空)'}",
"AI运行的前三步操作",
]
if recent:
for idx, row in enumerate(recent, start=1):
lines.append(f"{idx}. {row.get('tool_name')}: {json.dumps(row.get('arguments') or {}, ensure_ascii=False)}")
else:
lines.append("(无)")
lines += [
"请审核当前操作:",
f"{function_name}: {json.dumps(arguments or {}, ensure_ascii=False)}",
"仅判断危险性和越权风险,不判断任务必要性。",
]
return "\n".join(lines)
async def run_auto_approval(
*,
web_terminal: Any,
username: str,
approval_id: str,
conversation_id: Optional[str],
recent_tool_actions: List[Dict[str, Any]],
function_name: str,
arguments: Dict[str, Any],
sender,
) -> Dict[str, Any]:
def _progress(evt: Dict[str, Any]) -> None:
sender("auto_approval_progress", {"approval_id": approval_id, "progress": evt, "conversation_id": conversation_id})
def _manual_takeover() -> Optional[Dict[str, Any]]:
row = tool_approval_manager.get(approval_id)
status = (row or {}).get("status")
if status in {"approved", "rejected"}:
return {"decision": status, "item": row, "source": "manual"}
return None
_progress({"stage": "start", "message": "自动审批开始"})
payload = build_auto_approval_payload(
web_terminal=web_terminal,
recent_tool_actions=recent_tool_actions,
function_name=function_name,
arguments=arguments,
)
agent = ApprovalAgent(web_terminal=web_terminal)
result = await agent.review(payload_text=payload, progress_cb=_progress, cancel_check=_manual_takeover)
if not _manual_takeover():
decided = tool_approval_manager.decide(
approval_id=approval_id,
username=username,
decision=str(result.get("decision") or "rejected"),
reason=str(result.get("reason") or "").strip(),
decider="approval_agent",
)
out = {"decision": decided.get("status"), "item": decided, "source": "approval_agent"}
else:
out = _manual_takeover() or {"decision": "rejected", "reason": "人工接管失败"}
_progress({"stage": "done", "message": "自动审批结束", "decision": (out or {}).get("decision")})
return out