8.5 声明式工作区配置与工具联动
概述
前四节通过 `AgentBuilder` + `WorkspaceContextProvider` 手工构建工作区感知的 Agent。从 v0.1.0 起,RAF 支持通过 YAML / JSON / TOML 声明式配置文件定义工作区边界,将工作区配置、工具选择、审批策略统一在一个文件中管理,无需修改 Rust 代码。
全链路:从 YAML 到 HITL 审批
下面是一个完整链路——声明式工作区 + 文件系统工具 + 跨范围审批:
flowchart LR
A["agent.yaml<br/>(contexts.workspace<br/>+ tools.file)"] --> B[DeclAgentBuilder]
B --> C[WorkspaceContextProvider<br/>自动创建]
B --> D[ToolResolver<br/>解析 file 工具]
C --> E["注入 system prompt<br/>(工作区边界)"]
D --> F[IScopeTool 实例<br/>(scope: None)]
E --> G[AgentBuilder]
F --> G
G --> H["ChatClientAgent<br/>(路径守卫 + 审批)"]
H --> I{路径在工作区内?}
I -->|是| J[直接执行]
I -->|否 + ApproveOutside| K["ToolApprovalRequest<br/>→ HITL 审批"]
I -->|否 + DenyOutside| L[ToolResult::error]
配置文件:agent.yaml
kind: prompt
name: workspace-agent
description: 工作区管理的全栈开发助手
model:
id: agnes-2.0-flash
provider: openai
connection:
kind: key
api_key: $AGNES_API_KEY
endpoint: https://apihub.agnes-ai.com/v1
instructions: |
你是一个工作区感知的开发助手。
重要规则:
- 所有文件操作在指定工作区内进行
- 工作区外路径在工具返回中会标注 outside_workspace
- 若操作被拒绝,重新评估方案
# ── 工作区声明 ──
contexts:
- kind: workspace
name: my-project
config:
root: /home/dev/myapp
policy: approve # 工作区外 → 触发审批
# ── 工具声明(description 由宏内置,无需手写)──
tools:
- kind: file
name: read_file
- kind: file
name: write_file
- kind: file
name: edit_file
- kind: file
name: list_files
- kind: file
name: search_file
- kind: file
name: find_files
- kind: file
name: run_command
maxToolRounds: 15
工具路由规则(关键设计)
当 `contexts` 中存在 `workspace` 声明时,`DeclAgentBuilder` 会**自动分类并路由工具**:
flowchart TD
A["tools: 全部声明式工具"] --> B{contexts 中<br/>有 workspace?}
B -->|否| C[所有工具 → AgentBuilder.with_tool()<br/>普通注册]
B -->|是| D{工具实现<br/>IScopeTool?}
D -->|是| E["→ WorkspaceContextProvider<br/> .add_tool_arc() 注册<br/> (scope注入 + 审批包裹)"]
D -->|否| F[→ AgentBuilder.with_tool()<br/>普通注册]
E --> G["运行时 on_invoking():<br/> ContextResult.tools 注入<br/> + system prompt 边界指令"]
F --> G
**路由规则**:
| 工具类别 | 是否实现 IScopeTool | 有 workspace 时的注册路径 | 获得的能力 | |---------|:---:|------|------| | `read_file`, `write_file`, `edit_file` | ✅ | `WorkspaceContextProvider.add_tool_arc()` | scope 注入 + 审批包裹 + 路径守卫 | | `list_files`, `inspect_file`, `make_directory` | ✅ | 同上 | 同上 | | `remove_path`, `move_file` | ✅ | 同上 | 同上(含源+目标双路径检测) | | `find_files`, `search_file` | ✅ | 同上 | 同上 | | `run_command` | ✅ | 同上 | scope 注入 + 审批包裹 | | `web_search`, `web_fetch` | ❌ | `AgentBuilder.with_tool()` | 普通注册,不受工作区约束 | | `function` (自定义) | ❌ | `AgentBuilder.with_tool()` | 普通注册 | | `custom` (工厂) | ❌ | `AgentBuilder.with_tool()` | 普通注册 | | `mcp` | ❌ | `AgentBuilder.with_tool()` | 普通注册 |
> **为什么 IScopeTool 必须走 workspace 路径?** 只有通过 `WorkspaceContextProvider::add_tool_arc()`(内部调用 `try_inject_scope()` + `ApprovalRequiredTool` 包裹),工具才能获得 `WorkspaceScope` 引用并受 `ScopePolicy` 控制。直接注册到 `AgentBuilder` 的工具无法感知工作区边界。
两步处理:scope 注入 + 审批包裹
`WorkspaceContextProvider::add_tool_arc()` 对每个 IScopeTool 工具执行两步处理:
1. **Scope 注入**:通过 `AsAny` 下转型检测工具类型,调用 `IScopeTool::create_scoped(scope)` 注入 `WorkspaceScope` 2. **审批包裹**:若 `ScopePolicy::ApproveOutside`,用 `ApprovalRequiredTool` 包装工具
// add_tool_arc 内部逻辑(框架源码节选)
pub fn add_tool_arc(&mut self, tool: Arc<dyn ITool>) {
let mut tool = tool;
// Step 1: scope 注入(检测 IScopeTool)
if let Some(scoped) = try_inject_scope(&tool, Arc::clone(&self.scope)) {
tool = scoped;
}
// Step 2: 审批包裹(按策略)
if self.scope.policy == ScopePolicy::ApproveOutside {
tool = Arc::new(ApprovalRequiredTool::new(tool));
}
self.tools.push(tool);
}
加载代码
use rust_agent_decl::DeclAgentBuilder;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// 一行代码:YAML → 可运行的 Agent
let agent = DeclAgentBuilder::new()
.from_yaml_file("agent.yaml")
.build()
.await?;
// 运行 Agent(与手工构建的 Agent 完全相同)
let session = agent.create_session();
let messages = vec![rust_agent_core::ChatMessage::user(
"读取 src/main.rs 的内容,并检查 /etc/hosts 是否存在"
)];
let mut stream = agent.run(messages, Some(session), None).await?;
// ... 消费流式响应 ...
Ok(())
}
策略行为对比
AllowAll(无限制)
contexts:
- kind: workspace
name: dev
config:
root: /home/dev/project
policy: read # read / allow / allow_all → AllowAll
- 工作区内外的所有文件操作直接执行 - 工具返回中 `scope` 字段为 `"workspace"` 或 `"outside_workspace"` - 仅用于信息标注,不拒绝任何操作 - **适用场景**:本地开发、受信任环境
ApproveOutside(推荐默认)
contexts:
- kind: workspace
name: production
config:
root: /opt/myapp
policy: approve # approve / ask / approve_outside → ApproveOutside
- 工作区内操作直接执行 - 工作区外操作 → 触发 `ToolApprovalRequest` 等待用户审批 - 审批通过后执行,拒绝则返回错误 - **适用场景**:生产环境、需要审计的场景
DenyOutside(严格限制)
contexts:
- kind: workspace
name: sandbox
config:
root: /tmp/sandbox-42
policy: deny # deny / restrict / deny_outside → DenyOutside
- 工作区内操作直接执行 - 工作区外操作 → 工具直接返回 `ToolResult::error("Access denied")` - LLM 看到错误后可调整行为或告知用户 - **适用场景**:沙箱环境、CI/CD、自动化测试
> **策略解析**:`DeclAgentBuilder` 通过 `ScopePolicy::from_config_str()` 解析 YAML 中的 `policy` 字段。支持的别名见 [8.1 工作区范围](./scope-overview.md#从配置字符串解析策略)。未知值会回退为 `DenyOutside`(安全优先,fail closed),并记录 `ERROR` 级别日志。
与 AgentBuilder 手工构建的对比
| 维度 | AgentBuilder 手工构建 | 声明式配置 | |------|---------------------|-----------| | 工作区定义 | `WorkspaceScope::new("/path", "name").with_policy(policy)` | YAML `contexts.workspace.config` | | 工具注册 | 逐个 `.add_tool(ReadFile { scope: None })` | YAML `tools` 数组,一行一个工具 | | Provider 构建 | `WorkspaceContextProvider::new(scope).add_tool(...)` | `DeclAgentBuilder` 自动构建 | | 修改配置 | 需要改 Rust 代码并重新编译 | 只需改 YAML 文件 | | 团队协作 | 需要 Rust 开发人员参与 | 非 Rust 开发人员也能修改配置 | | 调试 | `println!` / 日志 | YAML 语法检查 + 日志输出 |
混合模式:YAML 声明 + 代码注入
声明式配置覆盖了大部分场景,但对于需要特殊处理的工作区(如动态路径、运行时计算),可以使用混合模式:
let agent = DeclAgentBuilder::new()
.from_yaml_file("agent.yaml") // YAML 定义基础工作区
.with_context({
// 代码注入:运行时动态决定工作区路径
let dynamic_root = std::env::var("PROJECT_ROOT")
.unwrap_or_else(|_| "/default/path".into());
let scope = WorkspaceScope::new(dynamic_root, "dynamic-workspace")
.with_policy(ScopePolicy::ApproveOutside);
Arc::new(WorkspaceContextProvider::new(Arc::new(scope))
.add_tool(ReadFile::default())
.add_tool(WriteFile::default()))
})
.build()
.await?;
`with_context()` 注入的 provider 会与 YAML 中声明的 provider 合并,代码注入的 provider 排在后面,可以覆盖前面的行为。
故障排查
工作区配置未生效
检查 YAML 中 `policy` 字段的值是否正确:
# 查看 DeclAgentBuilder 日志输出
RUST_LOG=debug cargo run
输出中应包含类似的信息:
DEBUG rust_agent_decl: building workspace provider: root=/home/dev/myapp, policy=ApproveOutside
工具返回 scope 字段始终为 none
检查工具的 `scope` 字段是否正确初始化。声明式配置中的 `kind: file` 工具由 `ToolResolver` 创建时使用 `::default()`,`scope` 字段初始为 `None`。这意味着工具不会主动执行工作区裁剪——`path_guard.rs` 的 `resolve_safe()` 会根据 `base_dir` 参数判断。要启用完整的工作区感知,需配合 `WorkspaceContextProvider`(声明式 `contexts.workspace` 已自动处理这一点)。
下一步
- 了解路径守卫的底层实现 → [8.3 路径守卫与跨范围检测](path-guard.md) - 了解跨范围审批的端到端流程 → [8.4 跨范围审批集成](cross-scope-approval.md) - 查阅声明式配置的完整字段参考 → [10.5 配置字段完全参考](../10-macros-declarative/config-reference.md)