首页 项目 博客 简历 联系 English
返回列表
2026年7月5日 约 1,824 字 预计 5 分钟读完

04:工具执行与安全边界:模型提出动作以后发生了什么

跟踪一次 ToolCall 从模型输出到真实执行的全过程,拆解 Schema 校验、权限裁决、生命周期 Hook、Handler 与结果回流构成的安全管线。

#RunLens#Tool Calling#权限控制#Agent 安全

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 会:

  1. 检查 artifact_name 只包含文件名。
  2. 检查扩展名为 .json
  3. 使用 RunWorkspace.write_json() 写入 artifacts/
  4. 返回实际路径和 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 和工具失败怎样恢复,重复调用、超时和完成条件怎样让任务可靠收束。