# AI 代理如何在安全控制下创建拉取请求

通过 API 创建拉取请求的 AI 代理确实能节省工程时间，但前提是仓库把每个生成的变更都视为不受信任、且作者可追溯的贡献。边界其实很简单：代理可以准备提议中的变更，是否应该进入代码库，则由人工和仓库控制来决定。

我见过一些团队把注意力都放在模型的代码质量上，却忽略了代理周围的权限，结果让系统变得不安全。真正糟糕的失败通常都很普通。面向测试服务的任务落进了生产仓库。一个范围过大的令牌悄悄获得了所有项目的写权限。代理在超时后连续创建了十个几乎相同的拉取请求。有人因为标题听起来合理就合并了其中一个。

安全的设置不依赖代理是否足够谨慎。它会限制代理可以在哪里操作，要求变更走正常的评审流程，并留下足够详细的记录，以便日后调查。

## 仓库范围必须明确，并由机器强制执行

代理应获得一组明确命名的仓库权限，而不是先授予通配符组织范围，打算以后再收窄。仓库范围要回答一个具体问题：这个进程可以读取哪些代码库、在哪些代码库中写入分支，以及针对哪些代码库创建拉取请求？

不要把允许列表放在代理的指令文本中。提示词可以引导行为，却不能执行授权。持有仓库凭据或发起 API 请求的组件，必须拒绝不在允许列表中的仓库。

对每个允许的仓库，定义可用的基础分支和允许写入的命名空间。一个实用的记录如下：

```yaml
repositories:
  - name: acme/payments-api
    base_branches: ["main", "release/2025.1"]
    write_branch_prefix: "agent/"
    pull_request_drafts: true
  - name: acme/docs
    base_branches: ["main"]
    write_branch_prefix: "agent/"
    pull_request_drafts: false
```

这个片段可以防止一种常见故障：代理收到“修复结账页面文案”的请求后，在它能够搜索的仓库中找到相似文件，因为凭据允许它写入，就把文件写到了那里。允许列表会把这种情况变成一次被拒绝的请求，而不是交给别人处理的清理任务。

范围也包括仓库操作。大多数代理只需要读取文件、创建分支、推送提交、读取检查结果，以及创建或更新拉取请求。它们很少需要修改仓库设置、注册 Webhook、修改受保护分支规则、添加部署密钥、管理成员或合并变更。不要因为一个方便的宽权限令牌把这些权限打包提供，就全部授予代理。

使用独立的机器人身份，不要使用开发者的个人访问令牌。个人令牌会让归属变得模糊，工作变化后也很难妥善处理，而且往往带有没人记得授予的权限。机器人身份能让你在行为出错时明确暂停一个操作者。

读取权限也需要像写入权限一样认真讨论。能够查看每个私有仓库的代理，可能会通过自身的日志、任务上下文和响应泄露源代码或配置。即使代理从未拿到直接写入凭据，也应给予它完成任务所需的最小仓库集合。

## 分支前缀是执行边界，不是命名偏好

代理只能在专用前缀下创建分支，例如 `agent/`，而且仓库服务器应强制执行这一限制。写在提示词中的约定迟早会被破坏，原因可能是格式错误的工具调用、重试漏洞，也可能是代理试图完成范围过大的任务。

保护 `main`、发布分支、环境分支，以及任何会自动部署的分支。代理不能向这些分支推送，也不能强制推送，更不能修改保护它们的规则。

分支名称要足够确定，便于调查，同时也要足够独特，避免冲突。可以加入任务引用，再加上短随机后缀或运行标识：

```text
agent/OPS-1842-retry-payment-7f3a
```

不要让代理直接使用任务标题作为分支名称。标题可能包含密钥、客户名称、不安全字符或误导性措辞。应由控制器生成分支名称，再将其作为不可变值传给代理。

基础提交也需要明确规则。控制器开始运行时，应将批准的基础分支解析为提交 SHA 并记录下来。代理应从这个 SHA 创建分支，而不是从一次漫长的代码生成过程结束后 `main` 所指向的位置创建分支。这样不能消除偏移，但能让偏移可见且可复现。

一个分支只能包含与分配任务相关的提交。这意味着不能顺手进行格式化扫描，不能加入无关的依赖升级，也不能因为附近的代码看起来奇怪就尝试“清理”它。生成的变更往往显得很有把握，评审者容易漏掉夹在合理补丁中的无关修改。

在工作开始前设置变更预算。预算可以限制修改的文件数量、总行数，或请求组件之外的路径。预算本身不是风险评估，而是一个触发器，告诉代理停止并请求新的任务，避免把一次小修复悄悄变成全仓库重写。

## 创建拉取请求需要经过检查的 API 事务

HTTP 请求返回成功，并不能证明代理创建的是正确的拉取请求。控制器必须在报告成功前，核对仓库、头部分支、基础分支、提交 SHA，以及返回的拉取请求标识符。

对于 GitHub 风格的 API，有意义的请求字段包括提议的标题、`head`、`base`、正文和草稿状态。具体端点会因代码托管平台而不同，但安全检查不会变：

```json
{
  "title": "OPS-1842: retry transient payment gateway failures",
  "head": "agent/OPS-1842-retry-payment-7f3a",
  "base": "main",
  "body": "Task: OPS-1842\nBase commit: 4b2c...\nTests: unit payment retry suite\nLimits: no configuration changes",
  "draft": true
}
```

发送请求前，查询分支并确认它的顶端 SHA 等于本次运行记录的提交。收到响应后，再获取拉取请求，比较其中的 `head`、`base` 和状态是否与请求一致。记录平台提供的不可变拉取请求编号或节点标识，不要只记录 URL。

重试需要特别处理。网络超时会造成经典的重复拉取请求问题：服务器可能已经创建了拉取请求 418，但客户端没有收到响应，于是再次尝试。控制器中应保存幂等记录，包括任务 ID、仓库、分支、基础 SHA 和拉取请求编号。重试时，先查找已有分支和拉取请求，再发起创建调用。

不要把任务标题作为唯一的幂等值。像“更新生成的文档”这样的重复请求会与之前的运行冲突。运行标识应识别一次运行，而任务 ID 用来帮助人们关联相关工作。

草稿拉取请求适合作为代理工作的默认设置。它告诉评审者变更已经存在，但还没有通过创建者自己声明的完成门槛。只有在必需命令执行完毕且运行记录包含结果后，代理才能将拉取请求标记为准备评审。如果仓库不使用草稿，则应由可信控制器添加类似 `agent-created` 的标签，而不是使用代理生成的文本来完成这件事。

## 评审者分配必须遵循所有权和风险

第一位评审者应来自仓库的所有权规则，而不是让代理猜测谁比较了解相关内容。GitHub 的 CODEOWNERS 文档描述了一种文件到所有者的映射，可以根据修改路径请求评审。GitLab 提供类似的审批和代码所有者机制。这些文件适合作为路由数据，但除非分支保护或合并规则强制要求，否则它们不会自动让评审变成必需条件。

这个区别很重要。根据设置不同，仓库可以显示已请求代码所有者评审，却仍然允许没有其批准的合并。要把路由和强制执行视为两个独立控制。在信任策略前，用一个明确未授权的测试拉取请求检查两者。

应使用最终提交后的变更文件列表，而不是代理计划修改的路径。生成的补丁可能在运行后期才触及共享库、部署目录或迁移文件夹。计算评审者时必须看到实际发生的变更。

当任务涉及所有权规则过于宽泛或没有覆盖的区域时，加入一位负责人。这个人负责确认任务意图。代码所有者可以确认实现是否适合某个组件，负责人则可以确认请求的行为是否符合产品需要。不要因为差异跨越了多个边界就分配十几个人。过长的评审名单往往会带来一个熟悉的结果：每个人都以为其他人已经处理了最难的部分。

某些路径应强制走更严格的流程，例如数据库迁移、授权代码、构建和发布定义、依赖清单、基础设施配置以及生成的构件。正确的做法不一定是“阻止代理”，很多时候应该是“要求了解后果的负责人参与”。迁移可能通过单元测试，却仍然让回滚变得不可能。

只有在拉取请求创建后，才能通过 API 分配评审者，并且要核对最终分配结果。如果某个所有者群组无法接收请求，控制器应将拉取请求标记为阻塞，而不是悄悄换成随机开发者。静默回退会把所有权规则变成装饰品。

## 测试描述证据，审批决定是否接受

代理应准确说明运行了什么、没有运行什么，以及原因。“测试通过”如果没有命令、退出状态和被测试的提交 SHA，就是没有价值的说法。除了拉取请求正文，还应在其他位置保存这些证据，因为代理之后可以修改正文。

每次运行都使用一份小型结构化报告：

```json
{
  "run_id": "run_01J...",
  "repository": "acme/payments-api",
  "head_sha": "8c71...",
  "commands": [
    {"command": "npm test -- payment-retry", "exit_code": 0},
    {"command": "npm run lint", "exit_code": 0}
  ],
  "not_run": ["integration suite requires payment sandbox approval"]
}
```

当证据中的 SHA 与分支顶端不一致时，控制器应拒绝将拉取请求转为准备评审。这能捕捉到一个微妙却常见的流程：代理运行测试后，又做了一次“小修正”，随后没有重新运行任何测试就创建了拉取请求。

必需的状态检查应留在仓库一侧。代理不得拥有豁免检查、批准自己的拉取请求、修改分支保护或合并的权限。检查可以证明某个已知命令成功运行，却不能证明新的授权规则正确、需求被正确理解，或任务本来就应该在那个仓库中执行。

不要让生成的摘要替代差异评审。好的摘要能帮助评审者快速了解情况，但它们仍然是作者提出的说法。评审者需要查看实际文件变更、测试、相关问题上下文，以及有意省略的内容。

## 每个创建的变更都需要能经受事故调查的审计轨迹

拉取请求 URL 不是审计记录。仓库迁移、分支删除、访问权限变化，或有人编辑描述后，URL 所能提供的信息都会消失。应保留追加式事件记录，让调查人员能够回答：谁发起了运行、哪个进程进行了每次调用、哪个仓库发生了变化，以及系统返回了什么。

至少记录以下字段：

- 运行 ID 和原始任务或工单引用
- 执行操作的机器人身份，以及经过身份验证的代理进程身份
- 仓库、基础分支、基础 SHA、头部分支，以及每个创建的提交 SHA
- API 请求类型、不可变的拉取请求标识符、时间戳和结果状态
- 评审请求、批准、检查结果、关闭、合并或拒绝事件

默认情况下，不要把明文凭据、完整源文件或任意任务提示词放入审计日志。调查人员需要的是可靠的操作事实，而不是另一份失控的敏感材料副本。如果保留补丁内容，应记录内容摘要，并遵循正常的保留和访问规则。

活动日志和决策日志的区别值得保留。活动日志说明某个 API 调用创建了拉取请求 418。决策日志说明谁批准了该代理会话、谁修改了它的授权，以及谁撤销了会话。发生事故时，两者都很重要。你需要知道发生了什么，也需要知道当时为什么这个操作者拥有权限。

Sallyport 可以在加密、哈希链式审计日志的基础上，将代理会话以及单独的 HTTP 或 SSH 调用投影到日志中，而 `sp audit verify` 可以在离线状态下对密文检查哈希链。这适合一种设置：代理请求执行操作，但从未直接获得仓库凭据。

哈希链可以让后续修改变得可检测，但不能让一份薄弱的事件记录自动变得完整。在操作发生的边界记录仓库和提交身份。一份完美验证的“已发送 HTTP 请求”记录，仍然无法告诉你该请求是否创建了错误的拉取请求。

## 将凭据留在代理之外，并把创建与合并分开

代理绝不能在提示词、环境变量、工作区文件或工具输出中持有宽权限仓库令牌。令牌一旦进入这些上下文，就可能通过日志、Shell 历史、错误消息、复制的会话记录，或诱使代理打印令牌的指令泄露。事后脱敏无法可靠地让秘密重新回到控制之下。

使用操作网关或范围狭窄的控制器，接受明确的请求，例如在这个仓库中创建分支、将这些提交推送到允许的前缀、针对批准的基础分支创建草稿拉取请求，或请求这些评审者。网关负责注入凭据并返回结果，同时拒绝超出声明范围的调用。

创建权限和合并权限是两种不同的特权。能够创建拉取请求的服务可能被允许提出成千上万个糟糕的变更，而能够合并的服务则可以把一个糟糕的变更送入生产环境。不要为了让演示更顺畅就把两者合并。即使代理生成了完美补丁，也应由受保护的分支规则和负责任的人工来控制合并操作。

按运行授权也优于永久信任代理进程。代理工具可以启动子进程，被意外复用于其他任务，并在原本授权访问的任务结束后继续存活。为进程提供一个有边界的会话，记录审批，并让撤销立即生效。

## 故障演练能暴露政策文字隐藏的缺口

在依赖自动创建拉取请求前，先进行一次受控故障演练。使用测试仓库或一次性分支策略，并给代理一个试图跨越每道边界的任务。重点是验证拒绝和记录行为，而不是欣赏顺利完成的演示。

先准备一个允许的仓库和一个禁止的仓库。确认控制器能在前者创建预期的草稿拉取请求，并在任何分支出现前拒绝后者。然后尝试向 `main` 推送、强制推送到 `agent/` 分支，以及针对未经批准的发布分支创建拉取请求。检查仓库事件，不要只看控制器消息。

接着，模拟创建请求到达仓库 API 后发生超时。重新启动运行，确认它找到原来的拉取请求，而不是再创建一个。记录测试结果后修改分支，验证准备评审的转换会因 SHA 不匹配而停止。

最后，在运行过程中撤销代理会话，再尝试进行一次 API 调用。调用应失败，拒绝应出现在决策记录中，并且代理输出中不能出现任何凭据。如果这些测试有任何一步依赖人工注意到聊天消息，那么这项控制实际上还不存在。

最实际的第一步，是盘点代理当前使用的令牌。列出它可以接触的所有仓库、可以更新的所有分支，以及它是否能够合并或修改设置。大多数团队都会发现令牌范围大于任务本身。让代理再创建一个拉取请求之前，先收窄权限。
