Agent 工具审批不止一个按钮:用 v0.22.1 复盘入口、恢复和副作用

0 阅读18分钟

“工具被批准了,接下来就能放心执行”常把三件事混在一起:输入是否可检视、恢复状态是否一致、执行环境是否受限。下面用 Agents SDK v0.22.1 的历史修复逐条拆开,不把官方补丁或伪代码当成项目实测。

升级 Agent SDK 时,最容易漏掉的不是“程序是否还能启动”,而是一次看起来合法的工具调用,在暂停、恢复、重试或切换执行环境后,是否仍然属于原来的调用者、原来的参数和原来的副作用边界。

OpenAI Agents SDK Python v0.22.1 的 Release 变化,正好把这条链路拆成了几个必须分别验收的契约:local MCP Server 转换工具的 Server 级 Guardrail、可调用审批策略遇到缺失或空字符串参数时的 fail-closed、审批恢复和 Session 持久化、opt-in 的本地 Unix 环境隔离、Shell 工作目录以及取消清理。它们共同说明一个判断:工具审批不是在按钮上点一次“允许”,而是从入口到执行器、从暂停到恢复、从状态写入到副作用落地的一组连续核对。

本文是历史版本的运行契约复盘,不是当前最新版推荐。v0.22.1 发布于 2026-09-08T09:18:00Z(北京时间 9 月 8 日 17:18);2026 年 10 月 5 日采集并核验官方来源,10 月 6 日冻结本次返工版本。本文只做静态整理,目的是把升级时应补的回归矩阵写清楚。文中示例是框架无关的伪代码和测试思路,不代表本项目已经接入、升级或运行该 SDK;除明确标出的官方行为外,矩阵、指纹和观测要求都是工程建议,不是 SDK 已保证的功能。

一、为什么“冒烟测试通过”仍然不够

一个最小的冒烟测试通常只做三件事:启动 Agent、调用一个工具、得到一个响应。这能发现导入错误、明显的类型错误和最短路径上的网络故障,却无法覆盖以下情况:

风险最短路径为什么可能通过需要补的证据
local MCP Server 转换出的工具未全部经过 Guardrail只测了第一个工具,其他工具仍可绕过策略同一 local Server 暴露的每个 converted tool 都经过同一策略
可调用审批策略遇到缺失或空字符串参数测试使用了完整参数,未覆盖 None 或 ""该审批路径对不可检视输入直接中断;显式 {} 是否拒绝须另验 schema 和业务约束
审批在恢复后被复用新 Run 没有暂停,审批只在内存中存在恢复时重新核对调用者、工具和最终参数
Session 写入失败后仍继续调用测试存储一直成功,未注入半途失败停止后续模型/工具调用,并恢复或对账到一致状态;是否回滚按后端契约证明
本地执行环境被误当成安全边界只验证命令能跑,没有观察工作目录、凭据和网络环境 allowlist、目录、取消与清理记录可回读

这几类问题都有一个共同特征:系统表面上“没报错”,但运行契约已经漂移。升级门禁应该先定义失败终态,再去验证成功路径。

二、v0.22.1 的变化应如何读

官方 Release 提到的变化可以按四个层次理解。第一层是工具入口:可为 local MCP servers 配置输入、输出 Guardrail,并统一施加到每个 converted tool;可调用 needs_approval 策略遇到缺失或空字符串参数时 fail-closed。第二层是暂停恢复:特定序列化审批恢复路径修复了当前响应 item 的归属;失败的 resumed Session append 在进一步模型调用前对账,已完成工具不重跑。第三层是执行环境:新增 opt-in 的 Unix-local 环境隔离(默认仍继承宿主环境),并包含 Shell 命令列表工作目录和取消清理修复。第四层是工程验收:上面这些 SDK 修复各有适用范围,不能推导出通用业务授权、事务或沙箱保证;项目仍需用自己的回归样例证明。

因此,升级清单不能只写“把版本号改成 0.22.1”。它至少要回答四个问题:

  1. 每个 local MCP Server 转换出的工具在到达执行器前是否都经过相同的策略?
  2. 恢复时继承的到底是哪一次批准、哪一组参数和哪一个调用主体?
  3. 状态写入失败时,系统是否会停止后续模型调用和工具副作用,并恢复或对账到一致状态?
  4. 本地执行环境的目录、变量、取消和清理是否有可观察证据?

三、第一道门:Server-wide Guardrail 不能只测一个工具

local MCP Server 级 Guardrail 的价值,是让同一 Server 下转换出的工具共享输入或输出检查。PR #4632 区分了二者:输入检查沿既有工具执行和审批生命周期运行;输出检查面对的是转换后的 SDK ToolOutput,不是原始 MCP CallToolResult。输出拒绝发生在工具已有结果之后,不能被写成“保证工具尚未执行”或“撤销外部副作用”。但“配置了 Server 级 Guardrail”不等于“所有路径都已经被项目证明覆盖”。工具列表变化、动态注册、错误分支和恢复路径都应分别验收。

建议先建立一份运行时工具清单,至少包含工具名、输入 schema、是否有外部副作用、所属 Server 和当前策略。然后对每个工具运行允许调用、输入策略拒绝、输出策略拒绝和参数无效四类样例。输入拒绝样例的关键断言不是返回了一条错误消息,而是下游执行次数为零,外部系统没有新增记录,审计日志能定位到拒绝发生在执行器之前;输出拒绝必须单独记录已发生的工具执行和副作用,不能套用“执行次数为零”。

可以把断言写成下面这种框架无关的形式。零副作用断言只适用于输入 Guardrail 拒绝;输出 Guardrail 拒绝必须单独核对已发生的执行和副作用。

# 输入拒绝专用 fixture:输入未通过,调用不能到达执行器。
input_result = await run_tool(
    name="send_email",
    arguments=input_rejected_payload,
)
assert input_result.status == "rejected"
assert input_result.rejection.stage == "input_guardrail"
assert side_effect_counter("send_email") == 0
assert audit.last.stage == "input_guardrail"

# 输出拒绝发生在工具返回结果之后,不能推断执行次数为零。
output_result = await run_tool(
    name="send_email",
    arguments=output_rejected_payload,
)
assert output_result.status == "rejected"
assert output_result.rejection.stage == "output_guardrail"
assert execution_counter("send_email") == 1
assert side_effect_counter("send_email") == observed_side_effect_count

如果同一个 local Server 既有只读查询,也有写入、发送或删除工具,不能因为只读工具通过就把整组策略标成通过。策略应按工具能力分层,但覆盖性要按 Server 的完整 converted tools 暴露面验收。

还要留意输入 Guardrail 的边界。输入门禁回答的是“这次调用能否到达执行器”,不能代替工具自身的 schema 校验、业务权限、资源归属、租户隔离和目标对象存在性检查。输出检查则是另一条结果检查路径。Server-wide 策略也不自动扩展到远程 MCP Server 或另一个本地 Server;跨 Server 的统一授权仍需要应用层或网关层显式设计。

四、第二道门:空参数必须保持 fail-closed

这里的 “empty tool arguments” 不能简单翻译为“空对象”。PR #4545 针对可调用 needs_approval 策略检视 function-tool 参数的路径:工具参数串缺失(None)或为空字符串 "" 时,旧逻辑中的 arguments or "{}" 会把它们转换成 {},导致依赖参数内容的审批策略可能返回“不需审批”,工具继续执行。现在这些不可检视输入会直接中断,不进入该审批 predicate,也不执行工具;无效 JSON 已通过此前修复保持 fail-closed。同一解析 helper 用于 Runner 工具执行和 Realtime session 工具调用;不能将这个修复扩大成所有工具输入的通用校验保证。

需要特别区分的是,显式传入字符串 "{}" 仍是合法空对象。它之后是否拒绝,取决于工具 schema 是否声明 required 字段、字段是否有 minLength 等约束,以及业务层是否允许空参数。不能把该修复表述成“官方补丁会直接拒绝所有空对象”。

至少准备以下互相独立的样例:

  • 参数串缺失(None);
  • 参数串为空字符串;
  • 显式传入 "{}",而工具声明需要 path,应由 schema 校验拒绝;
  • 调用传入 {"path": ""},但空字符串不符合业务约束;
  • 调用传入未知字段,且未知字段可能改变执行目标;
  • schema 中的必填字段类型不匹配。

每个样例都要区分“输入不可检视”“schema 验证错误”和“执行错误”。前两类错误发生在工具进程启动前,不能等工具启动后再由业务代码兜底。错误消息可以告诉调用方缺什么,但不能回显密钥、完整凭据或内部路径。

本文确指 None 和 "",不把只含空格的字符串、显式 {} 或其他数据类型统称为同一个补丁用例。空参数的回归还应和审批恢复组合起来。作为应用层验收建议,一个先被批准、随后参数串被清空的调用不能沿用旧批准;一个先被拒绝、随后补齐参数的调用应重新计算调用身份并重新走审批。这些组合需要项目实测,本文没有运行它们。

五、第三道门:审批必须绑定“这一次调用”

暂停恢复最危险的误区,是把审批看成一个布尔值:approved = True。官方 PR #4613 的修复范围是配置了输出 Guardrail 的后续轮次序列化审批恢复:以 live item identity 保存 schema 1.17 的当前响应归属,恢复 processed 和 interruption identity,并让 schema 1.16 或不一致检查点在获批工具执行前 fail-closed。这里的 item 归属不能翻译成“已验证业务用户或租户”。应用层另外需要保存一次具体调用的身份,包括调用主体、工具标识、规范化后的最终参数、目标资源、运行版本、Session 或检查点版本,以及批准的有效期和撤销状态。下表与指纹示例属于应用设计建议,不代表这些字段全部由 SDK 自动维护。

可以用稳定摘要表达“工具 + 参数”的一部分身份:

import json
from hashlib import sha256

def call_fingerprint(tool: str, arguments: dict) -> str:
    body = json.dumps(
        {"tool": tool, "arguments": arguments},
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
    )
    return sha256(body.encode()).hexdigest()

这里 sort_keys=True 只解决字典键顺序导致的伪变化,不能替代调用主体、资源租户、有效期和撤销状态。工具名称变化、目标资源变化、任何有效参数变化,都应让指纹发生变化。生产实现还应防止不同类型在规范化时被错误折叠,例如字符串 "1" 不应和数字 1 被当作同一参数。

审批恢复的最小回归矩阵如下:

场景预期
暂停后原调用完整恢复可恢复,调用身份和参数摘要不变
恢复前工具名变化必须重新审批
恢复前目标资源变化必须重新审批
恢复前任一有效参数变化旧审批失效
恢复时调用主体变化旧审批失效
审批已过期或被撤销旧审批失效
审批记录损坏或缺字段fail-closed,不继续执行

注意“批准后参数变化”不仅包括用户直接修改,也包括模型在恢复前重新生成参数、服务端补默认值、别名解析和资源重定向。最终送进执行器的参数才是验收对象。

六、第四道门:Session 写入与执行必须有一致性边界

Release 中关于 Session 写入和恢复失败的修复,提醒我们把持久化当成运行契约的一部分。假设审批已完成,系统准备把新的状态写入 Session;如果写入在中途失败,最危险的行为是继续请求模型或启动工具,因为内存中的状态和持久化状态已经不一致。PR #4630 的处理方式是保留失败的 resumed appends,在继续模型调用前完成恢复或对账,并且不重跑已经完成的工具。

PR #4630 明确要求恢复使用原后端、独占历史访问;handoff/terminal recovery 和分布式 exactly-once 不在修复范围。一个可靠的测试需要主动注入写入失败,随后观察模型调用次数、工具执行次数和 Session 内容。对“已完成工具、写入结果失败”的恢复样例,关键断言应是:对账前没有进一步模型调用,恢复不重跑已完成工具;已发生副作用计数保持原值,而不是变成零。对“工具尚未启动、先提交状态失败”的应用设计样例,则另外断言没有新的执行。具体是否通过替换回滚、追加补偿或其他方式恢复,需要由采用的后端契约证明,不能统一写成“必然回滚”。

如果业务确实需要重试,必须使用明确的幂等键,并说明重试的是状态提交还是外部副作用。不能用“再跑一遍”掩盖一次不确定的发送、扣款或写入。恢复后的检查点也应记录版本号,避免两个并发恢复分支互相覆盖。

可以把一次恢复拆成几个可观测阶段:

读取检查点 → 校验调用主体 → 校验工具与参数 → 校验审批有效期
→ 写入恢复状态 → 提交 Session → 允许模型继续 → 允许工具执行

这条顺序是建议的应用层门禁,不是 SDK 的统一执行时序;“工具已完成、结果写入失败”的恢复路径必须保留既有执行事实。任一阶段失败,都应停在该阶段并留下原因。只有一致性对账或恢复完成、必要的 Session 提交成功之后,才允许继续。日志中需要有检查点版本、调用指纹和阶段名称,但不要写入完整敏感参数。

七、第五道门:本地环境不是天然沙箱

Unix-local 环境、工作目录保留和取消清理为本地 Shell 执行提供了更清晰的契约,但只有环境隔离配置是这里讨论的 opt-in 能力,不能把所有工作目录和清理修复也叫作 opt-in。UnixLocalSandboxClient 默认仍继承完整宿主环境;设置 inherit_host_environment=False 才使用 SDK 的内置安全 allowlist,也可以通过 host_environment_allowlist 指定应用自己的精确变量集合。Manifest.environment 仍可覆盖所选宿主基线,HOME 固定到 workspace。继承策略属于受信任的运行时配置,用于新建和恢复 session,不序列化进 session state。

“本地”也不等于“没有权限边界”。同一用户权限下,工作目录可能含有凭据,环境变量可能带着令牌,网络仍可能可达,取消也可能留下子进程或 PTY。未启用环境隔离时,验收只能证明继承关系和风险可见,不能写成已经完成环境收紧。

回归时至少观察以下内容:

  • 工作目录是否是配置值,而不是进程启动目录的偶然继承;
  • 启用隔离时环境变量是否符合 allowlist;未启用时是否明确记录宿主环境继承;秘密是否通过专门的注入机制传递;
  • 取消后主进程、子进程和 PTY 是否都结束;
  • 依赖、临时文件和输出是否按约定清理;
  • 失败日志是否脱敏;
  • 网络、文件和凭据权限是否符合项目真正的隔离模型。

建议先使用无副作用命令验证目录、环境和取消,再为实际工具补充最小权限测试。不要用“命令执行成功”作为安全证据;安全证据应当是权限被限制、取消可观察、清理可回读。

八、把版本升级变成一张可执行矩阵

升级前可以先用这张表冻结验收范围:

维度最小样例通过证据
工具入口每个 local MCP converted tool 分别测允许、输入拒绝、输出拒绝输入拒绝时执行次数为零;输出拒绝记录既有执行,不声称副作用回滚
参数校验可调用审批路径的 None/""、无效 JSON;另外测 {} + required、类型错误、未知字段区分补丁行为和应用 schema/业务约束,错误脱敏
审批恢复原调用、工具变化、参数变化、主体变化只有完整匹配才继承批准
Sessionresumed append 失败及恢复;另测应用提交失败原后端和独占访问条件可证明;进一步模型调用前对账,已完成工具不重跑
本地环境默认继承、opt-in allowlist、目录、取消、PTY 清理默认值和启用后的隔离边界分别有可回读记录
并发恢复两个恢复分支争用同一检查点版本冲突可见,不静默覆盖
观测Trace、审计、失败日志能关联检查点和调用指纹,且无敏感泄露

矩阵应保存“样例输入、预期终态、实际证据、运行版本、执行时间和责任人”。只保存一个“测试通过”布尔值,会让下一次升级无法判断到底覆盖了什么。

九、适用边界:Release 说明不等于项目已验证

本文的事实范围很窄:只把 OpenAI Agents SDK Python v0.22.1 与 v0.22.0 官方 Release 中可核对的变化转成升级门禁建议。没有安装 SDK,没有升级锁文件,没有连接 MCP Server,没有调用真实模型,没有运行 Sandbox,也没有验证本项目的 Session 后端、业务授权或生产流量。

因此以下说法目前都不能成立:

  • “项目已经升级到 v0.22.1”;
  • “MCP 工具已经全部通过 Guardrail”;
  • “恢复审批已经在生产环境安全”;
  • “本地 Unix 环境可以替代容器或其他隔离方案”;
  • “官方 Release 已证明本项目没有越权风险”。

能够成立的说法是:官方 Release 暴露了需要回归的运行契约;上面的矩阵提供了进入真实升级和发布前验证的最小骨架。下一步若要进入实战,应在项目实际采用该 SDK 后,在自己的隔离测试环境加入“缺失/空字符串参数 + 已批准恢复 + Session 写入失败”的组合用例,并记录执行次数和最终状态。不能把其他 SDK 的本地模拟结果移植为本 SDK 的实测证据。

十、上线前检查清单

  • 记录 Agents SDK、MCP SDK 和运行环境的精确版本。
  • 列出每个 local MCP Server 的全部 converted tools、schema 和副作用等级;远程 Server 另行取证。
  • 每个工具按实际能力分别测输入拒绝、输出拒绝及 schema 错误;可调用审批路径另测 None/""、无效 JSON 和显式 {}。
  • 输入拒绝证明下游执行未启动;输出拒绝记录已经发生的工具执行和副作用。
  • 审批指纹绑定工具、规范化参数、调用主体和有效期。
  • 恢复时工具、资源、主体或参数变化都会使旧审批失效。
  • resumed Session append 失败时对账先于进一步模型调用,已完成工具不重跑;原后端和独占访问条件可证明。
  • 本地执行环境的默认继承关系、opt-in allowlist、工作目录、取消和清理可回读。
  • Trace 能关联检查点版本和调用指纹,日志不泄露敏感值。
  • 预发布证据明确写出“静态核验”与“实际运行”的边界。

官方来源