# 过时的 API 文档：安全测试智能体操作

过时的 API 文档是智能体安全问题，而不是编辑上的小麻烦。人读到过时示例时，可能会犹豫一下，再去询问同事。自主编程智能体往往会把同一个示例直接变成请求，然后把响应当作自己操作正确的证据。

这会改变我们对文档的要求。如果文档教智能体如何调用 API、轮换令牌、删除记录，或访问生产主机，就应该把这段文字当作可执行输入，并与工具本身一起测试。页面发布时可能准确无误，但如果它现在与线上服务不再一致，就可能把智能体引向不安全的操作，即使 API 完全按照当前维护者的意图正常运行。

最危险的漂移通常不会以明显的失败出现。404 会引起注意，但一个请求仍然返回 200，同时选中了更多资源、使用了改变后的默认值，或绕过了原本应有的确认，就不一样了。这类不一致会留下干净的活动日志，却带来糟糕的后果。

## 文档会成为智能体控制平面的一部分

智能体依靠文档选择操作、填写参数、解读响应，并决定是否重试。因此，示例、参考表、身份验证指南和迁移说明都属于它的控制平面。服务代码可能完全正确，但这个控制平面却在告诉智能体错误的用法。

团队常常人为地区分工具定义和指南。工具定义写着 `deleteProject(project_id)`，指南则说明应该获取哪个项目标识符、是否支持试运行、删除是否级联，以及授权失败后该做什么。智能体两者都需要。只要其中一方提供了错误信息，最终操作就可能出错。

这就是过时示例与拼写错误段落的区别。假设旧指令写着，缺少 `scope` 参数表示“当前项目”。后来后端改变了行为，同样的省略现在表示“该凭据可访问的所有项目”。端点仍然正常工作，示例仍然可以解析，但遵循旧指南的智能体可能把原本局部的变更应用到整个账户。

文档还会影响智能体的信心。具体代码片段通常比附近含糊的警告更有说服力。如果一页写着“遵循最小权限”，另一页却给出具有账户级访问权限的 bearer token 示例，实际操作中往往是代码片段占上风。智能体会优先选择能够产生结果的路径。

以下内容都应视为会触发操作的文档：

- 请求和命令示例
- 描述默认值和允许值的参数表
- 身份验证、凭据和环境设置说明
- 重试、分页、幂等性和错误处理指南
- 告诉用户哪个操作替代另一个操作的迁移和弃用说明

可以把漂移分为**语法漂移**和**含义漂移**。语法漂移会让示例因字段或路径变化而失败。含义漂移则会让示例仍然有效，却改变它影响的对象。语法漂移会让作者难堪，含义漂移却可能损坏数据、花费资金、暴露记录或扩大访问权限。测试套件必须发现这两类问题。

## 请求通过，也可能证明示例是错的

只检查状态码的文档测试只能抓住容易发现的失败，却会漏掉不安全的问题。HTTP 成功状态说明服务器接受了请求，但不能说明请求是否针对了正确对象、是否产生了文档所描述的副作用，或是否遵守了声明的边界。

假设参考页面发布了以下请求：

```bash
curl -sS -X POST "$API_URL/v1/exports" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"project":"demo","include_archived":false}'
```

基础测试检查是否返回 `202 Accepted`，然后宣布测试通过。这个检查会漏掉几种重要变化：

- 服务悄悄把 `project` 改名为 `project_id`，并把旧字段视为缺失。
- `include_archived` 从排除选项变成了被忽略的兼容字段。
- 凭据获得了账户级可见性，因此 `demo` 解析成了另一个租户的项目。
- 端点仍然会排队处理任务，但现在会导出指南所说应该排除的附件。

测试需要检查结果和服务状态，而不只是状态码。在一次性账户中创建一条活跃记录和一条归档记录，发送文档中的请求，轮询生成的任务，然后断言产物包含活跃记录、排除归档记录，并记录了预期的项目标识符。如果服务无法提供足够证据来完成这些断言，文档也不能安全地承诺这种行为。

RFC 9110 将 HTTP 状态码定义为请求处理的结果，并没有声称成功状态可以证明调用方的业务意图。这听起来很明显，但团队仍然不断构建把协议简化为 `curl` 加 `grep 200` 的文档检查。协议断言可以使用 HTTP 语义，但之后还要补充针对实际结果的断言。

好的测试会直接写出它验证的声明。`export_excludes_archived_records` 很有用，`docs_example_returns_success` 则基本只能说明有人发出了请求。

## 示例需要契约测试，而不是截图审查

在发布审查期间，把文档页面上的示例复制到终端运行，比什么都不做强，但这种方式无法扩展，也不会留下可靠证据，而且偏向成功路径。人们还常常在本地修好命令，却忘了修正文档页面。

把可执行示例放进结构化源文件，从这个源文件生成最终片段，并在 CI 中运行同一份源文件。可以使用 OpenAPI 示例、Markdown 代码提取，或单独的夹具目录。具体机制并不重要，关键是读者看到的命令必须与测试运行的命令完全相同。

不要悄悄维护一个拥有更安全 URL、更窄权限范围或更完整请求头的“测试版本”。这种分裂会制造令人安心的绿色构建，同时让公开指令逐渐腐烂。只有基础 URL、测试凭据和夹具标识符等必须因环境不同而变化的值，才应该参数化。方法、路径、请求体结构以及与安全相关的标志都应保持一致。

一个小型 shell 测试可以说明这种模式：

```bash
set -euo pipefail

project_id="docs-check-$RANDOM"
response=$(curl -sS -X POST "$API_URL/v1/projects" \
  -H "Authorization: Bearer $DOCS_TEST_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"id\":\"$project_id\",\"name\":\"Documentation check\"}")
printf '%s' "$response" | jq -e \
  --arg id "$project_id" \
  '.id == $id and .name == "Documentation check" and .archived == false'
```

`jq -e` 的预期输出是 JSON 值 `true`，不匹配时会以非零状态退出。重点不在 shell 语法，而在于断言编码了文档的声明：API 会使用提供的标识符创建项目，保留提供的名称，并且默认不会将其归档。

还要为渲染后的文档建立单独检查。如果 Markdown 提取器从页面提取标记为 `bash` 的代码块，测试就应该在替换经批准的环境变量后运行这段代码。如果文档由 OpenAPI 描述生成，应测试生成出的示例，而不是手工复制的另一个版本。

截图审查仍然可以帮助检查可读性，但无法证明行为。页面包含多个相似示例时，审查者很容易漏掉一个缺失的请求头。机器不会因为反复把字段与夹具进行比较而疲倦。

## 线上服务检查必须覆盖默认值和失败路径

最危险的 API 变化通常发生在默认值、授权边界和失败处理上。成功路径测试会避开这三者，因为它们难写，也难一直保持通过。

测试智能体可能产生的省略情况。智能体经常按条件构造请求体，因此当前一次查询没有返回值时，可选字段可能直接消失。对于每个文档中记录的可选参数，都要确定省略它是安全、会被拒绝，还是会产生有意义的不同，然后直接测试文档所描述的行为。

对于包含 `dry_run` 字段的操作，至少要在隔离环境中运行以下情况：

1. `dry_run: true` 返回计划，并且夹具保持不变。
2. `dry_run: false` 只对指定夹具执行声明的变更。
3. 省略 `dry_run` 时，要么拒绝请求，要么产生文档中说明的默认行为。
4. 权限范围不足的令牌在任何变更发生前失败。
5. 重复发送文档中的请求时，行为符合幂等性说明。

第三种情况可以发现一个常见的意外损害来源。服务团队为了方便交互式用户而改变默认值，但文档仍然假设旧默认值。人类界面可能会显示确认页面，API 客户端却没有这样的页面。

失败示例也需要测试。文档经常只说“遇到 429 就重试”，却没有说明响应是否包含 `Retry-After`、重复操作是否安全，或请求是否需要幂等令牌。这种建议可能把短暂的限流变成重复开票、重复部署或重复撤销。

测试确切的失败处理说明。在测试服务或受控环境中触发限流，确认文档中的客户端会读取指定请求头、按要求等待，并在 API 支持时使用相同的幂等标识符重新发送请求。如果服务无法稳定地产生该错误，就应记录这种不确定性，而不是发布看似确定的操作步骤。

OpenAPI 的 `default` 关键字也会带来类似陷阱。在 JSON Schema 和 OpenAPI 描述中，声明的默认值通常表示工具可以假设或显示什么，但不会自动让每个服务器实现都采用该值。应在省略字段的情况下检查已部署的服务。模式默认值和服务器默认值是两项独立声明，直到测试把它们联系起来。

## 破坏性工作流需要一次性环境中的证据

不要在共享预发布账户上测试破坏性示例，然后就把它称为安全。共享环境会积累旧夹具、手工实验和权限范围不清的凭据。最终，文档测试可能匹配错误对象，或者清理命令超出预期边界。

使用专用测试租户或账户，并让凭据只能访问测试资源。每个夹具都要带有唯一的运行标记。在修改前先通过这个标记取回夹具。测试结束后，验证最终状态，不要假设 API 的响应就代表它确实完成了所声称的操作。

删除示例应该展示完整生命周期：

```text
create fixture: docs-delete-<run-id>
read fixture: confirm owner=test-suite and run_id=<run-id>
delete fixture: send the rendered documentation request
read fixture: expect the documented absence or tombstone state
list nearby fixtures: confirm unrelated fixtures remain
```

最后一步很重要。只确认选定对象消失的删除测试，无法发现选择器范围过宽。我见过团队因为自己的一个夹具按预期消失，就接受了一个批量端点，却没发现它同时删除了所有具有相似前缀的资源。测试需要一个名称相似、但本应保留的邻近对象。

不要让文档告诉智能体使用 `latest`、`all`、空过滤器或人类可读名称等便利选择器，如果存在不可变标识符，就应优先使用它们。这些选择器在教程中显得友好，智能体在繁忙账户中运行步骤时却会变得危险。如果操作确实需要宽泛选择器，就把范围放进请求体或命令参数，让审查者可以直接看到。不要把它藏在服务器默认值里。

对于不可逆操作，应先执行预检读取，并让示例使用读取结果。先获取对象，确认其不可变 ID 和相关状态，再执行变更。这样会增加一点摩擦，但比解释智能体为什么删除了一个恰好拥有相同显示名称的对象便宜得多。

## 工具测试必须比较含义，而不只是模式

模式验证很有必要，但模式通常比后果更能准确描述结构。请求体可以满足所有类型约束，却仍然可能把操作指向错误环境，或使用错误权限。

围绕四个问题建立断言：操作影响了谁？什么状态发生了变化？什么状态没有变化？哪个身份完成了授权？这些问题适用于 HTTP API、SSH 命令和内部工具。

对于 HTTP，如果服务提供请求标识符，就捕获它，然后在测试环境中查询结果资源或审计记录。匹配请求标识符、操作者、目标和变更。对于 SSH，要在一次性主机上运行命令，记录退出状态和输出，再用单独的验证命令检查主机状态。不要让执行操作的命令自己给自己的作业打分。

有对比的夹具更有用。如果要测试只重启一个服务的命令，就创建另一个必须保持运行的服务。如果要测试仅限一个仓库的查询，就加入第二个仓库，让同一个凭据可以看到它，但请求不应触碰它。没有对比，范围过大的操作也可能看起来正确。

许多团队正是在这里误用了契约测试。消费者驱动的契约工具可以确认提供方接受某种请求结构并返回预期字段，却无法判断请求是否选中了正确的生产账户、`force` 选项是否有了新含义，或删除是否超出了文档所描述的对象。契约测试仍然要保留，但还应增加使用专门夹具发现越界影响的结果测试。

工具描述本身也需要测试。如果工具公开了 `environment`，除非测试确认它会路由到文档所说的主机并使用声明的授权路径，否则不要把 `production` 描述为允许值。智能体会依据描述填写参数。过时的描述就是过时 API 示例的另一种文字形式。

## 智能体需要新鲜度证据和安全的拒绝路径

智能体不应因为文档出现在代码仓库或内部门户中，就推断它仍然有效。应提供与计划调用的操作绑定的、机器可读的验证证据。

一个简单的清单就可能足够：

```json
{
  "operation": "POST /v1/exports",
  "documentation_source": "docs/api/exports.md#creating-an-export",
  "verified_in": "isolated-test-tenant",
  "verification_commit": "<commit-id>",
  "assertions": [
    "returns an export job",
    "omits archived fixtures when include_archived is false",
    "rejects a token without export scope"
  ],
  "review_required_when": ["production", "include_archived=true"]
}
```

提交标识符本身不是信任徽章，但它能让审查者追溯产生证据的文档和测试源文件。如果发布流程需要限制证据的有效期限，可以在自己的构建记录中保存验证时间，但不要假装时间戳能让旧行为变得安全。服务部署可能使昨天的测试失效。

智能体的决策规则应该简单明白。如果请求的操作没有针对已部署接口的通过验证记录，智能体必须在获准的测试环境中执行非变更预检，或请求人工批准确切操作。它不应根据相邻端点自行推断。

要把“未知”与“安全”分开。智能体倾向于填补空白，因为完成任务会得到正向反馈。工具设计必须让缺少证据时的主动停止也成为一种成功结果。可以返回这样的原因：`documentation example has no verified outcome test for this operation`。这会给开发者一个明确的修复目标，而不是模糊的拒绝。

不要用一份试图列举所有文档中每个危险措辞的长政策文件来解决问题。措辞变化会比规则更快。应把具体操作绑定到具体测试，并把结果提供给智能体。

## 发布门禁只有在阻止误导页面时才有用

如果验证程序只生成没人必须处理的报告，它就失败了。当服务契约发生变化时，检查必须阻止发布，或至少阻止智能体访问受影响的示例。

把检查连接到 API 规范、路由处理程序、身份验证中间件、SDK 请求构造器和文档源文件的变更。任何一处变化都应运行相关示例测试。测试失败时，团队只有三个诚实选择：恢复旧行为，按照新行为更新文档和测试，或在验证成功前将该操作标记为不可供智能体使用。

人工审查仍然有助于判断，但单靠人工审查不是正确建议。它受欢迎，是因为看起来成本低，也能保持快速发布流程。可它要求审查者根据文字，在脑中模拟服务、凭据、默认值和状态转换。人无法在日常发布中可靠地完成这件事。

让失败信息清晰可读。一份有用的报告会写出页面、代码块、操作、测试夹具、实际响应和未满足的断言。“文档集成失败”只会制造一场寻找问题的过程。“exports.md 第 42 行说会排除归档记录，但导出产物包含了 archived-run-817 夹具”则能让负责人直接修复。

不要因为服务变化让测试变得不方便，就放宽测试。先判断旧承诺是否有价值。如果有，就恢复它，或明确说明新的限制。如果旧承诺不安全，就删除示例，不要用更温和的句子勉强保留。智能体通常会遵循剩下的命令。

版本化文档也需要同样的纪律。旧 API 版本的页面可能准确描述旧部署，却仍然会误导指向当前基础 URL 的智能体。应把版本放在端点路径、服务器 URL 或工具元数据中，让智能体能够将它绑定到请求。页面上方某处写着“v1”，证据强度很弱。

## 授权限制影响范围，却无法修复错误指令

审批和凭据隔离仍然重要，因为文档检查不可能发现所有缺陷。它们可以在智能体选错操作时减少损害，却不能把过时指令变成正确指令。

应把两项工作分开。文档验证要回答：“这个示例是否描述了线上服务及其后果？”操作授权要回答：“现在是否应该允许这个智能体发出这次调用？”把两者混在一起会造成混乱。用户可能因为智能体说它只会导出一个项目而批准调用，但过时示例实际上会导出凭据可以看到的所有项目。

对于由智能体驱动的 HTTP 和 SSH 操作，Sallyport 可以让凭据留在智能体之外，并要求用户授权会话或单独使用某个凭据。当文档检查找不到可信证据，或操作后果值得人工审查时，这是一道有用的最后边界。

审批页面应以人能够判断的方式显示操作、目标和范围。只显示“POST /v1/exports”并不够，因为请求体中可能包含 `include_archived=true` 或账户级选择器。如果授权层无法呈现有意义的范围，就应缩小工具接口，直到它能够做到。

审计记录可以为测试改进提供材料。当用户撤销一次运行或质疑某项操作时，应检查确切请求、智能体引用的文档来源，以及它拥有的验证证据。不要把这次审查当成追责。用它补充缺失的夹具、断言或拒绝条件。

## 先测试最可能让你后悔的示例

不要从参考文档中最干净的 GET 请求开始。先测试可能删除、发布、轮换、授权、收费或访问生产主机的示例。给它配置隔离夹具，运行渲染后的确切命令，并同时断言预期变化和附近不应发生的变化。

然后把测试连接到文档源文件，并在发布或供智能体使用前让失败变得可见。这项工作没有编写新工具描述那么光鲜，却能消除一个危险假设：页面曾经通过审查，所以现在一定安全。

线上服务会变化。除非强制文档与服务在测试中相遇，否则文档会变化得更慢。把这次相遇纳入发布流程，赶在智能体把旧句子变成实际操作之前。
