# AI 代理审批控制：规则与清晰决定

AI 代理每次需要 API 令牌或 SSH 密钥时，都不需要启动一套微型企业授权方案。大多数团队需要的是三个在压力下依然容易理解的决定：机密存储是否可用，这个代理进程能否在本次运行中执行操作，以及当前凭据是否需要重新征得人工决定？

策略引擎可以回答更多问题，但也会把每次权限变更变成一个小型软件项目。你需要确定哪些输入值得信任、测试规则、解释例外，还要处理有人在最糟糕的时刻编辑条件后产生的故障。当访问问题确实涉及可变的所有权和约束时，再使用规则引擎。不要把规则引擎当成简单审批边界外面的装饰。

## 规则引擎会把授权变成软件维护

策略引擎就是代码，不管它的语法看起来像不像代码。有人必须定义可用事实、编写规则、决定优先级、测试变更、发布版本、调查意外匹配，并淘汰不再适合组织的规则。

这项工作可能很有必要。公司可能需要让不同资源所有者设置访问条件，按地区或环境限制操作，或者落实法律和合同要求。这些情况下，一组固定的小型控制可能会把不相关的情况都压进同一个粗糙的审批流程。错误在于，策略语言用声明式语法隐藏了复杂性，于是人们误以为这种复杂性不需要付出成本。

看一个常见规则：

```text
allow if
  agent.project == "payments"
  and request.host ends_with ".internal.example"
  and request.method in ["GET", "POST"]
  and time.weekday in ["Mon", "Tue", "Wed", "Thu", "Fri"]
```

在有人需要回答运维问题之前，它看起来很合理。谁来分配 `agent.project`？代理能影响它吗？`ends_with` 会接受 `not-internal.example` 吗？对于一个能够创建、退款、删除或触发转账的端点，POST 到底允许什么？周六发生事件时怎么办？后面的拒绝是否会覆盖前面的允许？

每个问题都会增加语义。每种语义都需要测试。每个例外都会成为权限模型的一部分，即使它最初只存在于一条匆忙发出的聊天消息中，并在一周后被复制进规则。

Open Policy Agent 文档正确地把策略称为代码，并建议对其进行测试。这不是为了宣传灵活性的口号，而是对运维契约的承认：如果策略控制着重要访问，团队就必须像对待代码变更一样对待编辑。审查人员需要测试样例，持续集成需要预期决定，值班工程师需要知道究竟是哪一个策略版本允许了请求。

很多团队会跳过这个契约。他们把几条规则粘进配置文件，六个月后才发现没人知道一次拒绝究竟来自拼写错误、缺失输入，还是某个有意设置的边界。AI 代理会让这种失败更加明显，因为它会生成不寻常的调用序列，比人工操作员快得多地触发那些被忽略的分支。

固定控制通过拒绝表达任意条件，缩小了维护范围。听起来有限制，是因为它确实有限制。真正有用的问题是：这种限制排除的是你确实拥有的需求，还是某个人未来也许会想要的规则？

## 当调用有后果时，决定必须清晰

操作人员应该能用一句话说明某次调用为什么被允许。如果答案需要阅读一组规则、解析优先级，还要检查代理提供的属性，那么操作人员在事件期间就无法可靠地批准或撤销访问。

清晰的决定也能避免一种隐蔽的分类错误：规则可以让某个操作在技术上获准，却没有让承担后果的人理解它。一个提示如果只说某个未命名进程请求使用某种笼统能力，操作人员几乎无从判断。这是仪式，不是审批。

一个有用的授权界面应该回答具体问题：

- 哪个可执行文件请求执行操作，谁为它签名？
- 这是新进程，还是本次会话已经批准过的进程？
- 操作将使用哪个凭据？
- 请求会发往哪里，或者 SSH 将连接哪台主机？
- 操作人员批准的是一次运行，还是一次敏感使用？

第一项通常没有得到应有的重视。代理名称只是标签。进程身份才是证据。一个进程可以自称 `release-agent`，但代码签名机构或可执行文件路径能给审查人员提供更可靠的依据，即使 shell 脚本改了名字也不受影响。身份凭据不能证明每条指令都安全，但能把问题收窄到一个真实主体。

NIST 特别出版物 800-207 将零信任建立在显式验证和持续评估之上，而不是继承网络信任。对于本地代理操作，实际经验比许多实现简单：在行动发生的地方评估操作者和请求。不要把令牌交给代理，然后希望令牌离开你的控制后边界仍然有效。

决定清晰本身也是安全属性。当人们能够预测一次审批时，就能发现意外审批。如果一个平时只读取问题数据的编程代理突然请求使用生产写入凭据，那么在调用离开机器前，这个差异应该一目了然。

## 身份、权限和机密使用是三个问题

团队经常把三个不同的问题写进同一条策略，之后却无法判断到底是哪项假设出了问题。把它们分开。

身份关注是谁发出了请求。对于本地代理，这可以包括进程、代码签名机构、父进程和生命周期。易读的代理标签有帮助，但不应单独承担安全决定。

权限关注已识别的进程是否可以在本次运行中执行操作。会话审批属于这个问题。它记录了操作人员检查一个新进程后，允许该进程使用定义好的操作网关，直到进程退出或操作人员撤销审批。

机密使用关注某个具体 API 密钥或 SSH 密钥能否用于这次操作。保险库门禁和按凭据审批应放在这里。保险库锁定时，无论之前是否有会话决定，所有操作都应被拒绝。某个特别敏感的凭据即使进程已经拥有会话权限，也可能要求每次使用都由人工批准。

混淆这些问题会带来可预见的错误。团队批准一次代理进程，然后把这次批准当成使用所有凭据的许可。或者团队解锁保险库，把可用性误认为授权。又或者，团队编写了同时检查进程名称和目标主机的策略，却允许代理取出令牌并在别处重复使用。

最后一种失败尤其重要。如果代理持有明文凭据，你的策略只检查了第一次使用。代理可以把值传给子进程、写进日志、发送给另一个服务，或者在原始审批过期后继续使用它。让机密留在代理之外的网关改变了边界：代理请求执行操作，网关自己完成经过身份验证的操作。

这和“隐藏机密”有明显区别。令牌到达代理后，再对输出进行脱敏，并不能把令牌从它的内存、提示历史、shell 环境或子进程中移除。不把令牌交给代理，则能消除整类意外复用问题。

## 三个显式控制足以覆盖普通代理场景

当每个控制只负责一个决定，而且不假装解决其他问题时，一组小型控制就能发挥作用。对于本地开发代理，三个控制可以覆盖相当大一部分实际风险，而不必建立一套策略语言。

第一，使用绝对的保险库门禁。保险库锁定时，所有操作都失败。这给操作人员一个物理上和概念上都明确的停止条件。它不应依赖规则评估、记住的会话或网络检查。在 Mac 上，通过 Secure Enclave 和 Touch ID 进行硬件门控解锁，可以让这个决定尤其清楚：操作人员打开了保险库，或者没有打开。

第二，在首次看到新的代理进程时，为它的生命周期授权。审批应该以能抵抗友好名称变更的方式标识进程，并在进程退出时过期。这样可以避免每个无害请求都弹出提示，同时默认不让审批永久有效。

第三，把指定凭据标记为每次使用都需要审批。要谨慎、有意识地使用这一项。能够修改生产基础设施的部署凭据、可以访问大量主机的 SSH 身份，或能转移资金的令牌，都可能值得每次调用立即作出决定。代理反复使用的只读开发令牌通常不需要这样做。

最终的决定流程很容易理解：

```text
if vault is locked:
    deny action
else if this agent process has no current session approval:
    ask for session approval
else if the selected credential requires approval per use:
    ask for credential approval
else:
    execute the action
```

这不是最小权限的替代品。凭据仍然必须拥有狭窄范围，请求路径仍然需要传输安全和目标验证。这个流程只是把人工控制点明确展示出来。它不会把管理员令牌变成安全凭据。

顺序很重要。每次调用提示绝不能绕过锁定的保险库。记住的会话绝不能绕过标记为重复审批的凭据。使用通用规则引擎时，这些优先级关系往往分散在不同策略中，审计起来意外地困难。使用固定流程时，顺序就是模型本身。

## 当所有权不断变化时，策略引擎才值得付出成本

当授权决定必须在许多由不同团队独立负责的资源之间变化，而且这种变化无法通过选择凭据或使用少量审批类别来表达时，策略引擎才有理由存在。

假设一个共享自动化服务处理多个业务部门。每个部门负责不同的代码仓库、云账户和数据存储。各部门负责人需要向定义好的群组授予临时访问权限，设置不同的保留条件，并在集中治理流程下审计决定。策略层可能是正确设计，因为组织确实需要委托规则所有权，并在大量资源上保持一致执行。

另一个适合的场景是接收许多不可信客户端请求的服务端服务。服务可能需要在行动前评估租户、角色、对象所有权、请求来源和交易状态。固定的本地会话审批无法替代这些判断。即使附近没有人可以批准，服务器也必须为每个请求作出决定。

不要把这些场景延伸成在每个本地编程代理前面都放一个策略引擎的理由。开发者机器通常面对的是更小的问题：操作人员允许期间，这个签名代理进程能否通过操作网关使用这个已存储的凭据？人工已经掌握本地上下文。再加入时间窗口、项目标签和假设性的风险分数，带来的可能是更多错误信心，而不是更多控制。

还有一个硬边界。策略无法修复范围过大的凭据。规则可以只允许请求某台主机，但如果代理能提取持有者令牌，令牌的发行方就必须自行执行范围限制。让机密留在网关中，并在提供方一侧限制其范围。把网关授权视为一层，而不是资源侧访问控制的替代品。

## 审批疲劳说明范围设置错了

反复出现的提示会让人点击得更快，而不是更加谨慎。如果代理获取仓库元数据时，一个人连续看到二十张相同的审批卡，他会学会用点击来让工作继续。第十一次请求如果出现重要差异，也可能得到同样的条件反射。

常见做法是编写更聪明的规则，在越来越具体的条件下压制提示。这通常只是把看得见的疲劳换成看不见的复杂性。有人为读取调用写了例外，之后才发现某个所谓的读取端点会触发远程计算，或暴露本应接受审查的数据。审批数量下降了，决定却更难检查。

根据人工判断所需的范围设置审批范围。会话审批表达的是：“我认识这个进程，允许它使用本次运行分配的普通凭据。”每次调用审批表达的是：“这次凭据使用的后果足够严重，我要单独检查。”提示不应只是因为实现需要一个确认对话框而存在。

良好的凭据设计会让事情更容易。按后果拆分凭据，不要保留一个强大的令牌，再希望策略过滤每个请求。给代理一个用于普通开发工作的范围狭窄令牌。单独保管会改变生产环境的令牌，并在使用时要求明确决定。凭据数量可能会增加，但授权边界不再依赖脆弱的请求解析。

有人可能提出一个看似安全的建议：“要求批准代理的每个操作。”它之所以听起来安全，是因为会产生完整的人为点击记录。对于重复的低风险调用，这通常是错误的。人们无法认真检查大量相似请求。在审查人员能够作出明确判断的地方要求审批，其余操作记录下来，供团队调查实际行为。

## 无害规则也可能授权有害请求

常见的失败始于这样一个团队：他们希望允许代理更新测试环境服务。他们设置一条规则，当代理声称属于 `staging` 项目时，允许向 `api.example.internal` 发送 POST 请求。由于网关无法直接注入令牌，代理会在环境中收到令牌。

调试任务中，代理执行了一条复制来的命令，使用同一主机名访问管理端点。该端点接受 POST，并支持将配置提升到生产环境的操作。策略看到允许的方法、允许的主机和允许的项目标签，于是返回 allow。

团队可能把它称为策略漏洞，但实际上有多项失败：

1. 方法范围太宽，无法表达意图。
2. 由代理控制的项目属性不能证明所有权。
3. 同一主机上的端点可能有完全不同的后果。
4. 令牌存在于执行控制点之外，可以在请求后重复使用。
5. 操作人员从未看到能够区分测试环境更新和生产环境提升的决定。

增加路由模式也许能堵住这个漏洞。之后又会有人加入带版本的路径、备用主机名、批处理端点或改变行为的查询参数。策略不断膨胀，是因为底层凭据承担了太多权限。

更好的设计是分开测试环境和生产环境凭据。普通会话可以通过网关使用测试环境凭据。生产凭据要求每次使用都审批，而且审批中要显示目标和操作。网关注入凭据并返回结果，代理从未获得凭据值。

这个设计仍然依赖 API 提供方正确限制两种凭据的范围，也不能阻止已获批准的代理在测试环境中做出错误修改。但它能确保测试流程不会因为宽松规则和可复用令牌而悄悄继承生产权限。

## 在代理替你测试之前，先测试拒绝路径

授权测试应该证明系统会在预期条件下拒绝操作，而不只是证明普通请求能够成功。实用的测试案例足够小，可以在团队变更凭据或代理集成前运行。

对于固定决定流程，把预期结果写成表格，并放在实现旁边：

| 保险库状态 | 会话审批 | 凭据设置 | 预期结果 |
| --- | --- | --- | --- |
| 锁定 | 存在 | 普通 | 拒绝 |
| 解锁 | 不存在 | 普通 | 请求会话审批 |
| 解锁 | 存在 | 普通 | 执行 |
| 解锁 | 存在 | 每次调用 | 请求凭据审批 |
| 解锁 | 已撤销 | 普通 | 拒绝或请求新的会话审批 |

这个表格能发现一类严重回归：工程师增加了一条便利路径，先检查会话权限再检查保险库状态，或者让记住的进程跳过每次调用的凭据决定。无需学习策略语言，就能审查预期顺序。

对于策略引擎，不要只测试应该允许的示例。还要测试缺失属性、格式错误的 URL、备用主机名、策略版本变化、规则冲突、时钟变化和明确拒绝。Open Policy Agent 的测试模型支持规则测试，但困难的工作仍由你完成：确定攻击者、有缺陷的代理或未来集成可以影响哪些输入。

还要测试代理处于活动状态时的撤销流程。启动会话并批准它，执行一次普通操作，撤销访问，然后重复相同请求。第二次尝试必须在操作网关处得到拒绝。撤销如果只改变了控制面板记录，却让进程继续持有可用凭据，那就没有撤销实际权限。

对于 HTTP，检查返回结果是否包含足够上下文，以便在不暴露授权标头的情况下诊断失败。对于 SSH，确认辅助程序使用选定身份建立连接，却没有把私钥材料交给调用代理。这些细节听起来平常，直到事件发生时，团队不得不确认进程实际拥有过什么。

## 审计日志回答的问题和提示不同

审批控制在当下阻止或允许操作。审计日志则告诉你事后发生了什么、谁批准了它，以及记录是否被修改。不要把这两项工作合并成一个含糊的“问责”功能。

有用的事件记录应包含会话身份、作出的决定、凭据引用而不是机密值、操作类型、目标、结果和排序信息。还应记录会话撤销。没有这条关联，调查人员能看到请求，却无法判断它发生在操作人员撤回审批之前还是之后。

描述防篡改能力时要准确。哈希链可以在验证者拥有预期链数据时，让后续修改或删除变得可检测。但它不能证明被攻破的系统一开始就记录了每个事件，也不能判断一次审批是否明智。日志提供的是关于已记录历史的证据，不是时光机。

对于本地网关，只写加密审计日志可以形成有用的分工：执行操作的组件记录事件，日常读取者消费投影日志，而不是改写历史。离线验证尤其有用，因为检查链结构不需要可用服务，也不必仅仅为了检查链结构而拥有解密密钥。

Sallyport 将代理运行记录在 Sessions 日志中，将单个操作记录在 Activity 日志中，两者都从加密哈希链审计日志投影而来。它的 `sp audit verify` 命令可以直接在密文上离线检查链，这对于一台可能已经遭到入侵的机器上的重要日志来说，是正确的方向。

让审计审查保持实用。当代理让你感到意外时，先找出获得会话审批的进程，按时间顺序列出操作，撤销活动会话，然后检查受影响的提供方日志。本地日志说明哪些内容经过了网关，目标服务则说明远程系统接受了什么。

## 选择你能运营的最小决定模型

从实际权限路径开始，而不是从对灵活性的抽象渴望开始。列出代理需要的凭据，确定哪些凭据的后果值得重新审批，并决定操作人员如何撤销活动进程。如果最终只有三个稳定决定，就把它们明确表达出来。

当组织需要的规则所有权和变化无法由凭据拆分、会话审批和每次调用审批来表达时，再采用策略引擎。然后接受随之而来的责任：给策略版本化，测试对抗性输入，记录优先级，指定负责人，并像审查代码一样仔细审查例外。

糟糕的设计不是“规则”或“提示”。糟糕的设计是：代理等待行动时，没有人能解释授权边界。 如果团队无法说明某个请求为什么被允许，就先缩小决定模型，再考虑让它变得更聪明。
