feat(v3): T28 操作审批流(dsh 式 allow-once/deny,fail-closed)
- ToolLoop 增 approval_hook:工具执行前挂起等待用户裁决,拒绝/异常折叠为
失败结果回喂模型(可改道),fail-closed
- 策略 agent.approval_policy:off | dangerous(写/编辑/命令询问,只读放行,默认)| all;
approval_timeout_s 超时自动拒绝(轮询实现,规避 portal 循环下 wait_for 定时器不可靠)
- POST /agent/{id}/approve 裁决端点;approval_request/decided 事件对进 SSE 与审计
- 前端:运行中审批卡(工具名+参数预览+拒绝/允许一次),composer 审批策略 chip
- 测试 +8(策略矩阵/拒绝回喂/允许执行/fail-closed/超时/端点分支),全量 289 passed
This commit is contained in:
@@ -414,6 +414,7 @@ class ToolLoop:
|
||||
on_event: Optional[Callable[[Dict[str, Any]], None]] = None,
|
||||
result_preview_chars: int = MAX_RESULT_CHARS,
|
||||
emit_final: bool = True,
|
||||
approval_hook: Optional[Callable[[str, Dict[str, Any]], Awaitable[bool]]] = None,
|
||||
):
|
||||
self.tools = tools
|
||||
self.chat_fn = chat_fn
|
||||
@@ -422,6 +423,8 @@ class ToolLoop:
|
||||
self.on_event = on_event
|
||||
self.result_preview_chars = result_preview_chars
|
||||
self.emit_final = emit_final # 两级模式内层循环置 False,由外层统一收尾
|
||||
# 审批门卫(D9):执行工具前调用,返回 False = 用户拒绝(可选;缺省跳过审批)
|
||||
self.approval_hook = approval_hook
|
||||
|
||||
def _emit(self, ev: Dict[str, Any]) -> None:
|
||||
if ev.get("type") == "final" and not self.emit_final:
|
||||
@@ -500,6 +503,24 @@ class ToolLoop:
|
||||
for c in calls:
|
||||
self._emit({"type": "tool_call", "round": round_no,
|
||||
"name": c["name"], "arguments": c["arguments"]})
|
||||
# 审批门卫(D9):按策略挂起等待用户裁决;拒绝/超时折叠为失败结果回喂
|
||||
if self.approval_hook is not None:
|
||||
allowed = False
|
||||
try:
|
||||
allowed = await self.approval_hook(c["name"], c["arguments"])
|
||||
except Exception as e:
|
||||
self._emit({"type": "approval_decided", "round": round_no,
|
||||
"id": "", "allowed": False,
|
||||
"note": f"审批流程异常: {type(e).__name__}"})
|
||||
if not allowed:
|
||||
denied = {"ok": False,
|
||||
"error": "用户拒绝执行该操作(如需执行请调整审批策略或换一种做法)"}
|
||||
preview = json.dumps(denied, ensure_ascii=False)
|
||||
self._emit({"type": "tool_result", "round": round_no,
|
||||
"name": c["name"], "ok": False, "preview": preview})
|
||||
messages.append({"role": "tool", "tool_call_id": c["id"],
|
||||
"content": preview})
|
||||
continue
|
||||
result = self.tools.execute(c["name"], c["arguments"])
|
||||
preview = json.dumps(result, ensure_ascii=False)
|
||||
if len(preview) > self.result_preview_chars:
|
||||
|
||||
Reference in New Issue
Block a user