本地审批与上游审批如何改变代理控制
用本地审批与上游审批决策矩阵比较延迟、上下文、身份、故障、重试和审计证据,为 AI 代理操作选择合适的控制点。

审批按钮本身并不是安全属性。它的价值取决于决策在哪里执行、审核者能看到什么、服务记录谁的身份,以及超时或连接中断后会发生什么。
对于调用 API 的 AI 代理,本地审批和上游审批回答的是不同问题。本地关口可以决定这个代理进程能否使用某项凭据或发送已经准备好的请求。上游关口可以决定远程系统是否应把请求的变更应用到自己的资源上。当 API 已经要求确认时,正确的设计很少是「选择更严格的那个」。应选择掌握待审核事实的关口,再把它留下的证据与操作的其余部分关联起来。
一张清楚的审批卡也可能掩盖很大的授权缺口。审核者可能在本地批准「部署版本 184」,而上游服务收到的却是可重复使用的令牌和另一份负载。上游服务也可能记录了一次部署审批,却不知道请求由一个不受信任的代理进程组装。两条记录都可能属实,却仍然无法解释这次操作。
本地关口与上游关口授权的对象不同
本地审批在请求越过机器边界之前授权能力的使用;上游审批则在资源所属服务内部授权资源状态的变化。把两者当作可以互换的机制,会丢失调用方上下文或资源上下文。
本地关口位于代理、凭据保险库、命令执行器或出站操作网关旁边。它可以检查可执行文件、代码签名、父进程、会话开始时间、所选凭据、目标主机、请求方法和拟用参数。它还可以不把秘密交给代理,而是自行执行调用。它回答的是:「这个本地进程现在是否可以行使这项能力?」
上游关口位于 API 提供方或与其相连的控制平面。它可以查看当前资源版本、组织成员关系、受保护环境、服务策略、冲突状态和审核者的远程身份。它回答的是:「在服务当前状态下,是否应发生这一次确切的远程变更?」
NIST 特别出版物 800-207 描述了策略决策点和策略执行点,并主张让执行点更靠近资源,以缩小隐式信任区。对于只有资源所有者才知道的事实,这项原则支持上游执行。但它没有让本地关口变得多余。除非客户端传递并绑定本地进程身份,服务无法检查这个身份;多数不记名令牌调用识别的是令牌持有者,不是促成调用的进程。
OpenSSH 给出了具体的本地例子。OpenBSD 的 ssh-add 手册说明,-c 选项要求代理每次使用已添加的身份前先确认。签名完成后,远程 SSH 服务器仍会执行自己的授权。一个关口询问本地客户端能否使用密钥,另一个关口判断由此产生的认证是否符合服务器策略。把任何一个称为重复控制,都会漏掉它各自保护的边界。
实用规则很简单:审批必须由能够阻止其所描述操作的组件执行。无法扣住凭据或调用的本地通知只是在表演。变更完成后才记录的上游评论是复核,不是授权。
决策矩阵从待审核的事实开始
选择主要关口时,应评估它能知道和执行什么,而不是数提示框的数量。下面是我在添加审批流程前使用的决策矩阵。
<table> <thead><tr><th>标准</th><th>本地审批</th><th>上游审批</th><th>设计结果</th></tr></thead> <tbody> <tr><td>延迟</td><td>通常只有一次本地交互和调用时间</td><td>还包括网络、提供方队列、通知和审核者等待时间</td><td>对频繁、可逆且本地上下文足够的调用,优先考虑本地审批</td></tr> <tr><td>代理上下文</td><td>可绑定进程、会话、可执行文件权限、工具调用和本地用户</td><td>通常只看到令牌、应用、工作负载或服务账号</td><td>当进程来源会改变决策时,保留本地关口</td></tr> <tr><td>资源上下文</td><td>只能看到已取得或提交的状态,状态可能已过期</td><td>可评估当前版本、保护规则、所有权和冲突</td><td>安全性依赖实时远程状态时,使用上游复核</td></tr> <tr><td>人员身份</td><td>可绑定设备前的人员</td><td>可绑定组织账号、团队角色或职责分离</td><td>使用承担责任的身份域,或同时保留两者</td></tr> <tr><td>凭据暴露</td><td>可让秘密留在代理外,只释放一次操作</td><td>往往在客户端已经持有可用凭据后才开始</td><td>威胁模型包含秘密保管时,本地执行不可少</td></tr> <tr><td>服务故障</td><td>可在本地拒绝并保留待处理意图</td><td>服务或审批平面不可用时无法批准</td><td>定义到期和取消,绝不能把故障解释成同意</td></tr> <tr><td>本地故障</td><td>会阻止该设备上的受控调用</td><td>可能仍可由其他受信任客户端使用</td><td>明确备用客户端是允许路径还是绕过路径</td></tr> <tr><td>审计细节</td><td>可记录提示、拒绝、进程退出和尝试的调用</td><td>可记录接受的请求和权威资源变更</td><td>用稳定的操作标识符连接两边记录</td></tr> <tr><td>防篡改边界</td><td>被攻陷的主机可能攻击本地记录或界面</td><td>远程记录由提供方控制</td><td>不要要求一份日志证明其信任边界外的事件</td></tr> <tr><td>覆盖范围</td><td>可以一致地包装许多 API</td><td>只覆盖提供方暴露给关口的操作</td><td>仅依靠上游审批前先清点未受保护的路径</td></tr> </tbody> </table>不要把这张表变成一个必然产生通用赢家的计分器。有些行具有否决权。如果代理绝不能收到 API 密钥,上游确认无法解决凭据暴露,即使它在资源上下文上得分更高。如果生产部署要求运维组成员批准,启动代理的开发者在本地按下 Touch ID 并不满足职责分离。
选择模式前先给操作分类。泄露风险低的只读调用可能只需要会话授权,不必每次提示。可逆写入可以使用本地关口、短有效期和幂等键。不可逆或受监管的变更通常需要上游复核,因为服务掌握最终状态和组织身份。一条命令若同时涉及秘密使用和不可逆变更,可能需要两个关口,但每个提示都必须说清自己的决策含义。
延迟包括等待、到期和人工恢复
本地审批通常有更低的交互延迟,但真正有意义的指标是得到安全且无歧义结果所需的时间。快速点击之后出现无法判断是否重试的状态,并不算低延迟。
当一个人在 Mac 前监督代理时,本地卡片可以在请求上下文仍然新鲜时出现。审核者可以立即响应,网关也可以直接发送调用,不必再等待一轮通知。这适合创建常规拉取请求、查询受保护内部 API,或在有人看守的会话中运行已知 SSH 命令,前提是本地能够看清操作后果。
上游流程有更多排队点。服务要创建待处理对象、选择合格审核者、发送或显示通知、等待组织身份回应、重新检查当前策略,最后应用变更。如果这段延迟带来了真正的职责分离,或让审核者看到权威状态,它就有意义。如果同一个人面对同一份负载连续批准两次,却没有得到新信息,它只是浪费。
GitHub 的环境文档提供了一个好例子。引用设有必需审核者环境的作业会在开始前等待,并且在获批前无法访问该环境的秘密。GitHub 还允许环境禁止自我审核。这些细节让上游延迟有了意义:关口同时控制服务状态和秘密释放,也可以绑定一位不同于发起者的审核者。本地审批即使显示审核者邮箱,也无法复制这种组织关系。
在生产中至少测量四段时间:创建意图到出现提示、提示到人员决策、决策到执行、执行到权威结果。拒绝和到期也要进入同一组数据。把放弃的请求排除在外后计算的中位审批时间,会让有问题的流程看起来很好。
提示频率也会改变人的行为。如果一个包含五十次相似读取的循环每次都在本地提示,审核者很快会不再阅读而直接点击。把五十个提示全部搬到上游不会解决问题。只有当操作共享边界清楚的能力、明确的目标集合和较短生命周期时才可分组。即使批处理更快,也应让破坏性变更独立审批。
到期时间应反映事实变化的速度。对基于快速变化分支头构建的请求,十分钟可能太长;对正式生产审核,十分钟又可能太短。不要静默续期。如果负载、目标版本、凭据或合格审核者集合发生变化,应创建新的决策。
上下文决定审核者能否判断
有用的提示应包含作出该项决策所需的最小完整事实集。本地系统和上游系统各自看到一半,简单地把一边的界面复制到另一边通常没有用。
本地一侧应以人员可以核验的方式显示请求者:可执行文件身份、可用时的签名机构、父会话、工具名、凭据别名、目的地、操作和易读的负载摘要。界面必须区分网关观察到的数据和代理提供的文字。代理写下的「安全清理」只是一项声明,不是证据。
上游一侧应显示权威对象和拟议变更:仓库与环境、账号与区域、资源版本、差异、策略检查、发起者身份和合格审核者。只收到不记名令牌时,它不应假装知道本地来源。
对规范化操作字段计算摘要,把已批准意图与执行绑定。下面的信封足够小,可以直接实现。action_id 用来连接系统;摘要防止后来的负载借用先前的批准。
{
"action_id": "act_01JQ7M6F4R2K",
"session_id": "ses_01JQ7KZ9J1AA",
"caller": {
"executable": "/usr/local/bin/agent",
"signing_authority": "Developer ID Application: Example Team"
},
"target": {
"service": "deploy-api",
"resource": "production/payments",
"version": "184"
},
"request": {
"method": "POST",
"operation": "promote",
"body_sha256": "98b0...e42c"
},
"approval": {
"scope": "single_action",
"expires_at": "2026-07-24T14:05:00Z"
}
}
执行器必须重新计算 body_sha256,比较资源和版本,确认有效期,并且只消费一次单操作批准。如果代理在批准后修改一个参数,比较就必须以拒绝结束。只要提供方支持,上游请求应在元数据或关联字段中携带 action_id。不要把它塞进会改变业务行为的字段。
截图无法形成牢固绑定。它可以帮助人理解请求,但代码必须执行已批准字节与发送字节之间的关系。我见过系统从一个对象生成友好摘要,稍后却组装另一个对象来执行。要审查序列化路径,不能只看界面。
身份涉及三个角色,不是一个
一次代理操作至少有三个身份:提出操作的进程、执行操作的凭据主体,以及批准操作的人。把三者压成一个 actor 字段,会得到漂亮却回答错误问题的日志。
进程身份可能包含可执行文件路径、哈希、签名机构、父进程、代理协议会话和本地操作系统用户。这些信息都不会自动变成远程身份。API 通常只能看到 OAuth 客户端、服务账号、部署密钥、角色会话或用户令牌。
批准者也属于某个身份域。根据操作系统机制,本地生物识别确认可以证明登记人员当时在设备前,但它未必能证明此人目前在组织中的角色。上游审核者账号可以证明团队成员关系和与发起者的分离,却可能完全不知道哪个本地二进制文件请求了操作。
保留三个身份,并注明绑定方法。
<table> <thead><tr><th>身份</th><th>证据示例</th><th>它回答的问题</th></tr></thead> <tbody> <tr><td>提议者</td><td>进程签名、可执行文件哈希、会话 ID</td><td>哪段正在运行的代码提出请求?</td></tr> <tr><td>执行者</td><td>API 主体、SSH 公钥指纹、角色会话</td><td>哪项权限执行了调用?</td></tr> <tr><td>批准者</td><td>本地用户在场证明或上游组织账号</td><td>谁接受了哪项风险?</td></tr> </tbody> </table>不记名凭据使这件事更重要。如果五个代理进程共享一个令牌,上游审计轨迹可能正确记下令牌主体,却仍无法区分这些进程。本地网关可以补充缺失的来源信息,但前提是会话记录不容易被随意修改,并能与远程事件关联。
人点击批准时,不要把代理写成批准者。服务账号执行请求时,也不要把人写成 API 调用者。应直接记录委托关系:提议者 P 请求操作 A,批准者 H 授权范围 S,执行者 E 应用结果 R。
当服务掌握组成员关系并能阻止自我批准时,职责分离是上游的优势。本地审批更擅长证明用户在场和进程来源。策略若要求两者,应要求不同证据,而不是让同一个人在同一设备上点击两次。
故障必须留下唯一且持久的状态
审批系统把网络错误当成界面问题时,往往会严重失败。操作需要一台持久状态机,能经受进程退出、响应丢失和服务恢复。
使用 created、locally_approved、submitted、upstream_pending、executing、succeeded、denied、expired 和 unknown 等明确状态。终止状态必须保持终止。每次转换都要连同操作摘要和时间保存。本地重启可以继续观察上游待处理请求,但除非重试规则允许,否则不能创建新的变更。
安全的故障规则很严格:
- 本地关口不可用时,受控操作不得离开机器。
- 上游审批平面不可用时,需要它的操作保持待处理或到期。
- 批准成功但执行状态不明时,重试前按操作标识符或幂等标识符查询。
- 等待审批期间资源发生变化时,使决策失效,或让上游服务重新评估。
- 审核者拒绝或撤销时,取消所有待执行操作,并记录取消是否到达服务。
故障时放行不是恢复策略。审批服务位于关键路径时,团队有时会加入紧急绕过。绕过可能有正当用途,但它必须是一个独立的特权操作,具有实名人员、狭窄范围、短持续时间和自己的持久记录。把它隐藏在「不经批准重试」后面,会在事故最需要复核时摧毁控制。
本地故障和上游故障并不对称。网络断开时,本地关口仍可能记录一次被拒的尝试;提供方无法记录从未收到的请求。反过来,本地进程崩溃后,提供方可能完成操作,而本地记录仍停在 submitted。对账逻辑必须接受两边各自掌握的知识不完整。
上线前就要设计取消。确认已批准但仍在队列中的操作能否撤回,执行中审批到期会怎样,以及拒绝和完成竞争时哪个状态生效。描述实际发生了什么时,以远程资源状态为准;审批记录仍应说明操作是否获授权。
重试不得重复消费一次批准
批准和执行是两个独立操作,因此重试可能重复其中任何一个。风险最大的情况是客户端取得批准、发送变更、丢失响应,然后再次发送同一变更。
RFC 9110 根据重复相同请求的预期效果来定义幂等方法。它指出,客户端不应自动重试非幂等请求,除非已知语义具备幂等性,或能确定原请求没有应用。审批不会改变这条规则。一个人同意一次付款、部署或删除,不代表他同意次数不明的尝试。
在创建不可变意图时生成幂等键,不要等到执行开始才生成。把它与负载摘要绑定,并在安全重试中保持不变。上游服务应对重复键返回原结果,或提供状态查询。如果 API 两者都不支持,网关应把含糊的变更移到 unknown,并要求对账,不要猜测。
审批标识符和幂等标识符应分开。一次审批可能授权一次执行尝试、一组有边界的相同重试,或一项会话能力。幂等键告诉服务某项变更只能产生一次效果。用同一个令牌承担两种含义,会让到期、撤销和调查更困难。
一个实用测试序列不需要故障注入框架:
- 用固定 action_id、摘要和幂等键创建一项已批准操作。
- 发送请求,在请求体离开后切断客户端连接。
- 重启本地代理,让恢复逻辑检查已存状态。
- 验证它在任何重试前查询提供方。
- 确认只有一次权威资源变更,并且所有尝试记录属于一条相连的链。
在上游审批待处理时,以及断线期间审批到期时,分别运行同一序列。如果重启会用新标识符创建第二个提示,恢复路径已经破坏证据链。
对于使用确认端点的 API,要检查它的语义。prepare 后接 confirm 的两次调用只有在确认操作一次性消费不可变服务器对象,或使用幂等键时才安全。如果 confirm 只是再次接收客户端提供的字段,它可能只是一个披着安心名字的变更端点。
完整审计连接意图、决策、尝试和结果
除非同一个组件观察到拟议意图、审批决策、出站尝试、提供方接受和最终资源状态,否则任何单一日志都不完整。在多数真实系统中,完整性来自跨信任边界连接的记录。
本地审计应包含被拒和到期的请求,因为它们永远不会出现在上游。它应记录进程与会话身份、显示摘要的版本、规范负载摘要、凭据别名而不是秘密、人员决策、出站尝试、响应代码和所有不确定状态。上游审计应包含权威主体、审批对象、审核者身份、策略版本、资源版本、执行事件和最终状态。
在所有可控位置使用同一个稳定 action_id,并在可用时保存提供方事件 ID。不要只按时间戳连接。时钟偏差、批处理和并发调用迟早会制造错误匹配。
下面的事件形状足以完成第一个实现:
{
"event_id": "evt_01JQ7N2AZ8S4",
"action_id": "act_01JQ7M6F4R2K",
"phase": "upstream_result",
"observed_by": "local_gateway",
"principal": "service-account:deploy-agent",
"approver": "org-user:release-reviewer",
"payload_sha256": "98b0...e42c",
"provider_event_id": "dep_91358",
"outcome": "succeeded",
"recorded_at": "2026-07-24T14:02:18Z"
}
哈希链或只追加存储可以暴露后来的编辑,但两者都无法证明被遗漏的事件发生过。覆盖测试必须为每类操作比较预期阶段。一次读取可能需要意图、本地决策、尝试和响应。受保护部署还可能需要上游待处理、上游决策、执行和最终资源版本。
AWS CloudTrail 文档展示了提供方身份凭据的一面:IAM Identity Center 事件可以说明请求来自用户、角色、联合用户还是其他服务,部分事件还包含 Identity Center 标识符。这是有用的权威证据,但除非传播关联数据并保留本地记录,它仍然无法说明哪个本地代理进程构造了请求。
对绕过路径的审计要像主路径一样严格。直接使用凭据、备用 CLI 配置、没有保护的 API 端点、管理员绕过,以及重放已批准的服务器对象,都可能让完美的审批日志失去意义。覆盖声明应准确说明关口控制哪些通道,不要在没有清单时声称「所有操作都经过审批」。
两个关口只有保持独立才有用
当本地审批和上游审批控制不同事实或不同人员时,可以分层使用。如果一个关口只是重复相同决策并增加疲劳,就应删除它。
一个良好的分层部署流程可以这样工作。本地关口验证代理进程,并要求开发者为一个不可变发布候选项授权使用部署能力。它在不暴露凭据的情况下提交候选项。随后,上游环境根据当前检查结果和受保护环境状态,要求一位合格运维审核者批准提升。最终日志把两个决策连接到同一操作和同一已部署版本。
一个糟糕的分层流程先在本地显示「部署生产环境吗?」,再向同一个人显示完全相同的上游提示。两个提示都没有摘要或版本,两次批准都无限期有效,代理始终持有令牌。第二次点击增加了延迟,却没有增加信任边界。
「始终尽量靠近资源审批」这个常见建议并不完整。它来自正确的执行原则,却忽略了能力保管和本地进程身份。用上游审批判断资源事实。当你必须控制哪个进程能够调用能力,或必须防止进程看到秘密时,保留本地执行。
Sallyport 为 HTTP API 和 SSH 操作承担这个本地角色:代理通过它的 MCP 垫片连接,秘密留在加密保险库中,由应用执行操作。它的每会话授权和可选的每次使用审批不会取代 API 提供方的受保护环境或组织审核者;它们在本地一侧补充进程与凭据使用证据。
在部署两个关口前,为每个关口写一句话:「这个关口阻止 X,因为只有它能观察 Y 并执行 Z。」如果两句话中的 X、Y 和 Z 相同,就合并关口。如果某个组件实际上无法阻止它所描述的操作,先修正执行点,再润色提示文字。
一次部署演练会暴露缺失的连接
设想一个自主编程代理把版本 184 提升到一个 API 已经要求复核的生产环境。安全流程会让同一个意图贯穿本地批准、上游等待、执行和对账。
代理先提出一个规范请求,其中包含环境、发布版本、预期当前版本和幂等键。本地网关分配 action_id,记录进程会话和负载摘要,再决定是否需要本地审批。批准只在短有效期内授权该摘要。随后网关注入凭据并提交准备好的请求。
提供方创建待处理部署对象。审核者看到当前检查、受保护环境、确切版本和发起操作的组织身份。批准会消费这个待处理对象。如果生产环境已经从预期版本前进,提供方应拒绝或重新打开复核,而不是应用过期同意。
现在在上游批准后切断连接。本地一侧不能返回成功,也不能立即再次提交提升。它应保存 unknown,通过待处理部署 ID 或幂等键查询提供方,并记录权威结果。如果提供方报告成功,本地日志关闭现有操作。如果提供方没有记录,只要批准仍有效,网关可以按照原幂等约定重试。
这次演练会发现界面复核漏掉的问题。代理能否在本地点击后修改版本?另一个客户端能否确认待处理部署?上游拒绝是否撤销本地授权?提供方管理员的绕过是否出现在关联记录中?调查人员能否不相信代理自己写的标签,就证明哪个进程提出了发布?
根据这些答案选择架构。当全部风险都在本地能力使用,且远程操作常规、边界明确、可以恢复时,只用本地审批。当受信任客户端已经持有可接受的凭据,只有当前资源状态或组织角色能够决策时,只用上游审批。当秘密保管或进程来源在本地很重要,而权威变更复核在远程很重要时,两者都用。
不要承诺两次审批就能让操作安全。它们只是让两个决策可见。真正的安全来自把每个决策绑定到不可变意图,在掌握相应事实的边界执行决策,在不确定时避免重复效果,并保留足够的关联证据,让所有人忘记提示内容后仍能重建结果。
常见问题
上游审批一定比本地审批安全吗?
不是。上游审批拥有更好的资源上下文,但开始审批时代理可能已经拿到可重复使用的凭据。当决策涉及进程身份、用户在场或不让代理接触秘密时,本地执行更强。
API 已经要求确认时还需要两次审批吗?
只有两次审批判断不同事情时才需要。如果本地关口控制能力使用,而上游关口控制权威资源变更或独立组织审核者,就应保留两者。
本地审批提示应该显示什么?
显示观察到的进程身份、会话、目的地、操作、凭据别名、目标资源、负载摘要、范围和到期时间。代理提供的解释应标为声明,不能当作可信事实。
上游审批服务中断时该怎么办?
操作应保持待处理或到期。绝不能把故障转成批准;业务若需要紧急路径,应单独授权并完整记录。
API 重试可以重复使用一次审批吗?
可以,但定义的范围必须绑定相同的不可变负载和幂等键。如果执行状态不明,重试变更前先查询提供方。
审批应保持多长时间有效?
有效期取决于待审核事实变化的速度,以及合规审核者实际需要的时间。负载、目标版本、凭据或审核资格发生任何变化,都应使旧决策失效。
如何关联本地和上游审计日志?
在审批前创建稳定的操作标识符,并尽可能通过提供方支持的元数据传播。把提供方事件 ID 和负载摘要保存在本地;单靠时间戳无法可靠关联。
生物识别确认能识别上游审核者吗?
它根据设备机制证明本地用户在场,不能证明 API 提供方的组织角色。应分别记录本地和上游人员身份,不能让一个身份代替另一个。
只读 API 调用需要审批吗?
有些读取会泄露敏感数据,因此 HTTP 动词不能单独决定。根据数据分类、凭据范围、调用方来源和调用量,选择会话审批、逐次审批或无需人工提示。
测试审批设计最快的方法是什么?
在已批准变更发出但响应返回前断开客户端。重启后验证恢复逻辑会查询权威状态、保留原标识符,并且只产生一次资源变更。