哪些智能体拉取请求关卡应阻止合并?
使用七个可衡量的智能体拉取请求关卡阻止不安全代码:测试、CodeQL、依赖项、密钥、授权、迁移和回滚。

智能体可以生成整洁的差异、有说服力的说明,以及能通过快速审查的代码,却仍让应用暴露风险或无法恢复。因此,是否合并应取决于仓库能够衡量的证据,而不是智能体听起来多么自信,或补丁看起来多么小。
我使用七个阻断关卡:测试、CodeQL、依赖项审查、密钥扫描、授权检查、迁移演练和回滚验证。每个关卡回答不同的失败问题。测试套件全绿不能证明新软件包安全,静态分析结果干净也无法说明数据库迁移不会锁住最繁忙的表。
这些关卡同样适用于人工和智能体变更。智能体创作会改变错误的数量、速度和形式,却不能因此走一条单独且更宽松的流程。如果一项变更无法提供与其他拉取请求相同的证据,就还不能合并。
关卡必须产出证据,而不是建议
合并关卡应对即将进入受保护分支的精确提交,给出可复现的通过或失败结果。写着“请审查这个依赖项”的评论是建议。能标识软件包、版本、安全公告和严重性阈值的必需检查才是证据。
这个区别很重要,因为许多安全功能默认在有用的决策点之后才报告。扫描器可能创建告警、发送邮件或创建议题,但合并按钮依然可用。团队于是说扫描“已启用”,尽管它无法阻止变更。对每个关卡,都要验证四项属性:
- 它针对拉取请求当前的头部提交运行。
- 分支保护要求其指定结果。
- 被跳过、超时或崩溃的任务不能算作通过。
- 结果记录了足够细节,以便复现决策。
把策略保存在仓库中。一个小型清单比只有管理员知道的一堆设置更易审查:
merge_gates:
tests: required
codeql: required
dependency_review: required
secret_scan: required
authorization: required
migration_rehearsal: required_when_changed
rollback_verification: required
required_when_changed 不是漏洞。它表示关卡先检测相关文件,然后执行演练,或记录清晰的“不适用”结果。不要让路径过滤器使必需检查永久挂起,也不要让智能体自行决定自己的高风险变更可免于检查。
将工作流权限固定为每个任务所需的最小范围。拉取请求代码属于不可信输入,即使分支来自你的组织。若某个关卡把写入令牌或生产密钥暴露给它所检查的代码,可能会制造比原本要发现的问题更严重的风险。
保护检查的身份与保护其逻辑同样重要。分支规则通常要求一个状态名称,因此两个能报告同一名称的工作流可能让较弱任务满足规则。给策略任务一个唯一名称,限制谁能修改其工作流,并要求该文件所有者审查。合并队列创建新的合并提交时,应在该提交上再次运行关卡,或使用将结果绑定到排队修订版本的平台功能。昨天头部提交的证据不是今天合并的证据。
把关卡配置本身当作敏感代码。修改阈值、移除路径、降级查询包或添加例外的拉取请求,会改变之后每个绿色结果的含义。应突出显示策略差异,并要求理解受影响控制措施的维护者审查。智能体可以提出此类变更,但不能因为修改了评判自己的机制就更容易通过。
测试阻止可观察到的回归
测试关卡应阻止任何在支持的运行时和数据库版本上破坏既定行为的变更。它必须运行合并提交将使用的同一组构建输入,包括锁定依赖项、生成文件、功能开关和模式状态。
智能体尤其擅长满足最近的一条断言。它们可能添加一个兜底逻辑,让一个测试变绿,却破坏错误处理、分页、并发或相邻 API 契约。变更行为时,要求拉取请求新增或修改测试,但不要以新增测试行数衡量质量。应审查:如果实现被移除或回退,测试是否会失败。
有用的测试关卡分层,并采用不同名称:
- 单元测试,用于本地逻辑和边界情况。
- 集成测试,用于数据库、队列、缓存和外部服务契约。
- 契约测试,用于公开请求和响应格式。
- 针对已构建构件的小型冒烟测试。
对不稳定测试应追查根因,而不是自动重试到变绿。一次重试可以收集诊断证据,但最终状态应显示初始失败。否则,智能体可以合并唯一被证明“有时能运行”的代码。
冒烟测试应启动构件,访问健康检查端点和一条有意义的写入路径,再干净地停止。只测试源代码却不启动打包后的应用,会漏掉缺失文件、错误环境默认值、损坏迁移和启动崩溃。对于 Web 服务,结果可记录为简洁格式:
{"commit":"abc123","build":"pass","startup_ms":842,"health":200,"write_read_cycle":"pass"}
不要把通用覆盖率百分比设为主要关卡。覆盖率能发现未测试的变更,但仓库可以在断言薄弱的情况下达到很高比例。应以必需套件和已变更行为作为关卡,再把覆盖率变化用作审查证据。
保护测试不受其评判的实现影响。如果一个拉取请求既修改规则,又重写断言来接受新结果,套件可能通过,而契约已悄然改变。要求审阅者将修改后的测试与公开 API、议题或验收规则比较。对于解析器、验证器、计费逻辑和访问检查,可加入变异测试或一组刻意错误的输入。真正有价值的问题是:套件能否拒绝看似合理的错误实现,而不是智能体能否让自己编写的实现满足自己写的断言。
关卡失败时保留测试构件。保存随机测试的失败种子、精确的数据库镜像、已去除密钥的服务日志,以及可复现运行的命令。没有复现数据的状态会让下一位智能体或工程师重新猜测。按仓库的数据规则限制构件保留时间,绝不能为了方便复现失败而上传生产快照。
CodeQL 阻止通向漏洞的已知代码路径
CodeQL 关卡应阻止在 CodeQL 实际分析的语言和生成构件中出现的、高置信度新发现。它不应暗示干净结果证明整个应用安全。
GitHub 将 CodeQL 描述为把代码编译为可查询数据库,再对其运行查询。这个模型很有用,因为它跟踪数据在代码中的流动,而不只是匹配可疑文本。但它受语言支持、构建是否成功、查询选择和分析时存在的代码限制。如果数据库构建悄悄排除了某项服务,绿色结果覆盖的范围就比审阅者以为的更小。
使用明确语言和固定查询策略的工作流:
name: codeql
on: [pull_request]
permissions:
contents: read
security-events: write
jobs:
analyze:
strategy:
matrix:
language: [javascript-typescript, go]
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
queries: security-extended
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3
在真实仓库中,应将第三方 action 固定到经过审查的提交摘要。标签让示例更易读,但可变标签会扩大必需安全任务的信任边界。
在出现第一条告警前决定哪些情况会阻断。我通常会阻止达到仓库约定严重性和精确度阈值的新发现,同时让既有债务保留在基线中。第一天就阻断所有历史结果会鼓励大规模忽略。永远忽略既有结果则会留下永久盲区。应为基线分配负责人和截止日期。
当服务、语言或构建命令变更时,检查已分析文件集。引入新的移动端客户端、生成的解析器或独立后端的拉取请求,可能需要另一个分析器或构建步骤。只有审阅者能说明 CodeQL 检查了什么,“CodeQL 已通过”才有意义。
将分析失败与干净分析区分开。如果 autobuild 无法编译软件包,任务必须报告基础设施或配置失败,而不是零发现。保存数据库创建日志和按语言统计的已分析源文件数。将这些数量与基础分支比较,并标记无合理解释的大幅下降。这能捕捉一种常见失败:构建编辑排除了存在漏洞的模块,分析变快,安全检查因看到的代码更少而变绿。
把驳回结果视为策略变更,而不是清理工作。误报需要与代码路径和查询相关的具体解释。抑制注释应范围精确、有人负责,并在拉取请求差异中可见。对生成文件的仓库范围排除或许合理,但首先要确认该目录没有人工维护的模板或生成器输入。
依赖项审查在安装前阻止风险
当依赖项差异引入违反明确策略的软件包或版本时,依赖项审查应阻止拉取请求。策略可包括已知安全公告的严重性、禁止的许可证、意外的软件包来源,以及没有负责人新增的直接依赖项。
这个关卡不同于仓库漏洞告警。告警告诉你分支中存在易受攻击的依赖项。依赖项审查则问:这个拉取请求是否让依赖关系图变得更糟。GitHub 的依赖项审查会比较拉取请求两侧的清单和锁定文件,因此决策及时且可追溯。
要求锁定文件保持一致。如果智能体修改 package.json 却不修改锁定文件,或修改锁定文件却没有对应的清单变更,任务应失败。CI 中解析全新版本的安装命令会让结果无法确定,也可能测试了不同于审阅者所见的依赖关系图。
简洁策略可写为:
dependency_policy:
fail_on_severity: high
deny_licenses:
- AGPL-3.0
allow_sources:
- registry.npmjs.org
- proxy.golang.org
require_owner_for_direct_additions: true
具体许可证列表属于法律和产品决策,不应盲目照抄。关键在于仓库明确声明它,检查能打印触发规则的软件包。
不要因为软件包名称像某个推荐库就自动批准。智能体可能臆造软件包名称、选择被遗弃的分支,或为了一个简单辅助功能加入庞大客户端。审查输出应展示新增的直接和传递软件包、源注册表、解析版本、许可证和安全公告状态。审阅者随后可以判断现有代码或更小的依赖项是否已能解决问题。
如果依赖项服务无法产生差异,应默认失败。安全公告源不可用可以成为暂停合并的理由,不能把不确定性变成绿色检查。紧急流程可允许指定维护者进行有文档记录的覆盖,并将理由附在提交上。
在隔离任务中检查安装行为,并把网络访问限制在已批准的注册表。生命周期脚本和构建插件会在安装期间执行代码,因此即使应用从未导入某个软件包,它也可能有危险。记录新依赖项是否加入安装脚本、原生二进制文件或陌生注册表。不要使用发布凭据、云令牌或与可信构建共享的可写软件包缓存运行此任务。
供应商代码和容器镜像应纳入同一决策,即使普通清单审查可能漏掉它们。比较镜像摘要、基础镜像名称、Git 子模块和提交的归档文件。对发布输入要求不可变摘要。latest 这类友好标签会让拉取请求日后无法复现,因为字节可以在没有新差异的情况下改变。
密钥扫描必须检查差异及其历史
即使字符串出现在测试夹具、已删除文件、生成包或拉取请求中的早期提交里,只要拉取请求引入凭据模式或已验证的有效密钥,密钥扫描就应阻止合并。
推送保护和拉取请求扫描解决的是相关但不同的问题。推送保护可以在已识别的密钥到达远程仓库前阻止它。拉取请求关卡检查已抵达的内容,并可覆盖推送保护漏掉的贡献者或令牌类型。没有分支保护的告警不会阻止合并。
扫描相对基础分支的完整提交范围,而不是只扫描最终文件系统。智能体可能在一次提交中加入令牌,下一次又删除它;令牌仍在 Git 历史中,且可能已进入日志或缓存。应把它视为已泄露。撤销或轮换凭据,从拟议历史中移除它,然后重新运行检查。
使用无法认证的合成夹具。夹具应清楚标识自身,并匹配本地定义的测试模式,而不是复制真实云密钥的形态。宽泛的允许列表很危险,因为攻击者和意外情况最终都会找到被忽略目录。例外应精确、经过审查,并靠近检测器配置。
将模式检测器与熵检查结合,并在提供方可安全支持时结合凭据验证。模式匹配的网络和泄露风险较低,却会漏掉自定义令牌。验证可以减少不确定性,但会把候选密钥的一部分发送给另一个服务,绝不能针对拉取请求提供的不可信端点运行。记录哪些检测器会验证,哪些仅在本地运行,以及哪些数据会离开 CI。
扫描常见编码形式和生成输出,但不要把每个随机字符串都当作凭据。Base64、URL 编码和压缩包都可能隐藏在源步骤中明文出现的密钥。根据已测试夹具设置阻断阈值,再像审查其他策略变更一样审查检测器更新。嘈杂的扫描器会训练维护者忽略结果,沉默的扫描器则会制造虚假信心。
关卡输出需要定位信息,但不能泄露内容:
{"result":"fail","detector":"generic-api-token","commit":"abc123","path":"config/dev.env","line":7,"fingerprint":"sha256:8f2c..."}
绝不要在 CI 日志或拉取请求评论中打印完整匹配内容。扫描器输出后再掩码可能已经太迟,因为日志系统、通知和任务构件可能已复制它。
密钥扫描不能替代仓库权限审查。工作流可以读取生产凭据而不把它放进差异,智能体也可以修改部署任务来外泄它。让密钥远离拉取请求任务,限制工作流权限,并要求人工审查 CI 定义变更。
授权检查证明禁止操作仍被禁止
授权关卡应证明,每项受保护操作都会在服务边界拒绝错误行为者,并允许预期行为者。仅有登录测试并不能测试授权。
团队经常混淆认证、授权和用户界面可见性。认证确认是谁发出了请求。授权决定该身份能否对这个对象执行此操作。在 React 中隐藏管理员按钮,既不会改变 Go API 的认证,也不会改变其授权。只要后端接受请求,应用就仍有风险。
为已变更端点和业务操作建立权限矩阵。它应足够小,便于审查,同时包含所有权和租户边界:
read_private_project:
anonymous: deny
member: deny
other_tenant: deny
owner: allow
admin: allow
update_project:
anonymous: deny
other_tenant: deny
owner: allow
从这个矩阵生成测试,或在服务语言中编写等效的表驱动案例。每个拒绝案例都应使用真实标识符调用真实处理程序。替换授权中间件的模拟可以证明路由可用,却跳过了正在测试的控制措施。
测试对象级访问,而不仅是角色。两名用户都可能具有 member 角色,却属于不同组织。分别改变资源标识符和租户标识符,以捕捉不安全的直接对象引用。还要测试批量端点、导出、后台任务和 GraphQL 解析器,它们经常绕过普通 REST 处理程序已有的检查。
如果新的受保护路由没有策略映射,关卡应失败。这能把缺少授权从审阅者的直觉变成可衡量的缺陷。服务端默认应拒绝。显式允许规则比散落在代码中、只拒绝几个已知坏情况的逻辑更易审计。
智能体经常复用相邻处理程序,保留其正常路径,却遗漏所有权检查。权限矩阵使这种遗漏显而易见。角色或租户规则日后变化时,它也为审阅者提供稳定契约。
在输入标准化后测试授权。大小写折叠、替代标识符格式、重复查询参数和嵌套对象引用,会让等价请求走不同代码路径。测试直接端点,以及所有到达同一操作的批量或导入路由。如果后台工作器执行最终写入,应将行为者和租户上下文传入任务,而不是把工作器当成全能可信用户。
记录预期决策及产生该决策的策略规则,但避免在日志中暴露私有对象数据。有用的失败信息会说明行为者类别、操作、对象类别和预期状态,而不会转储访问令牌或完整记录。这些证据能让审阅者区分损坏的测试夹具和真实的权限变化。
迁移演练衡量锁定和可逆性
迁移关卡应将每项拟议模式变更应用到接近生产形态的副本,运行兼容性探针,并在合并前记录耗时、锁定和回滚行为。在空测试数据库上成功的迁移几乎证明不了什么。
使用经过脱敏的快照或生成数据集,使其表大小、索引、约束和数据倾斜情况相似。精确的生产数据不应进入拉取请求基础设施。目标是在不复制个人或机密记录的前提下重现运维压力。
演练实际部署顺序。如果迁移运行期间旧应用实例仍在运行,就用旧代码测试新模式,用新代码测试过渡模式。添加可为空列通常兼容。一步重命名列可能破坏所有仍在提供流量的旧实例。
以机器可读结果记录证据:
{"migration":"20260727_add_project_state","apply_ms":18420,"max_lock_ms":310,"old_app_probe":"pass","new_app_probe":"pass","down":"pass"}
按数据库和表类别设置阈值。300 毫秒锁定对一张表可能无害,对另一张表则可能造成中断。审阅者应看到所选限制及测得结果,以及演练使用的数据库版本。
对于 PostgreSQL,检查会重写表、验证约束或在阻塞写入时构建索引的操作。优先采用扩展和收缩式变更:添加新结构,部署可使用两种结构的代码,分批受控回填,切换读取方式,再在后续变更中移除旧结构。额外一个拉取请求比在故障期间临场应对成本更低。
并非每项迁移都能安全执行 down 操作。删除列会丢失数据,逆转转换也可能含糊不清。在这些情况下,关卡应要求前向恢复流程和经过测试的备份还原点。仅因存在 down 文件就称不可逆迁移“可安全回滚”,并不诚实。
测试重试和部分失败。部署可能在创建索引后、将迁移标记为完成前停止,下一次尝试不能破坏状态或永久失败。在受控边界中断演练,重新运行,并验证模式与迁移账本一致。对于长时间回填,应证明批次能从已记录游标恢复,且同一批次运行两次不会重复或删除数据。
除耗时外,还要检查磁盘和复制影响。表重写可能消耗临时空间、扩大预写日志,并在主操作看似完成后延迟副本。关卡不需要完美预测生产情况,但应在演练数据集上记录这些量,并与数据库所有者设定的限制比较。没有这些证据,一项快速的本地迁移仍可能耗尽生产磁盘空间。
回滚验证必须运行恢复路径
回滚关卡应将候选版本部署到隔离环境,创建具有代表性的状态,触发受支持的回滚方法,并证明先前版本仍能正确提供读写服务。书面的回滚计划不是验证。
区分应用回滚和数据回滚。将流量重新指向旧二进制文件可能只需几秒,而撤销破坏性模式或数据转换可能根本做不到。关卡应报告两者。如果旧应用无法在新模式上运行,应将候选版本标记为不可逆,并要求分阶段部署计划。
实用顺序如下:
- 部署当前 main 提交,并写入有代表性的记录。
- 升级到拉取请求构件,并执行已变更路径。
- 在升级后的状态中创建新记录。
- 使用已有文档的控制方式恢复先前构件或快照。
- 运行读、写、队列和后台任务探针。
检查应保留构件标识符、快照标识符、时间戳和探针结果,不应保留凭据或复制的客户数据。将恢复时间作为你自身运维目标的证据,而不是普遍承诺。
只有有人证明快照包含应用所需的一切,快照才有用。文件、对象存储、队列状态、模式变更和外部副作用可能位于服务器快照之外。在结果中列出这些边界。已经发出的付款、邮件或 webhook 无法通过还原数据库收回。
让回滚探针强于健康检查。读取升级前创建的记录,读取升级后创建的记录,在兼容允许时更新两者,并处理每个应用版本创建的一项排队任务。比较用户可见输出,而不只是状态码。返回 200 却丢弃新字段或错误读取枚举的服务器,并没有恢复。
测试值班人员实际使用的控制方式。如果生产回滚需要选择构件、还原快照或变更流量,隔离演练应使用同一接口和权限路径。一位工程师笔记本上的私有脚本不是运维控制。证据应显示指定操作员角色可在不获取永久管理员权限的情况下完成恢复。
Koder.ai 支持源代码导出、部署和托管、快照与回滚,因此在其上构建的应用可在这个关卡中使用这些具体构件,而不是把恢复当作拉取请求中的一段文字。同样的规则适用于任何平台:运行恢复控制并探测已还原的应用。
一个必需检查应汇总全部七项
最终合并决策应要求一个稳定的策略检查,为精确头部提交验证七项底层结果。单个任务名称会变化,矩阵任务会增多,可选路径会跳过工作。一个小型聚合器可防止分支保护逐渐偏离策略。
让每个关卡输出带签名或平台证明的结果,其中包含提交、策略版本、结果和证据位置。聚合器应拒绝缺失、过期、中性或取消的结果,绝不能从未报告结果的任务中推断成功。
{
"commit": "abc123",
"policy": "merge-gates-v3",
"results": {
"tests": "pass",
"codeql": "pass",
"dependency_review": "pass",
"secret_scan": "pass",
"authorization": "pass",
"migration_rehearsal": "not_applicable",
"rollback_verification": "pass"
},
"decision": "allow"
}
由于紧急情况总会发生,应定义狭窄的覆盖路径。要求指定维护者、第二位批准者、书面理由、到期时间和后续议题。不要让智能体请求或批准自己的例外。在与普通关卡结果相同的位置报告覆盖,以便事件过去后仍然可见。
这些检查会为某些拉取请求增加几分钟,为迁移变更增加更多时间。当这些时间换来具体证据时,这是可以接受的。尽早运行低成本关卡,取消被取代的提交,缓存可信构建输入,并仅对相关路径保留完整环境演练。不要只为了让仪表盘更快变绿就削弱关卡。
先让当前行为变得可观察。把七个名称都放进一个策略文件,将每个名称连接到必需结果,并强制被跳过的工作说明原因。第一个无法产出这份记录的智能体拉取请求,会在生产环境之前发现交付系统中的漏洞。
常见问题
智能体的拉取请求应比人工拉取请求遵守更严格的规则吗?
两者都应使用同样的阻断性证据。智能体变更速度更快,或许需要更多自动检查,但人工编写的差异同样可能暴露密钥、引入授权漏洞或不安全的迁移。
人工审阅者可以覆盖失败的合并关卡吗?
可以,但只能走严格且有记录的例外流程,需要两位负责人员、明确理由和到期时间。生成变更的智能体绝不能批准自己的例外。
CodeQL 检查通过是否表示拉取请求是安全的?
不能。它只说明所选查询在 CodeQL 成功分析的代码中没有发现需要阻断的结果。依赖项、运行时授权、密钥、配置和运维恢复仍需要独立证据。
必需的扫描器不可用时该怎么办?
检查应保持阻断状态或默认失败。如果变更确属紧急情况,应走已有文档的例外流程,而不是把未知结果当作通过。
密钥扫描应检查已删除提交吗?
应检查拉取请求的完整提交范围。先加入后删除的密钥仍留在历史中,应先轮换密钥,再接受已清理的变更。
如何在拉取请求中测试授权?
用一组身份、角色、租户、对象和操作调用真实服务处理程序。应包含拒绝案例和对象所有权变更,因为登录成功和隐藏界面控件都不能证明服务器授权正确。
所有数据库迁移都需要 down 脚本吗?
不需要。有些破坏性变更无法诚实地撤销。应改为要求经过测试的前向恢复或备份还原流程,并将变更标记为不可逆。
回滚规划和回滚验证有什么区别?
规划描述预期的恢复步骤。验证则会针对候选构件和具有代表性的状态实际执行这些步骤,并记录旧版本能否继续正确读写。
团队怎样避免七个关卡拖慢每个拉取请求?
先运行成本低的检查,取消被后续提交取代的运行,缓存可信输入,并且只对相关变更触发迁移演练。每个关卡都必须给出明确的通过或不适用结果。
分支保护应要求什么结果?
要求一个稳定的策略结果,汇总精确头部提交的全部七个关卡。它应拒绝缺失、过期、跳过、取消或中性结果,不能猜测它们已经通过。