04:工具执行与安全边界:模型提出动作以后发生了什么
跟踪一次 ToolCall 从模型输出到真实执行的全过程,拆解 Schema 校验、权限裁决、生命周期 Hook、Handler 与结果回流构成的安全管线。
04:工具执行与安全边界:模型提出动作以后发生了什么
上一篇介绍了 Agent 怎样形成行动:Skill 给出任务方法,Prompt 提供现场,Plan 提供当前步骤,Tool 提供真实动作。
当模型生成一个 ToolCall 时,它表达的是“我希望执行这项操作”。实际文件读取、统计和写入由 Runtime 完成。
这中间需要一条稳定管线,把模型输出转换成可校验、可授权、可记录的执行结果。
RunLens 把一次工具调用拆成契约校验、权限裁决、生命周期 Hook、Handler 执行和结果回流。

模型输出 ToolCall 只是提出动作
模型调用工具时会给出:
- 工具名称。
- 参数对象。
- 可选 call_id。
例如写反馈分析文件:
{
"name": "write_json_file",
"arguments": {
"artifact_name": "feedback_analysis.json",
"content": {
"items": [],
"failed_batches": []
}
}
}
这段 JSON 仍然来自模型输出。字段可能缺失,工具名可能拼错,文件路径也可能越过当前 Run。
ToolExecutor 会把这次请求放进固定执行链:
ToolCall
→ ToolRegistry
→ Input Schema
→ PermissionGuard
→ PreToolUse Hook
→ Handler
→ Output Schema
→ PostToolUse Hook
→ ToolResult
每一层都只解决一个明确问题。
ToolDefinition 把动作变成明确契约
Plugin 注册的每个工具都对应一个 ToolDefinition。
它包含:
| 字段 | 作用 |
|---|---|
name | 模型使用的工具名 |
description | 告诉模型工具适合做什么 |
handler | 执行动作的 Python 函数 |
input_schema | 校验模型参数 |
output_schema | 校验 Handler 返回值 |
permission_requirements | 声明文件操作和内容大小检查 |
ToolRegistry 在 Runtime 初始化时注册 Plugin Tools,并拒绝重复名称。
模型看到的是 Tool Spec,Runtime 保存的是完整 ToolDefinition。两者共享工具名和输入结构,Handler 与权限只存在于服务端。
第一道检查:Input Schema
ToolExecutor 找到工具后,首先调用 Input Schema。
RunLens 的轻量 Schema 支持:
- object、array、string、number、integer、boolean、null。
- required fields。
- additional properties。
- enum。
- 字符串长度。
- 数值范围。
- 嵌套对象和数组。
write_json_file 要求:
artifact_name: string
content: any
两个字段都属于 required。
模型漏掉 content 时,Schema 会返回带路径的错误:
arguments.content: field is required
ToolExecutor 将结果标记为:
status = failed
error_code = input_validation_failed
retryable = true
Handler 将在后续检查通过后执行,此时工作区保持原状。
第二道检查:PermissionGuard
参数通过 Schema 后,ToolExecutor 会把工具名、参数和 Permission Requirements 交给 PermissionGuard。
权限检查分成三个部分。
工具白名单
当前工具必须位于 PermissionPolicy.allowed_tools。
使用 Skill 时,白名单来自 Skill 的 required tools。模型实际看到的工具和 Runtime 允许的工具使用同一能力快照。
路径边界
PermissionRequirement 会从参数解析真实目标路径。
例如 write_json_file 的目标是:
runs/{run_id}/artifacts/{artifact_name}
Guard 会把路径解析成绝对路径,再判断它是否位于允许的 write roots。
默认权限大致是:
read: data/、当前 Run
write: 当前 Run
Feedback Analysis Skill 将写入范围进一步缩小到当前 artifacts/。
大小限制
读文件时会检查文件大小,写文件时会计算内容的 UTF-8 字节数。
当前默认限制:
| 类型 | 上限 |
|---|---|
| 单文件读取 | 5,000,000 bytes |
| 单次写入内容 | 2,000,000 bytes |
这可以避免一次工具调用把过大的内容直接带入内存和工作区。
.env 和路径逃逸怎样处理
RunLens 对 .env 有单独禁止规则。
路径检查会先规范化 Windows 和 POSIX 分隔符,再查看路径组成部分。目标路径中出现 .env 时,Guard 直接返回 forbidden path。
对于 ../、绝对路径和其他 Run,系统会使用 Path.resolve() 得到最终位置,然后判断它是否属于允许根目录。
例如:
runs/current/artifacts/../../other-run/secret.json
规范化后指向另一个 Run,因此权限判断返回 denied。
这里的判断依据是最终文件位置,路径字符串的表面写法只参与解析。
第三道检查:PreToolUse Hook
权限通过以后,ToolExecutor 触发 PreToolUse。
当前最重要的 Pre Hook 是 PlanProgress。
它会检查:
- 当前 Plan Step 是哪一步。
- 当前工具是否位于该步骤 allowed tools。
- Plan 是否已经全部完成。
Feedback Analysis 的第一步只允许 read_feedback_csv。如果模型直接调用 write_markdown_file,PlanProgress 会返回 blocked:
tool write_markdown_file is not allowed for current plan step step_1
ToolExecutor 把结果转换为:
status = blocked
error_code = pre_hook_blocked
权限和 Plan 的作用不同:
- Permission 判断动作是否位于安全边界。
- Plan 判断动作是否适合当前任务步骤。
Handler 执行真实动作
通过前面三层检查后,ToolExecutor 才会调用 Handler。
Handler 可以:
- 读取 CSV。
- 提取 Evidence。
- 统计分类。
- 写入 JSON。
- 写入 Markdown。
- 列出当前 Run 文件。
以 write_json_file 为例,Handler 会:
- 检查 artifact_name 只包含文件名。
- 检查扩展名为
.json。 - 使用
RunWorkspace.write_json()写入artifacts/。 - 返回实际路径和 artifact_name。
文件写入使用 UTF-8,JSON 会稳定缩进并排序 key。稳定输出有利于测试、diff 和后续读取。
Handler 抛出异常时,ToolExecutor 会生成 handler_failed ToolResult,并继续走统一记录流程。
Output Schema 检查 Handler 返回值
Tool 的输入来自模型,输出来自 Handler。两端都需要契约。
write_json_file 返回:
{
"path": "...",
"artifact_name": "feedback_analysis.json"
}
Output Schema 要求两个字段都存在,并关闭 additional properties。
输出校验失败时,结果标记为 output_validation_failed。这类问题通常意味着 Handler 实现和 Tool Contract 发生了偏差。
Input Schema 主要帮助模型修正参数,Output Schema 主要帮助开发阶段发现工具实现问题。
PostToolUse 记录结果和推进 Plan
Handler 完成后,ToolExecutor 触发 PostToolUse。
PlanProgress 会根据结果更新当前 Step:
- success:记录成功工具,重新检查完成条件。
- failed:把当前 Step 标记为 failed。
PostToolUse 属于 Observer。它负责观察和更新状态,已经发生的文件写入会保留。
最终产物结构有问题时,Run 会在 Stop Validation 阶段失败,文件仍然留在工作区供排查。
ToolResult 怎样回到模型
ToolExecutor 最终返回统一 ToolResult:
tool_name
status
output
error_message
error_code
retryable
AgentLoop 会把它序列化成 observation,再创建 role=tool 消息。
成功结果让模型获得新事实。例如 CSV 工具会返回 rows 和 row_count。
失败结果同样会进入下一轮。例如参数缺失时,模型会看到精确错误路径,并有机会重新生成参数。
工具输出过长时,Runtime 会给模型发送截断预览,同时记录 observation truncation 数量。这样可以控制模型上下文长度。
一次写文件调用的完整过程
把前面内容串起来:
模型请求 write_json_file
→ Registry 找到 ToolDefinition
→ Schema 校验 artifact_name 和 content
→ Permission 解析 artifacts/feedback_analysis.json
→ Guard 确认路径和内容大小
→ PlanProgress 确认当前步骤允许写 JSON
→ Handler 通过 RunWorkspace 写文件
→ Output Schema 校验 path 和 artifact_name
→ PostToolUse 更新 Plan
→ ToolResult 写入事件并返回模型
这条管线把模型的自然语言判断转换成了一次结构化、可授权、可记录的真实操作。
当前边界
Tool Handler 在当前 Python 进程中同步运行。
Run timeout 会限制 Agent Loop 和 Provider 请求。一个已经进入 Handler 的阻塞操作仍然需要 Handler 自身控制耗时。
因此 RunLens 当前的工具设计强调:
- Handler 保持短小。
- 文件操作确定且可测试。
- 网络访问留给受控 Provider。
- 大任务拆成多个工具步骤。
更强的工具隔离可以在后续使用子进程、任务队列或外部执行器实现。
小结
模型生成 ToolCall 后,RunLens 依次完成 Registry、Input Schema、Permission、Pre Hook、Handler、Output Schema 和 Post Hook。
每一层都负责一种明确风险:参数、能力、路径、任务顺序、实现结果和状态同步。ToolResult 再把真实环境反馈带回模型,形成下一轮判断依据。
下一篇继续看整个 Agent Loop 怎样控制节奏:Run State 如何变化,Provider 和工具失败怎样恢复,重复调用、超时和完成条件怎样让任务可靠收束。