# 破坏性 API 参数：删除前先验证

当请求看起来在语法上正确时，AI 代理很容易直接发起破坏性 API 调用。格式有效的 JSON 请求体、熟悉的账户名称，以及一次成功的查询，都不能证明代理即将删除的是目标对象。在任何外部请求移除、撤销、断开、取消或覆盖数据之前，都要通过服务本身验证身份、所有权、范围和新鲜度。

我见过经验丰富的工程师把一个请求理解成“删除这个测试环境”，API 却把它解释成“删除该组织下的所有环境”。严格来说，代码通常并没有明显的 bug。它可能接受了格式宽松的选择器、从错误的账户解析了名称，或者信任了本次运行更早阶段获取的数据。自主代理会更快、更自信地犯下这些常见错误。

解决办法不是让模型“更加谨慎”。应在代理提出的请求与真正携带凭据的请求之间放置一个确定性网关。网关默认拒绝含糊的请求，并要求人批准实际目标和实际影响。

## 格式正确的请求仍可能指向错误对象

只有在确认以下四点后，请求才足够安全，可以发送：资源类型正确；不可变 ID 就是目标 ID；资源属于获准的父账户；操作影响的集合符合批准范围。团队经常把这些事实合并成一次查询。删除操作出问题，往往就是从这个捷径开始的。

假设某项服务同时提供显示名称和 ID：

```json
{
  "id": "env_7d3a",
  "name": "staging",
  "account_id": "acct_blue",
  "state": "active"
}
```

搜索 `staging` 的代理找到的是候选对象，而不是删除授权。许多账户都有一个名为 `staging` 的环境。即使在同一个账户内，旧对象消失后也可能重新使用原来的名称。唯一安全的操作请求，是使用返回的不可变 ID 构建，并根据预期的父 ID 进行核验。

当 API 支持类似 `/accounts/{account_id}/environments/{environment_id}` 的父级路径时，两个路径片段都要检查。不要因为某个 ID 出现在更早的搜索响应中，或因为代理在计划中写了同一个账户名称，就推断资源归属正确。操作 URL 中的父级部分也是授权边界的一部分。

资源类型需要单独检查。API 经常使用共享搜索端点，或返回混合记录。名为 `staging` 的结果可能是环境、项目、访问组，也可能是保存的模板。如果服务针对不同对象提供不同的删除端点，必须先确认返回的类型，再选择端点。不要通过拼接模型生成的类型字符串来构造端点。

最后，在查看候选对象之前，先定义预期影响。“删除旧部署”可能意味着删除部署记录、停止运行中的任务、撤销为它签发的令牌，或者删除整个环境。这些影响不能共用一个通用的 `delete` 操作。要在操作契约中明确请求的动词和对象类型。

## 名称方便人识别，ID才控制请求

使用不可变 ID 来定位资源，同时向审批者提供足够的人类可读信息，以便发现错误选择。只显示 `env_7d3a` 的审批卡片很容易让人机械点击；只显示 `staging` 又会造成歧义。应同时展示两者，以及父账户和操作后果。

一个实用的目标记录如下：

```json
{
  "operation": "delete_environment",
  "account": {"id": "acct_blue", "name": "Blue Team"},
  "target": {"id": "env_7d3a", "name": "staging", "type": "environment"},
  "expected_state": "active",
  "effect": "permanently removes this environment and its managed resources"
}
```

账户显示名称能帮助操作人员发现代理是否误入了错误的租户。资源名称帮助他们识别对象。ID 则让请求没有歧义。明确写出影响，可以避免一种常见的审批错误：操作人员以为批准的是可恢复的停止操作，而服务商实际上会删除数据。

不要取搜索结果中的第一项来解析名称。搜索端点经常按相关性排序、返回部分匹配结果，或使用分页。如果任务给出精确名称，那么在按父账户和预期类型过滤后，必须恰好得到一个候选对象。零个候选应当失败，多个候选也应当失败。让代理自行选择并不能解决问题，因为它没有证据区分这些对象。

大小写处理也需要遵循服务商的具体规则。有些服务商区分大小写，有些会进行规范化。不要在本地规范化名称后，就假定服务商会采用相同方式。应以服务商返回的资源为依据，并在审批记录中保留返回的准确标签。

标签、标记和描述只能提供上下文，不能作为身份。它们经常变化，用户也可以在其中写入几乎任何内容。像 `temporary=true` 这样的标签可以缩小已审核列表，但不能替代账户绑定或不可变资源 ID。

## 在代理请求审批前，范围必须具体明确

即使请求体只包含一个 ID，破坏性操作也有范围。范围包括父账户、选定资源、服务商会自动删除的子资源，以及任何会扩大选择集合的筛选条件。在请求审批前，先把范围具体化。

删除单个对象有一个简单契约：一个不可变 ID、一个预期父级和一种资源类型。批量删除需要不同的契约。它必须先解析出完整集合，然后针对该集合，或针对人可以检查的有限摘要获得审批。直接把选择器发送给破坏性端点，等于让外部服务在审批完成后才决定范围。

假设代理提出以下请求：

```json
{
  "account_id": "acct_blue",
  "filter": {"label": "cleanup-candidate"},
  "delete": true
}
```

这个请求体隐藏了操作人员唯一需要知道的事实：当前究竟有哪些资源匹配。应通过只读列表调用展开筛选结果，拒绝未完整检查的分页，并把结果规范化为 ID。然后展示数量和包含名称的简短样例。如果集合超过批准上限，就停止并要求新的指令。

绝不要让缺少筛选条件表示“全部”。在请求模式中，要区分必填的空列表和不存在的选择器。更好的做法是：在代理操作接口内部，禁止破坏性端点接受筛选条件。让网关只接受已经解析出的 ID 列表来执行批量操作。

一个实用的结构如下：

```json
{
  "operation": "delete_resources",
  "account_id": "acct_blue",
  "resource_type": "snapshot",
  "resource_ids": ["snap_104", "snap_105"],
  "selection_observed_at": "2025-03-08T14:32:11Z"
}
```

除非工作流明确允许，否则拒绝空的 `resource_ids` 数组。拒绝重复 ID。拒绝属于其他账户的 ID。强制设置符合操作人员批准范围的最大数量。数量上限不能替代审核，但可以防止格式错误的循环把两项资源清理变成一千项资源事故。

级联删除也属于范围的一部分。如果删除项目会同时删除代码库、部署密钥、环境或账单记录，必须在审批前说明。如果服务商只有通过预检调用才能提供级联详情，就保留该响应并要求代理展示。“删除项目”过于笼统，因为项目可能有依赖对象。

## 当时间可能改变目标时，要读取服务两次

预检读取可以验证意图，但不能冻结对象。在验证和删除之间，资源可能发生变化、转移账户或消失。对于敏感操作，应在变更前立即重新读取目标，并在服务商提供并发控制时使用它。

HTTP 提供了标准机制。RFC 9110 规定了使用 `If-Match` 的条件请求：只有当前表示与客户端提供的实体标签匹配时，服务器才执行请求方法。`GET` 可能返回 `ETag`，后续的 `DELETE` 可以携带这个准确值。

```http
GET /v1/accounts/acct_blue/environments/env_7d3a HTTP/1.1
Authorization: Bearer [injected credential]

HTTP/1.1 200 OK
ETag: "v42"
Content-Type: application/json

{"id":"env_7d3a","account_id":"acct_blue","name":"staging","state":"active"}
```

将响应内容与已批准的目标比较后，发送：

```http
DELETE /v1/accounts/acct_blue/environments/env_7d3a HTTP/1.1
If-Match: "v42"
Authorization: Bearer [injected credential]
```

如果服务返回 `412 Precondition Failed`，应把它视为防护措施成功生效。不要让代理不带条件地重试删除。重新获取资源，与已批准的事实进行比较；如果任何相关事实发生变化，就要求新的审批。版本冲突说明旧审批可能已经不再适用。

有些服务使用修订号、更新时间、代数或请求令牌，而不是 HTTP ETag。应使用服务商文档规定的机制。如果服务商没有提供任何机制，就缩短最终读取与写入之间的间隔，让操作串行执行，并接受无法证明目标保持不变这一限制。这项限制应影响你是否允许无人值守删除。

不要把成功的 `GET` 与变更权限混为一谈。读取凭据能看到的内容，可能多于写入凭据可以修改的内容，授权状态也可能独立于对象状态发生变化。最终还是由变更响应决定服务商是否接受请求。

## 验证应位于凭据边界

只在代理提示词或生成代码中执行验证，属于建议而非强制。持有或注入凭据的组件必须执行检查，因为它是阻止出站请求的最后位置。

这个边界应接收结构化操作提案，而不是自由格式的 URL 和任意请求头。一个狭窄的操作定义可以要求账户 ID、资源 ID、方法、预期类型和预期版本等字段。它可以根据经过验证的片段构建出站路径，并拒绝会扩大范围的查询参数。

不要接受代理提供的完整 URL，再试图从中解析出安全性。URL 编码、重复查询键、备用主机名和路径规范化会把任务变成解析器竞赛。应接受类型明确的字段，根据服务商契约逐项验证，并自行构建 URL。请求体也遵循同一规则。生成已知结构的请求体，不要直接传递未经检查的字段集合。

一个最小网关可以强制执行以下顺序：

1. 确认请求的操作存在于允许列表中，并且其方法本身具有破坏性。
2. 在同一个父账户下，通过读取调用解析每个声称的目标。
3. 将返回的 ID、类型、父级和所需状态与结构化提案进行比较。
4. 针对解析出的影响获得审批，然后重新检查新鲜度并发送变更请求。
5. 记录结果，包括服务商返回的请求 ID。

允许列表要尽量小。名为 `raw_http` 的通用逃生口会破坏本文的所有检查，因为代理可以重新引入任意目标、方法和请求体。工程师通常在缺少某个端点时添加这种逃生口，直到它绕过原本以为存在的防护，才想起它还在那里。

凭据应留在代理上下文之外。代理需要的是获准操作的结果，而不是可以复制到 curl 命令、日志或第三方集成中的 bearer token。Sallyport 的 HTTP 操作遵循这种结构：它将凭据保存在加密保险库中，自行执行请求，然后把结果返回给代理。

## DELETE 不代表请求简单，也不代表操作可恢复

HTTP 方法名无法说明完整的业务影响。RFC 9110 规定，`DELETE` 是请求源服务器移除目标资源与其当前功能之间的关联。该 RFC 并没有保证数据会立即消失、关联数据会保留，或在特定 API 中重试一定没有风险。

实际影响必须由服务商文档定义。有些 API 会把对象标记为稍后删除，有些会创建墓碑记录，有些会将对象从父级分离，还有一些会级联删除子对象。在把操作归类为低风险之前，先阅读端点的响应码和生命周期说明。

除非服务商明确记录，否则不要在 `DELETE` 中发送请求体。RFC 9110 指出，`DELETE` 请求中收到的内容没有普遍定义的语义，可能导致实现拒绝请求。依赖请求体筛选条件的删除 API 对该服务商来说可能有效，但应通过其文档规定的客户端路径进行额外测试。这不能成为从代理传入自由格式选择器的理由。

重试同样需要谨慎。网络超时会造成未知结果：客户端停止等待后，服务商可能已经完成删除。立即重试可能产生误导性日志，在设计不佳的端点上触发第二次影响，或者在按名称重新解析时删除一个后来创建的替代资源。

对未知结果采用“先检查”规则。使用准确的不可变 ID 和准确的父级查询对象。如果对象已经不存在，并且服务商的删除模型支持这种判断，就将操作记录为已完成，但注明第一次响应不确定。如果对象仍然存在，则先检查其状态，以及服务商提供的请求历史，再决定是否重试。对于支持幂等键的操作，重复使用同一个幂等键，但不要在服务商没有提供幂等能力时自行假定它具备幂等性。

`204 No Content` 只表示服务器按照该端点的定义接受并完成了 HTTP 交互。它不能证明所有下游清理都已结束。如果代理的下一步操作依赖删除已经完成，就轮询文档规定的操作状态或资源状态，不要把空响应体当成保证。

## 审批应展示后果，而不是原始传输内容

当审批说明用普通语言描述影响，并包含验证所需的标识符时，人更容易做出正确判断。展示方法、路径和 JSON 请求体对 API 工程师有用，但会把过多解析工作交给本应发现错误目标的人。

对于单个资源，审批应说明将发生什么变化，写出父账户，显示资源名称和 ID，并指出不可逆或级联影响。对于批量操作，应显示数量、有限样例、生成列表时使用的选择规则，以及最终操作使用冻结 ID 而不是规则本身这一事实。

不要在漫长的代理运行开始时请求一次宽泛审批，然后把它用于后续所有删除。代理发现资源的过程中，目标集合会发生变化。审批应绑定到会话和解析出的具体操作。如果代理进程发生变化，审批不应悄悄跟随一个可能拥有不同代码或指令的新进程。

另一种失败是审批疲劳。让人每次无害读取都点击确认，会训练他们不看内容就点击，随后危险删除也会得到同样的视觉权重。在适当情况下，让读取操作无需交互；新启动的代理要求会话授权；只有涉及凭据或破坏性影响的操作才需要逐次确认。提示更少，人才能更仔细地检查真正重要的提示。

审批记录需要明确的有效期。代理在验证后等待越久，预检的意义就越小。如果操作不能及时执行，就重新解析并再次请求审批。清理任务期间这可能显得严格，但比解释一小时前的审批为何适用于一个后来已经重新创建的资源，代价要低得多。

## 审计证据必须能重建决策，同时不能泄露密钥

有用的审计轨迹回答的不只是“请求是否发生”。它应该让人重建代理提出了什么、变更前服务报告了什么、人员批准了什么、网关发送了什么，以及服务返回了什么。

记录规范化后的字段，而不是只保存原始请求字符串。应记录操作名称、代理会话身份、父账户 ID、资源 ID、预期版本、选择时间、批准的影响、审批时间、出站方法和路径、响应状态，以及服务商请求 ID。请求材料可能含有敏感值时，保存哈希或脱敏版本。把授权请求头复制进审计日志，就等于创建了第二个凭据存储。

保留预检响应，或保留其受完整性保护的摘要。没有这些信息，后来的审核人员无法判断代理删除错误对象，是因为验证失败、对象在验证后发生变化，还是外部服务的行为与文档不一致。这一区分决定了应该如何修复。

当代理和操作网关运行在同一台机器上时，篡改证据尤其重要。造成事故的进程可能编辑可变的文本日志。Sallyport 的 Sessions 和 Activity 日志来自加密的哈希链审计日志，`sp audit verify` 可以在不使用保险库密钥的情况下离线检查该链。这不会把错误审批变成正确审批，但会提高事后隐藏记录改动的难度。

成功请求和失败请求都要测试日志记录。拒绝、审批过期、版本冲突和格式错误的 ID，可以显示控制措施是否真的阻止了操作。只有成功记录的日志，几乎无法告诉你网关是否会拒绝危险请求。

## 把破坏性操作构建成狭窄契约

最安全的 API 操作往往非常单调。它只接受一个已知操作，要求已知的父级和对象 ID，执行预检，并明确说明影响。通用接口看起来高效，直到代理发出你没有预料到的请求，而你唯一的防线变成希望操作人员注意到某个细微参数。

先盘点所有可能删除、撤销、轮换、禁用、覆盖、发布或触发收费的操作。针对每个操作，写下不可变目标字段、父级字段、允许状态、级联行为、新鲜度机制、重试行为和审批文案。如果无法说明这些事实，就暂时不要把该操作提供给自主代理。

然后让错误输入有意识地通过网关：在错误账户下使用有效资源 ID；使用匹配显示名称但得到两个结果；使用过期 ETag；传入空的批量列表；省略筛选条件；让目标以同一名称重新创建；以及发送请求后发生超时。这些输入可以揭示边界验证的是请求含义，还是仅仅验证了 JSON。

不要通过让代理写更长的计划来解决歧义。应在操作契约中让歧义无法表达。代理可以提出意图并收集证据，凭据边界则负责决定这些证据是否在此刻、此账户下，准确指向一个获准的影响。这样的分工让你在请求平常时可以检查，在请求异常时也值得信任。
