8.4 跨范围审批集成
概述
跨范围审批是工作区管理与 HITL 审批机制的集成点。当一个操作的目标路径不在 `WorkspaceScope.root` 内时,根据 `ScopePolicy` 的不同,Agent 可能直接拒绝、自动执行、或触发审批。本章通过端到端示例展示不同策略下的行为差异。
策略行为矩阵
路径在 scope 内 路径在 scope 外
ScopePolicy ───────────────────── ──────────────────────────
AllowAll 自动执行 自动执行
ApproveOutside 自动执行 触发审批(ApprovalRequiredTool)
DenyOutside 自动执行 工具级拒绝(ToolResult::error)
端到端示例:read_file("/etc/passwd")
假设工作区设置为 `/workspace`,Agent 被要求读取 `/etc/passwd`。以下追踪三种策略下的完整代码路径。
场景:工作区设置
// 工作区根路径:/workspace
// 目标路径:/etc/passwd(绝对路径,显然不在 /workspace 下)
let scope = WorkspaceScope::new("/workspace", "dev-workspace")
.with_policy(/* AllowAll | ApproveOutside | DenyOutside */);
策略 1: AllowAll
sequenceDiagram
participant Agent as ChatClientAgent
participant FICC as FunctionInvokingChatClient
participant Tool as ReadFile
participant PG as path_guard
Agent->>FICC: run(messages)
FICC->>FICC: LLM 返回 tool_call(read_file, "/etc/passwd")
FICC->>FICC: 检查 requires_approval()
Note right of FICC: AllowAll → 工具未被 ApprovalRequiredTool 包装<br/>requires_approval() → false
FICC->>Tool: execute({path: "/etc/passwd"})
Tool->>PG: resolve_safe("/workspace", "/etc/passwd", Some("/workspace"))
PG-->>Tool: (/etc/passwd, OutsideScope)
Tool->>Tool: ScopePolicy::AllowAll → 不拒绝
Tool-->>FICC: ToolResult { ok: true, data: {content: "...", scope: "outside_workspace"} }
FICC->>FICC: 追加 assistant + tool 消息
FICC->>FICC: 继续 LLM 循环
**关键代码路径**:工具中 `ScopePolicy::AllowAll` 检查通过,直接执行。
// 工具内部(伪代码)
let (resolved, scope_status) = resolve_safe(base_dir, path, scope_root)?;
// AllowAll → 不拦截,直接执行
let content = std::fs::read_to_string(&resolved)?;
Ok(ToolResult::success(json!({
"scope": scope_status.to_label(), // "outside_workspace"
"content": content,
})))
策略 2: ApproveOutside(推荐)
sequenceDiagram
participant Agent as ChatClientAgent
participant FICC as FunctionInvokingChatClient
participant Tool as ReadFile<br/>(ApprovalRequiredTool)
participant PG as path_guard
participant Caller as 调用方
Agent->>FICC: run(messages)
FICC->>FICC: LLM 返回 tool_call(read_file, "/etc/passwd")
FICC->>FICC: 检查 requires_approval()
Note right of FICC: ApproveOutside → 工具被 ApprovalRequiredTool 包装<br/>requires_approval() → true
FICC-->>Caller: ToolApprovalRequest {<br/> call_id: "call_1",<br/> name: "read_file",<br/> arguments: {path: "/etc/passwd"},<br/> description: "读取文件内容"<br/>}
FICC-->>Caller: Finish(AwaitingApproval)
Note over Caller: 等待用户审批
Caller->>Caller: 用户批准 → ToolApprovalResponse {<br/> call_id: "call_1",<br/> approved: true<br/>}
Caller->>Agent: run(messages, options.with_tool_approval_responses([response]))
Agent->>FICC: run(messages + approval_responses)
FICC->>FICC: 审批恢复:pending = assistant(tool_calls) 中的 call_1
FICC->>FICC: approved → 执行工具
FICC->>Tool: execute({path: "/etc/passwd"})
Tool->>PG: resolve_safe("/workspace", "/etc/passwd", Some("/workspace"))
PG-->>Tool: (/etc/passwd, OutsideScope)
Tool-->>FICC: ToolResult { ok: true, data: {...} }
FICC->>FICC: 追加 assistant + tool(result) 消息
FICC->>FICC: 清空 approval_responses,继续 LLM 循环
**关键**:在 `ApproveOutside` 策略下,审批发生在两个层面:
1. **Provider 层**:`WorkspaceContextProvider::add_tool()` 将工具包装为 `ApprovalRequiredTool` 2. **执行层**:`FunctionInvokingChatClient` 检测到 `requires_approval() == true`,触发审批流程
工具本身不感知审批 —— 它只负责执行业务逻辑和返回 `ScopeStatus`。审批决策完全由 HITL 基础设施处理。
策略 3: DenyOutside
sequenceDiagram
participant Agent as ChatClientAgent
participant FICC as FunctionInvokingChatClient
participant Tool as ReadFile
participant PG as path_guard
Agent->>FICC: run(messages)
FICC->>FICC: LLM 返回 tool_call(read_file, "/etc/passwd")
FICC->>FICC: 检查 requires_approval()
Note right of FICC: DenyOutside → 工具未被 ApprovalRequiredTool 包装<br/>requires_approval() → false
FICC->>Tool: execute({path: "/etc/passwd"})
Tool->>PG: resolve_safe("/workspace", "/etc/passwd", Some("/workspace"))
PG-->>Tool: (/etc/passwd, OutsideScope)
Tool->>Tool: ScopePolicy::DenyOutside + OutsideScope
Tool-->>FICC: ToolResult {<br/> ok: false,<br/> error: "Access denied: '/etc/passwd' is outside workspace 'dev-workspace'"<br/>}
FICC->>FICC: 追加 assistant + tool(error) 消息
FICC->>FICC: LLM 看到错误,可调整行为或告知用户
**工具级拒绝代码**:
// 伪代码:工具内部的 DenyOutside 检查
if let Some(scope) = &self.scope {
if scope.policy == ScopePolicy::DenyOutside
&& matches!(scope_status, ScopeStatus::OutsideScope)
{
return Ok(ToolResult::error(format!(
"Access denied: '{}' is outside workspace '{}'",
path, scope.name
)));
}
}
// 否则继续执行
注意:`DenyOutside` 在工具级别直接返回错误,不做审批。这不同于 `ApproveOutside` 策略,后者通过 `ApprovalRequiredTool` 在工具执行前(Provider 层)就先触发审批。
策略选择指南
graph TD
A[你需要工作区管理吗?] -->|否| B[AllowAll]
A -->|是| C{Agent 是否可能<br/>需要访问工作区外?}
C -->|是,但需审批| D[ApproveOutside]
C -->|是,无需审批| B
C -->|否,严格限制| E{拒绝时<br/>如何处理?}
E -->|LLM 自行调整| F[DenyOutside]
E -->|需要人工介入| D
style D fill:#4CAF50,color:white
style F fill:#f44336,color:white
style B fill:#2196F3,color:white
AllowAll 适用场景
- 本地开发环境 - Agent 仅在受信任的 Docker 容器内运行 - 工作区概念本身不适用(如纯对话 Agent)
ApproveOutside 适用场景(推荐默认)
- 生产环境 - 需要审计的 CI/CD 流程 - 用户直接交互的 Agent 应用 - 需要渐进式信任建立的场景
DenyOutside 适用场景
- 高度受控的沙箱环境 - 自动化测试 Agent - 代码审查 Agent(只读操作) - 安全敏感的操作场景
路径白名单
通过 `WorkspaceScope.properties`,可以实现更细粒度的控制:
let scope = WorkspaceScope::new("/workspace", "controlled")
.with_policy(ScopePolicy::DenyOutside)
.with_property("path_allowlist", serde_json::json!([
"/etc/hostname", // 允许读取
"/usr/share/zoneinfo", // 允许读取
]));
工具可以在执行逻辑中检查 `path_allowlist`:
// 伪代码:白名单检查
if scope_status == ScopeStatus::OutsideScope {
let allowlist: Vec<String> = scope.properties
.get("path_allowlist")
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_default();
if !allowlist.iter().any(|allowed| resolved.starts_with(allowed)) {
if scope.policy == ScopePolicy::DenyOutside {
return Ok(ToolResult::error("Access denied"));
}
}
}
归纳
跨范围审批将工作区管理(第 8 章)与 HITL 审批(第 7 章)无缝集成:
| 策略 | 级别 | 机制 | 用户感知 | |------|------|------|---------| | `AllowAll` | — | 无限制 | 透明 | | `ApproveOutside` | Provider 层 | `ApprovalRequiredTool` 包装 → HITL 审批 | 审批提示 | | `DenyOutside` | 工具层 | `ToolResult::error` 拒绝 | 错误消息(LLM 看到并调整) |
选择策略时,核心考量是:**你希望 Agent 在执行越界操作时,是自动执行(AllowAll)、需要审批(ApproveOutside),还是彻底禁止(DenyOutside)?**