# AI 代理的 API 版本升级安全

API 版本升级可能在代理指令一字未改的情况下扩大其权限。危险的变化通常藏在看似无害的更新说明里：默认值发生变化、出现新端点、旧令牌获得兼容路径，或者响应开始包含旧版本省略的记录。

由人操作的集成有时能应对这种模糊性，因为人会注意到陌生界面，或在奇怪请求出现时停下来确认。自主编程代理不会自然地停顿。如果它能根据文档构造请求、检查错误并重试其他方案，那么任何新出现的可达操作都会成为它实际权限的一部分。

## 版本标签不能衡量权限

版本号描述的是 API 提供商的兼容性承诺，而不是代理实际经历的权限变化。在比较凭据升级前后的能力之前，应把每次 API 升级都视为一次权限审核。

语义化版本规范规定，当公共 API 发生不兼容变化时，应提升 MAJOR 版本。这能帮助库维护者判断调用方是否可能出错，但它并不表示 MINOR 版本不能新增管理端点、扩大默认过滤范围，或接受面向另一受众的访问令牌。这些变化都可能保持兼容，同时增加代理能够完成的事情。

这一点很重要，因为团队经常问错问题：「我们的代码还能运行吗？」真正能保护账户的问题是：「这个现有代理现在能对哪些资源成功执行哪些操作？使用的是哪种凭据？」

一次 API 升级有四个独立的影响面：

- 请求兼容性：方法、路径、参数和请求体格式。
- 资源范围：请求能够访问的账户、项目、代码库、文件或记录。
- 操作范围：能够完成的读取、写入、删除、部署、计费和身份操作。
- 凭据接受范围：服务提供商接受哪些令牌、密钥、签名、受众和范围。

请求兼容性测试通过，几乎不能说明另外三个方面没有变化。这就是为什么一次变更可以通过传统回归测试，却仍然为代理打开通往生产数据的新路径。

不要以为使用带日期的 API 版本就能解决问题。服务提供商可以保持带日期的接口稳定，同时改变共享认证服务、增加代理能够发现的可选字段，或在端点路径之外改变默认值。固定版本很有用，但把版本固定当成权限边界就不谨慎了。

## 建立升级前后的权限地图

只看更新日志无法完成升级审核。先建立一张简洁的地图，列出代理可能发出的请求，再比较旧版本和新版本中的实际行为。

从真实流量开始，而不是从设计意图开始。代理经常会使用超出原始任务暗示范围的端点：发现调用、验证错误后的重试、分页、把名称转换为 ID 的查询端点，以及错误消息建议的便捷 API。要把这些调用纳入审核，因为它们可能泄露资源标识符，或提供比计划操作更宽的路径。

对每一类请求记录以下信息：

| 字段 | 需要记录的内容 |
|---|---|
| 操作 | HTTP 方法和规范化路径，例如 `POST /v2/projects/{id}/deployments` |
| 资源边界 | 它能够触及的租户、项目、代码库、环境或记录类别 |
| 凭据 | 令牌类别或 API 密钥标签，绝不要记录密钥本身 |
| 授权条件 | 允许该操作的范围、角色、受众、用户授权或服务端规则 |
| 默认行为 | 缺少可选过滤条件、分页限制和目标字段时会发生什么 |
| 拒绝预期 | 对禁止访问的资源和操作预期返回的状态及错误 |

地图应当用直白的语言说明资源边界。「可以调用部署 API」太模糊。「只能在沙盒项目中创建部署」才可以测试。如果服务提供商没有提供足够细节来明确边界，在确认边界前，应为代理使用独立的测试身份。

然后制作两列对比。使用包含明确隔离资源的独立账户，将同一组请求分别发送到旧版本和新版本。测试账户至少应包含一个允许访问的项目、一个禁止访问的项目、一条未激活记录，以及一个不同租户下的账户，前提是服务支持多租户。测试数据应使用容易识别的名称，这样才能发现结果中的意外越界。

不要只比较状态码。一个 `200` 响应可能隐藏真正重要的差异：记录数量翻倍、出现跨越边界的新 `next_page` 链接、多出凭据字段，或返回一个之后可以让代理调用特权端点的对象标识符。比较响应结构和标识符，再检查所有新增字段是否会带来后续权限。

## 变化的默认值会创建无人请求的访问路径

当服务器决定省略参数的含义时，省略参数本身也是一次授权决策。变化的默认值应该得到与新增写入端点同等程度的审核。

常见的风险始于一个看似无害的列表请求。版本一要求提供 `project_id`，只返回活跃记录。版本二允许请求省略 `project_id`，并将省略解释为「该令牌可见的所有项目」。如果代理原本就省略了这个可选字段，那么它的源代码没有变化，但可访问的数据已经扩大。

其他默认值也会产生相同结果：

- 列表端点开始包含归档、已删除或继承的对象。
- 分页从较小的固定结果集变成带有 `next` URL 的游标遍历。
- 创建端点在缺少工作区 ID 时选择调用方的默认工作区，而不是拒绝请求。
- 更新端点将省略字段解释为「保留当前值」，而不是要求明确的并发版本。
- 搜索端点开始索引连接服务中的内容。

服务提供商把这些变化称为改进，因为它们减少了客户端工作。对代理来说，客户端工作减少，往往意味着操作触及更广目标前的阻力也减少了。

使用刻意不完整的请求审核默认值。对每个可选参数，分别发送不带参数的请求，在 API 允许时发送空值请求，以及带有明确安全值的请求。比较目标集合和服务器错误。生成请求的代理会自然地尝试省略字段，尤其是在看到没有填写这些字段的文档示例后。

不要依靠「只能使用项目 A」这样的提示词来限制范围。提示词会影响请求选择，但 API 才决定请求能否触及项目 B。应把项目边界写入凭据、端点设计，或在请求离开机器前验证请求的网关中。

## 新增端点会让宽权限凭据变得更宽

如果现有凭据能够通过认证访问新端点，那么新端点就会改变该凭据的权限。即使代理在升级前从未调用过这个端点，风险也已经存在。

团队经常把新增端点排除在审核之外，因为它们被称为「新功能」。只有在人类用户获得新的界面控件，且管理员另外授予访问权限时，这种逻辑才成立。如果一个具有宽范围权限的 bearer token 能自动用于新路由，这种逻辑就失效了。

假设代理拥有一个标记为 `projects:write` 的令牌。版本一中，该令牌可以创建和编辑项目元数据。版本二新增 `POST /projects/{id}/exports`，可以创建可下载的导出文件，并使用同一范围。令牌的范围字符串没有变化，但拥有它所产生的效果变了。代理可能通过 API 模式、生成的客户端、错误提示或普通文档发现这个端点。

应根据效果而不是 HTTP 动词对新端点分类。`GET` 端点也可能暴露源代码、秘密值、审计历史、个人数据或带签名的下载 URL。`POST` 端点可能创建无法撤销的费用，或触发外部工作流。与其说 `DELETE` 路由更危险，不如说某个暴露凭据的 `GET` 路由可能更危险。

审核每条新路由时，回答四个问题：

1. 现有代理凭据能否成功完成认证？
2. 哪些现有范围、角色或 API 密钥类别允许访问它？
3. 它的输出能否为其他操作提供标识符、URL 或令牌？
4. 代理能否通过客户端库、发现文档或提供的文档访问它？

最后一个问题可以发现一个常见的错误建议：「我们不告诉代理这个新端点。」这个限制看似实用，因为代理大多数时候会遵循工作上下文，但它不是控制措施。代理可以检查模式、推测常见路径，或在之后的任务中得到指示。即使客户端知道精确 URL，服务器也必须拒绝未经批准的操作。

如果服务提供商无法将新路由与旧的宽范围权限分开，应在升级前创建更窄的集成身份。用于单一窄工作流的令牌，不应继承服务提供商之后赋予某个友好范围名称的每一种新含义。

## 认证变化就是权限变化

认证行为属于升级审核范围，因为接受凭据的方式发生变化，就会改变谁能够执行操作。团队经常测试成功登录，却跳过升级真正造成影响的拒绝场景。

OAuth 2.0 将访问令牌定义为代表授权许可的凭据，而 RFC 9700《OAuth 2.0 安全最佳实践》要求精确匹配重定向 URI，并说明了令牌重放和发送方约束令牌的防护。实际经验不限于 OAuth：令牌格式本身不能说明预期接收方、发送方或范围。资源服务器必须在每个接受的请求中强制执行这些属性。

版本变化经常间接影响这种强制执行。服务提供商可能引入新的签发者，接受发给兄弟 API 的令牌，增加令牌交换路由，改变刷新令牌轮换方式，或允许旧 API 密钥与带范围令牌同时使用。兼容性压力会让这些变化很有吸引力，也会创造工程师忘记测试的替代路径。

同时测试接受和拒绝。对每类凭据，测试允许的操作、对禁止资源执行相同操作、过期凭据、受众错误的令牌、缺少范围的令牌，以及已撤销凭据。如果服务支持刷新令牌，还要测试刷新是否保留旧授权、改变受众，或在之后的同意流程中悄悄获得新范围。

一个有用的记录如下：

```text
credential: build-agent-sandbox
request: POST /v3/projects/prod-42/deployments
expected: 403 forbidden
old version: 403 {"error":"insufficient_scope"}
new version: 201 {"id":"dep_...","environment":"production"}
review result: block upgrade and revoke credential
```

响应正文很重要。`403` 变成 `404`，可能是有意隐藏信息的变化。`403` 变成 `201` 则是权限增加，即使更新日志称其为兼容性改进。

还要检查标头行为。自定义标头可以选择 API 版本、组织或被模拟的用户。如果新 API 将缺少标头解释为默认组织，那么代理在标头格式错误后重试，可能会落到错误位置。测试时记录完整标头并隐藏秘密，同时单独测试省略标头的情况。

## 代理行为会把小差异串成完整工作流

代理可以把若干单独看起来普通的调用串成 API 设计者从未作为一个权限整体审核过的结果。版本审核必须跟踪这些调用链。

新增的列表字段可能暴露代码库 ID。这个 ID 可能被传给下载端点。下载响应可能包含签名 URL。该 URL 可能暴露一个配置中含有另一服务端点的构件。每次调用单独看似乎都被允许，但整个序列可能超出代理收到的任务范围。

因此，逐端点授权审核虽有必要，却并不完整。为希望代理执行的操作和希望排除的相邻操作增加工作流测试。跟踪调用之间的标识符流动：ID、分页游标、位置、预签名 URL、作业 ID，以及泄露有效资源名称的错误消息。

测试要具体。如果代理应该更新一个代码库中的问题，请测试它能否：

- 读取指定问题并更新获准字段。
- 在尝试使用另一个代码库的问题 ID 时失败。
- 在尝试修改代码库设置或 Webhook 时失败。
- 在跟随链接访问导出、成员列表或令牌管理路由时失败。

失败路径与成功路径同样重要。代理会把错误当作信息。详细的拒绝消息如果点出了另一个端点，可能会让意外路径更容易被发现。对人类开发者可以接受这种取舍，但在把集成暴露给自主进程前，应先确认它确实存在。

升级测试期间应限制重试次数。当请求具有幂等性时无害的重试策略，如果新版本改变了幂等性处理，或在完成工作后返回超时，就可能造成重复操作。确认 API 是否使用幂等键、保留该键多久，以及版本升级是否改变标头名称或请求哈希规则。

## 权限差异测试能发现普通测试遗漏的变化

权限差异测试是一种可重复的测试，关注凭据能够完成哪些请求，而不是应用是否仍然收到预期数据。它应足够小，以便对每个候选版本运行。

在不包含生产秘密的代码库中建立请求集合。使用环境变量提供测试令牌，只访问一次性账户。以下 shell 模式会记录能够暴露权限变化的部分，同时避免输出凭据：

```sh
curl -sS -D headers.txt -o body.json \\
  -H "Authorization: Bearer $TEST_TOKEN" \\
  -H "X-API-Version: 2025-01-01" \\
  "https://api.example.test/v1/projects?limit=2"

printf 'status: ' \u0026\u0026 head -n 1 headers.txt
printf 'headers:\\n' \u0026\u0026 grep -Ei '^(link|location|x-request-id|www-authenticate):' headers.txt
printf 'identifiers:\\n' \u0026\u0026 jq -r '.. | objects | (.id? // empty)' body.json | sort -u
```

对每个版本运行一次请求集合，然后比较状态、选定标头和规范化后的标识符。不要盲目比较完整 JSON。时间戳、请求 ID 和排序会制造噪声，让审核者习惯于忽略差异。先规范化这些字段，但保留分页链接、资源 ID、角色名称，以及所有能够引导后续请求的字段。

请求集合应包括成功请求、预期拒绝、省略可选参数的请求，以及第一页和后续分页请求。对每个新记录且看起来与现有代理范围相关的路由至少增加一个请求。目的不是枚举整个服务提供商，而是覆盖代理能够现实地发现或组合的每个操作。

一个简单的结果文件能让决策可审核：

```json
{
  "case": "forbidden-production-deploy",
  "credential": "build-agent-sandbox",
  "request": "POST /v3/projects/prod-42/deployments",
  "expected_status": 403,
  "observed_status": 403,
  "observed_resource_ids": [],
  "version": "2025-01-01"
}
```

每个差异都必须有明确的审核决定。「服务提供商改了，所以这是预期行为」不算决定。审核者必须说明新行为是否仍在代理获准权限内，如果是，权限在哪里得到强制执行。

## 日志能证明发生了什么，却不能证明应该发生什么

请求日志有助于调查升级，但不能取代部署前的权限审核。它们回答的是不同问题。

升级前，权限差异测试告诉你服务提供商是否会接受不应接受的请求。升级后，日志告诉你代理是否实际尝试了该请求、哪个进程发起了尝试，以及是否需要控制账户。两者都需要，因为今天被拒绝的请求，明天可能会因服务提供商的行为变化而被接受。

记录版本选择器、规范化操作、目标边界、凭据标签、决定、状态和关联 ID。不要记录 bearer token、原始授权标头、完整请求正文或包含秘密的响应字段。保存本应保护的凭据，只是把泄露位置换了地方。

把会话记录和操作记录分开。会话记录说明哪个代理进程在一次运行期间获得了操作许可。操作记录说明它发出了哪个具体请求。当长期运行的代理以审核过的版本启动，之后却收到环境变化或重新生成的客户端库时，这种区分尤其重要。

Sallyport 通过一份加密、哈希链式审计日志生成 Sessions journal 和 Activity journal，因此操作员可以同时检查代理运行和每个 HTTP 或 SSH 操作。离线的 `sp audit verify` 检查无需访问保险库即可验证链条，这在升级审核转为事件调查时很有用。

不要把篡改可见性误认为预防能力。完整的审计记录可以证明新端点曾被使用，却无法从远程服务收回已经导出的数据。应让敏感操作受凭据和批准保护，并确保它们在请求离开机器前就能失败。

## 批准应绑定进程，而不是模糊任务

只有当批准明确告诉人类哪个可执行程序正在请求权限时，人类批准才能阻止未经审核的升级。「代理想要 API 访问权限」在多个本地进程都能使用同一协议时，信息远远不够。

在操作系统提供相关信息的情况下，将会话授权绑定到请求进程的代码签名权限。这可以阻止一种常见的替换失败：受信任的代理启动会话后，不受信任的辅助程序或复制的二进制文件试图复用同一凭据路径。进程身份不能证明未来每个请求都明智，但它能让操作员批准或撤销一个具体对象。

对生产部署、账户管理或数据导出等本来就难以限制影响的凭据，才使用逐次调用批准。要求人类批准每次无害读取，只会让他们习惯性点击确认。批准疲劳是设计错误，不是用户缺陷。

Sallyport 使用固定的三级决策阶梯：锁定的保险库拒绝所有操作；新的代理进程默认请求会话授权；按密钥设置可以要求每次使用都单独批准。这种窄模型无法表达所有组织规则，但能避免把权限变化埋在大量策略语法中。

当 API 升级改变凭据的实际范围时，应撤销当前会话，并在审核完成后强制重新批准。不要让针对昨天端点批准的会话，悄悄延续到明天更宽的端点集合。

## 把权限审核设为发布门槛

当现有凭据获得了无法解释的成功请求、被拒绝的请求变得获准，或响应暴露了能启用受禁止工作流的新标识符时，API 版本升级就应无法通过发布门槛。

把审核放进与依赖更新和生成客户端变更相同的变更记录中。记录旧的和新的 API 选择器、服务提供商更新说明、权限差异测试输出、测试过的凭据类别，以及接受每个有意差异的人员。这是日常工作，所以常常被跳过，直到第一条奇怪的审计记录出现。

不要等到 MAJOR API 版本才审核。当服务提供商更改 API 版本、认证服务、OAuth 应用设置、生成的 SDK、发现模式、默认标头或范围定义时，都应触发审核。URL 之外的变化同样可能改变远程服务器作出的决定。

从这样一种凭据开始：如果它多出一条路由，造成的伤害最大。为它配置隔离测试账户，写出五个允许和拒绝的请求，并针对拟议版本运行。如果你无法说明每个成功请求为什么属于代理的工作范围，那么该集成还没有准备好用于自主操作。
