代理批准提示如何防止跨运行误批准
代理批准提示需要同时包含进程身份和会话身份,才能让审核者在并行运行共享一个队列时,批准预期运行发起的具体操作。

自主代理的批准队列不是一摞权限按钮,而是一套实时的归属系统。当两个或更多代理运行可以请求同一个凭据、调用同一个 API,或打开同一个 SSH 连接时,审核者在批准前必须能回答一个明确的问题:究竟是哪个正在运行的进程请求我为这个具体操作授权?
大多数批准界面之所以失败,是因为它们让每张卡片看起来都来自一个叫作“代理”的通用角色。在只有一个终端窗口的演示中,这个标签没有问题。但当一个编程代理正在修复生产事故,另一个代理正在准备发布,而两者使用同一个客户端并请求访问同一服务时,情况就变得危险。审核者看到一个熟悉的目标,大致认出了任务,于是批准了正确的操作,却批准给了错误的运行。
批准是一项归属判断
审核者并不是孤立地批准一个 HTTP 请求。他们之所以批准,是因为这个请求属于某项特定工作,而他们预期这项工作正在进行。如果界面无法保留这种关系,批准按钮就会变成披着人工控制外衣的猜测。
把每次批准都视为包含五个部分的一句话:
- 某个具体的可执行文件或代理进程发起了请求。
- 该进程的某个具体会话仍在运行。
- 该会话声明自己负责一项有名称的任务或工作项。
- 它想执行一个具体的外部操作。
- 审核者要么授权这个完整组合,要么拒绝它。
第五点的重要性超出直觉。“批准访问部署 API”并不是完整的表述。审核者可能愿意让发布运行读取构建记录,却不愿意让无关的调试运行修改发布设置。单凭目标无法传达完整含义。
之所以容易混淆,是因为同一个代理客户端经常会创建多个运行。命令行可能完全相同,代码签名来源可能相同,代码库路径也可能相同。代理甚至可能生成几乎一样的请求。但这些都不能让两个同时发生的调用成为同一个安全主体。
我见过一些团队在真正缺少归属信息时,反而不断给操作摘要增加文字。他们把“POST /deployments”改成“POST /deployments,以触发版本 1.8.4 的暂存环境部署”。这确实有助于解释操作,但审核者看到的仍然是两张都声称要执行暂存部署的卡片。更好的句子无法弥补缺失的会话边界。
批准界面必须让身份层级清晰可见。操作是主要对象。发起进程和活动会话解释了为什么会出现这个操作。任务标签帮助审核者识别意图,但不能替代任何一个技术标识符。
经过签名的进程不等于正在运行的会话
代码签名身份可以告诉系统是谁签署了代码,却不能告诉审核者是哪一次调用生成了待处理请求。Apple 的代码签名文档也用不同的语言划出了类似边界:代码要求用于确立代码身份,而指定要求用于描述跨版本哪些代码应被视为同一代码。这些信息可以作为调用方的证据,却不能替代运行 ID。
当批准提示开头类似下面这样时,这一点尤其重要:
Request from: Acme Agent CLI
Signed by: Example Engineering, Team ABCD1234
这是一个不错的开始。它能让审核者判断是否是预期的客户端请求访问。但它无法回答:这是五分钟前启动的发布助手、今天早上一直开着的测试助手,还是第二个终端中运行的一条复制出来的 shell 命令?
进程身份和会话身份回答的是不同问题:
| 字段 | 它回答的问题 | 它无法回答的问题 |
|---|---|---|
| 代码签名来源 | 谁生成了这个可执行文件? | 这是哪一次调用? |
| 可执行文件路径 | 哪个已安装的客户端启动了它? | 它正在执行什么任务? |
| 进程 ID | 当前哪个本地进程拥有这个连接? | 重启后人还能识别它吗? |
| 会话 ID | 哪一次有边界的运行发出了请求? | 请求的操作是否合理? |
| 任务标签 | 用户要求这次运行做什么? | 标签是否真实,或是否足以作为证据? |
不要因为增加了第四行,就隐藏前两行。审核者需要进程来源,以发现意外的客户端;需要会话 ID,以区分同时运行的预期客户端;还需要任务标签,把技术标识符与对工作内容的理解联系起来。
实际规则很简单:将进程身份显示为来源信息,将会话身份显示为获得授权的对象。
在 macOS 上,单独使用代码签名标识符作为批准标签尤其薄弱。Apple 明确指出,多个签名者可以声明相同的签名标识符,并建议将标识符检查与验证类别结合起来;对于非 Apple 代码,还应加入团队标识符。只显示友好的应用包名称,或只显示声明的标识符,都会让审核者获得的证据少于平台本身能够提供的证据。
不要把原始要求语言作为主要标签。它很精确,但大多数审核者无法快速读懂并据此做决定。用通俗语言显示签名来源,把技术要求保留在详情中,并与易于识别的可执行文件名称放在一起。然后将会话标识符放在无需展开就能看到的位置。
让每次运行都有能穿过队列的会话身份
会话 ID 必须在第一次受保护调用之前生成,在整个运行期间保持稳定,并在运行结束时从授权集合中消失。任何更弱的设计都会在负载升高时制造歧义。
在连接或进程注册时生成高熵的不透明 ID。审计记录保存完整值,但在提示中显示简短且不含糊的前缀。显示形式应足够长,让两个活动会话极不可能共享同一个前缀;如果真的发生碰撞,界面也必须让人立刻注意到。不要根据当前时间、队列位置、代码库名称或单独的进程 ID 推导会话 ID。
一个有用的内部记录可以是:
{
"session_id": "ses_7TQ4N8M2KDXP6R9V",
"display_id": "7TQ4N8M2",
"process": {
"pid": 84172,
"executable": "/usr/local/bin/agent-cli",
"signing_authority": "Example Engineering (Team ABCD1234)"
},
"task": {
"label": "Prepare the staging release notes",
"workspace": "/Users/maya/work/app"
},
"started_at": "2026-07-22T16:42:11Z"
}
任务标签应来自用户可见的指令或有意设置的会话标题,而不是来自每次工具调用都会变化的模型生成句子。模型可以提出标签,但系统应在会话开始时将其固定下来。如果会话从“准备发布说明”转变为“轮换生产环境 webhook 凭据”,审核者应看到明确的任务变更,或看到一个新的会话。悄悄改写标签会让历史记录更难阅读,也会让运行借用早期、更安全任务的可信度。
采用边界明确的生命周期:
- 在任何需要凭据的操作之前创建会话。
- 将每个请求、批准、拒绝、取消和结果都关联到该会话。
- 当发起进程退出或失去有效连接时关闭会话。
- 当审核者选择撤销时立即撤销会话。
- 会话关闭或撤销后,即使卡片暂时仍然可见,也要拒绝待处理批准。
最后一点可以避免一个隐蔽但常见的竞态。审核者看到的是运行 A 的卡片。运行 A 退出,新的运行 B 开始,并请求类似操作,而旧卡片仍然接受输入。如果批准服务把这次点击关联到“最近一次请求这个凭据的请求”,审核者看着 A,却批准了 B。每张卡片都必须绑定一个不可变的请求 ID 和一个会话 ID。当其中任意一个失效时,唯一安全的按钮就是关闭。
避免使用“会话 1”“代理运行”或“当前任务”这类标签。它们在第二次运行开始前看起来没问题。标签可以友好,但界面还需要一个在进程重启、笔记本休眠或数天后进行事故复盘时仍有意义的标识符。
在前两行放入正确的信息
批准提示的前两行应让审核者无需打开详情,就能把这条请求与其他所有待处理请求区分开。先放请求执行的操作,再在下面直接写出发起进程和会话。
一张可用的卡片可以是这样:
Allow POST to api.example.internal/v1/releases?
agent-cli, signed by Example Engineering, session 7TQ4N8M2
Task: Prepare the staging release notes
Creates a release record named "2026.07.22-rc3"
Credential: release-service-write
[Review request] [Deny] [Allow]
操作行说明会发生什么以及目标在哪里。归属行说明是谁提出了请求。任务行说明审核者应将请求与哪项工作联系起来。预览则提供足够的信息,帮助审核者做出初步判断。这样的顺序是有意安排的。
不要以“请求批准”或“代理想使用一个密钥”开头。这些说法无法帮助审核者整理繁忙的队列。也不要以凭据名称开头。凭据属于实现细节。审核者通常先判断请求是否属于正确的发布运行,然后才会确认 release-service-write 是否是合适的存储密钥。
对于 SSH,类似的提示应写出远程主机和命令类别,而不是只说“SSH 访问”。例如:
Allow SSH command on build-staging-03.example.internal?
agent-cli, signed by Example Engineering, session 7TQ4N8M2
Task: Verify the staging migration
Runs: /usr/local/bin/check-migration --database app_staging
Credential: deploy-ssh
[Review command] [Deny] [Allow]
如果系统无法生成安全且易读的预览,就应明确说明。“无法获取命令参数”好过编造一句隐藏 shell 展开、间接脚本或未经检查输入的友好描述。审核者随后可以打开完整命令,或拒绝请求。
好的提示不会要求审核者根据时间戳推断发起请求的运行。时间应放在详情和活动记录中。两个代理可能在同一秒发出请求,而人们并不能可靠地依靠时间戳把卡片与某个终端对应起来。时间只能作为辅助证据,不能成为承载归属信息的标签。
也不要把会话 ID 放在浅色页脚中。这个标识符不是诊断性杂项。在并行队列中,它正是防止审核者把两张几乎相同的卡片当成可互换对象的字段。
队列需要分组,但不能错误合并
并行工作会带来视觉整理问题。按会话分组可以帮助审核者,但当界面把请求之间有意义的差异折叠掉时,分组也会变得危险。
队列应让审核者看到某个会话的所有待处理操作,同时在操作影响不同的情况下保留每个操作独立的批准决定。一个发布助手如果需要进行三次只读 API 调用,可以合理地显示成一个紧凑分组。一个会话如果要读取问题、写入部署记录并执行远程迁移,就应显示三个独立决定,因为它们的后果不同。
错误的模式是显示一个全局横幅:
agent-cli requests access to 5 services
[Allow all]
这个按钮要求审核者在确认每个项目属于哪个运行、检查每个项目的影响之前,就批准一组请求。它还会助长队列疲劳。审核者越频繁看到这个横幅,就越可能养成清除它、尽快回到工作的习惯。
更好的队列会使用会话标题提供上下文,而不是授予宽泛权限:
Session 7TQ4N8M2 · agent-cli · Prepare the staging release notes
2 pending requests
GET api.example.internal/v1/builds/rc3 [Allow]
POST api.example.internal/v1/releases [Review]
Session C5J1W6PA · agent-cli · Investigate test failure #1842
1 pending request
SSH build-staging-03.example.internal [Review]
审核者可以按运行快速浏览,但每一行仍然提出一个独立请求。如果系统支持按会话授权的状态,就在标题中显示清晰的范围,例如:“此会话在退出前获得授权。”不要让这种状态看起来像批准了每个调用操作。按会话授权只允许运行请求操作,并不会把意外操作变成预期操作。
排序也会影响错误率。纯按时间排序的队列可能把五个运行的请求彻底交错,让审核者无法保持任何一个运行的上下文。纯按会话分组的队列又可能把紧急请求埋在嘈杂的会话后面。应提供两种视图:默认使用按会话分组的视图,便于判断归属;另提供按时间排序的视图,用于事故响应。无论在哪种视图中,都要保留会话标记。
不要仅仅因为两个请求共享目标和凭据,就把它们合并。两个不同运行同时写入同一个端点,正是队列必须解决的歧义。相似性应成为突出运行身份的理由,而不是合并批准的理由。
批准必须绑定到审核者检查过的确切请求
即使提示正确识别了运行,如果批准适用于可变模板,而不是不可变操作,也仍可能授权错误的请求。审核者必须批准一个具体的请求表示,而不是批准未来某个碰巧复用了相同路由、命令或凭据的请求。
在卡片出现前创建规范化的操作记录。对于 HTTP,至少包含方法、规范化的来源和路径、按名称选择的请求头、凭据别名以及请求正文的摘要。对于 SSH,包含规范化的主机身份、请求账户、端口、命令字节和凭据别名。完整的受保护材料应保存在可信组件中,不能因为审核者需要看到提示,就把它发送给代理。
然后将批准绑定到规范化记录:
{
"approval_id": "apr_K9H2D7LQ",
"session_id": "ses_7TQ4N8M2KDXP6R9V",
"action_digest": "sha256:2a13c4e0...",
"expires_at": "2026-07-22T16:47:11Z",
"decision": "allow_once"
}
执行时,根据实际要离开本机的操作重新计算摘要。如果摘要不同,就拒绝批准并创建新卡片。不要悄悄把变更后的请求视为“差不多”。变更的 URL 参数可能重定向付款,变更的请求头可能改变账户范围,变更的 shell 参数可能把检查命令变成写入命令。
许多看似周全的系统就在这里失败。它们显示可供审核的摘要,批准“使用凭据 X 访问端点 Y”,随后却允许代理在这个宽泛授权下发出第二个请求。审核者并没有审核第二个请求。系统把一次明确的决定变成了不可见的策略规则。
待处理批准应设置较短的有效期。目的不是惩罚去喝咖啡、暂时离开的审核者,而是防止周围上下文发生变化后,旧决定仍被使用。如果代理稍后仍需要这个操作,可以用相同的进程和会话信息重新请求。审核者也能再次判断它是否仍然属于这次运行。
NIST 关于自动化决策中人工参与的材料警告了自动化偏见,以及低摩擦批准循环如何让警报疲劳变得正常。这里的实际结论很直接:批准更容易点击,并不意味着批准更有意义。界面必须让决定足够小,便于检查;也必须足够具体,能够追责。
详情视图应该解决争议,而不是制造争议
紧凑的批准卡片无法容纳请求的每一个字节,但必须提供详情视图,回答谨慎的审核者在面对陌生、昂贵或可疑操作时会提出的问题。
对于 HTTP 操作,应显示完整的方法和 URL,默认打码的请求头,对正文中各字段以及敏感信息的明确处理,凭据别名,发起进程身份,会话 ID,任务标签和请求创建时间。如果正文是二进制数据或过大而无法渲染,应显示其大小、内容类型和摘要。不要假装一行文字摘要足以替代这些信息。
对于 SSH,应显示主机、用户、端口、可用时的主机验证状态、将要执行的完整命令、相关的工作目录,以及所有会影响行为的环境值。命令预览必须保留引号和参数边界。将数组松散地拼接成 shell 字符串,可能让安全命令看起来不安全,更糟的是,也可能让不安全命令看起来无害。
为了安全而打码,与为了省事而省略信息,有明显区别。应从提示中打码 bearer token、私钥、密码或敏感正文值。但不要因为详情会让卡片变长,就省略路径、目标账户、远程主机、命令或变更字段。这些往往正是判断请求是否属于当前运行的关键信息。
在详情视图中使用稳定的请求指纹。审核者联系同事时,应能说:“我拒绝了会话 7TQ4N8M2 的请求 apr_K9H2D7LQ。”同事也应能找到唯一对应的记录。不要让人只能把事件描述为“4:40 左右的第二个部署提示”。这种说法在需要精确调查事故时就不够用了。
审核界面还应说明批准会授权什么,以及不会授权什么。例如:
Allow once
This decision authorizes only this POST request with digest 2a13c4e0…
It does not authorize later calls from session 7TQ4N8M2.
这句话值得占用界面空间。它能防止审核者误以为自己授予了宽泛权限,也能防止实现者以后在同一个按钮标签背后扩大授权范围。
取消和撤销必须产生清晰可见的结果
审核者最终会批准错误的卡片,或发现某个运行已经偏离预期。恢复过程必须快速、清晰可见,并与批准过程中使用的同一组标识符绑定。
界面需要分别提供拒绝单个请求和撤销单个会话的操作。拒绝表示“不要执行这个操作”。撤销表示“这个运行中的进程失去授权,取消它的待处理批准,并拒绝之后的操作”。两者的后果不同,因此不能共用一个含义模糊的“停止”按钮。
审核者撤销会话时,应显示包含进程、会话 ID 和任务名称的确认信息:
Revoke session 7TQ4N8M2?
agent-cli is running “Prepare the staging release notes.”
Pending requests from this session will be cancelled. New protected actions
from this process will be denied until it starts a new session.
[Keep session] [Revoke session]
如果请求已经在执行,要如实报告。撤销可以阻止未来的特权操作,但可能无法撤回远程服务已经接受的 HTTP 请求,也无法停止已经启动的远程命令。活动记录应显示操作处于待处理、已发送、已完成、失败还是已取消状态。不要用“已撤销”来暗示外部影响已经消失。
这也是每张卡片都显示会话 ID 的另一个原因。在紧张的事故处理中,审核者需要从可疑提示直接找到能停止该运行的控制项。如果队列只显示“agent-cli”,撤销操作就可能误杀有用的运行,却让有问题的运行继续活动。
会话日志和活动日志应使用一致的标识符。一条记录说明进程会话 7TQ4N8M2 何时开始、获得授权并被撤销;另一条记录说明它在每次状态变化前后尝试执行了哪些具体操作。如果两类记录使用互不相关的标签,响应人员就不得不重新构建本应由产品明确提供的关联关系。
在用户犯错前测试错误运行批准
无需红队或生产事故,就能测试这种失败场景。使用同一个代理客户端启动两个实例,让它们针对同一个工作区发出相似的受保护调用。只有当审核者在繁忙队列中仍能正确识别每个请求时,测试才算成功。
可以进行以下练习:
- 启动运行 A,任务标签设为“检查失败的暂存构建”。
- 启动运行 B,任务标签设为“发布暂存版本记录”。
- 让两个运行在几秒内请求同一个凭据。
- 让它们的第一次请求相似,例如都调用同一个 API 来源。
- 修改其中一个操作,使得把它批准给另一个运行会在测试环境中产生明显且不良的影响。
然后让一个没有参与界面开发的人只批准运行 B 的请求。不要通过队列位置告诉他哪张卡片是 B。观察他依据什么做决定。如果他依赖时间戳、卡片顺序,或记得自己先打开了哪个终端,说明界面已经失败。如果他无需打开其他页面,就能指出进程来源、会话 ID、任务、目标和操作,那么这至少是一个可用的起点。
也要测试过期批准的行为。为运行 A 创建卡片,结束运行 A,启动运行 B,然后点击旧卡片。系统必须拒绝点击,并解释该请求已经不再活动。在审核后修改请求正文或 SSH 参数,再重复测试。系统必须要求新的批准,不能把旧批准应用到变更后的操作上。
测试会发生冲突的名称。两个任务都可能叫“修复 CI”。两个代码库可能有相同的目录名。两个分支可能共享同一个发布编号。友好的文字有助于理解,但完整设计仍必须在这些文字含糊时正常工作。
Sallyport 的按会话授权为建立这条边界提供了自然位置:新的代理进程第一次调用时会识别该运行,以便进行批准;而按调用设置的密钥则可以要求每次使用都单独批准。当这些控制项出现时,应继续显示会话标签和进程来源,否则并行运行仍会重新合并成一个模糊的“代理”。
审计记录必须能够重现批准决定
只有当审计记录能在事后回答审核者看到了什么、系统执行了什么时,它才真正有用。记录“用户批准了 API 访问”远远不够。它没有解决核心争议:这是哪个运行的访问,针对哪个操作,以及当时显示了哪些上下文?
对于每项决定,记录不可变的标识符和展示信息:
{
"event": "approval_granted",
"approval_id": "apr_K9H2D7LQ",
"session_id": "ses_7TQ4N8M2KDXP6R9V",
"process_identity": "agent-cli / Example Engineering (Team ABCD1234)",
"task_label": "Prepare the staging release notes",
"action_digest": "sha256:2a13c4e0...",
"displayed_action": "POST api.example.internal/v1/releases",
"decision_scope": "once",
"recorded_at": "2026-07-22T16:44:03Z"
}
规范化的操作记录应单独保存,或与批准事件一起保存,并设置适当的加密和访问控制。审计事件应告诉调查人员卡片上写了什么;规范化记录则应证明可信组件使用了哪些字节和目标。如果两者不一致,应将其视为安全缺陷,而不是普通的日志问题。
哈希链日志有助于发现历史记录被改写,但它不能修复含糊的事件内容。你可以用密码学方式证明“批准已授予”没有被修改,却仍然无法判断批准授予了哪个请求。完整性和归属解决的是不同问题,两者都需要。
Sallyport 会将会话级记录和单次调用记录写入同一个加密、哈希链式的审计日志;其 sp audit verify 命令可以在离线状态下对密文验证链条。这很有用,因为审核者可以在不向验证步骤暴露存储密钥的情况下,保留日志未被改写的证据。
最后的设计测试很简单。随便选一个已经完成的操作,倒推整个过程:调查人员能否识别进程、具体会话、当时显示的任务标签、审核过的确切请求、审核者的决定范围和最终结果?然后从一个会话向前查看:能否按顺序看到所有待处理、被拒绝、已批准、已取消和已执行的操作?如果任一方向都需要猜测,批准队列仍然会诱发把批准给错误运行的事故。
并行代理会让批准队列成为日常工具。解决办法不是用更多卡片淹没审核者,也不是给他们一个宽泛的“全部允许”控制项。应为每个正在运行的进程提供持久的会话身份,把它放在用户真正会查看的操作旁边,并将每次点击绑定到审核者检查过的请求。多个代理同时活动时,批准才会因此真正有意义。
常见问题
什么是并行代理批准队列?
并行代理批准队列,是多个代理进程同时产生的批准请求集合。审核者需要足够的上下文来判断每个请求由哪个正在运行的进程发起,而不能把所有卡片都当成来自“代理”的、可以互换的请求。
为什么代理批准不能只显示进程名称?
进程名称只能说明谁启动了工作,不能识别某一次具体运行。两个终端窗口可能运行同一个经过签名的客户端,并发出相似请求,因此批准卡片还需要会话 ID 和简短的任务标签。
代理会话 ID 应该是什么样?
应在代理进程连接时生成稳定且不透明的会话 ID,并在该进程退出或被撤销前保持不变。不要只用队列位置、单独的请求计数器或时间戳作为会话身份。
批准提示中最先应该显示哪些信息?
先显示操作和目标,然后在下一行直接写出发起请求的进程和会话。审核者通常会先判断该操作是否属于自己预期的工作,再查看请求头、命令参数或请求正文。
批准提示应该显示完整命令或 HTTP 请求吗?
批准卡片应显示简洁的预览,并提供查看完整命令、URL、目标、方法和变更字段的方式。不要因为完整请求被埋在活动日志中,就让审核者只能批准一个含糊的摘要。
如何避免批准卡片过期?
不要让卡片在出现后悄悄改变含义。应将批准绑定到不可变的请求摘要,在会话结束时让批准失效;如果进程试图用原批准执行参数已经改变的操作,就拒绝该操作。
批准提示中可以使用较短的会话 ID 吗?
使用一个人可以在批准卡片、会话日志和活动记录之间相互核对的持久标识符。可以使用较短的显示形式,但它必须能明确对应审计记录中的完整 ID。
代理会话被撤销后应该发生什么?
取消操作必须指出受影响的会话,并告诉审核者待处理请求会发生什么。如果撤销会取消这些请求,就明确说明;如果已经批准的请求仍可能完成,也应显示这一状态,不要让人误以为取消会停止一切操作。
会话批准和每次调用批准有什么区别?
每会话批准回答的是“这个进程是否可以运行?”。每次调用批准回答的是“它现在是否可以执行这个具体操作?”。当宽泛的会话批准被展示得像是在证明之后的每个破坏性请求都符合预期时,团队就容易出问题。
如何测试代理批准队列?
先启动多个并发运行,让它们有意使用同一个代理客户端、代码库、凭据和目标。如果审核者在一张收起详情的队列截图中都无法判断每张卡片属于哪个运行,那么队列还不适合投入实际工作。