# 如何控制 AI 代理的刷新令牌

AI 代理不应持有刷新令牌。这个规则听起来很严格，但只要看看令牌的作用就能理解：它允许某个进程持续获取访问权限，即使最初的人类批准已经淡出视线。访问令牌可能很快过期，刷新令牌却会让这段关系继续存在。

更合理的设计，不是让代理更擅长保护 bearer 凭据，而是把凭据放进由人工控制的进程负责的操作层。代理请求执行某项具体的外部操作，操作层判断这次运行是否可以执行，只有在需要时才刷新令牌，然后返回结果。这样，你就有了一个可以审批、撤销和调查使用情况的位置。

单靠密码保管库解决不了问题。保管库负责保护存储，代理操作层负责管理使用。团队经常把这两项工作混在一起，直到发现：即使令牌放在加密存储中，只要某个进程能向存储提出正确的问题，它仍然可以拿到令牌。

## AI 代理刷新令牌会改变信任边界

AI 代理刷新令牌之所以危险，是因为它会把权限延伸到最初需要令牌的代理进程之外。如果编程代理能从环境变量、配置文件、浏览器配置文件或密钥管理器响应中读取刷新令牌，它就能通过自己可以调用的任何 HTTP 客户端使用该令牌。令牌不再属于某个边界明确的任务，而属于任何取得该进程控制权的对象。

OAuth 2.0 的 RFC 6749 将刷新令牌描述为一种凭据，用于在当前令牌过期或失效后获取访问令牌。规范允许不使用刷新令牌，但这不代表它无害。提供商签发刷新令牌，是因为每次续期访问令牌都进行交互式授权会很麻烦。正因为这种便利性，无人值守的代理才不应持有刷新令牌。

请把下面三件事分开：

- 账户所有者，是授予访问权限的个人或服务身份。
- 操作请求者，是此刻请求调用 API 的代理进程。
- 凭据保管者，是保存刷新令牌并与令牌端点通信的组件。

在小型环境中，同一个人或程序可以承担多个角色，但这些角色仍然必须存在。如果同一个代理同时承担三者，它就能连接新账户、扩大作用域、无限刷新，并把操作藏在普通请求中。这不是授权设计，而是给聊天机器人接上了 bearer 令牌。

我见过团队说“代理只需要读取权限”，然后把拥有仓库、电子邮件或云服务广泛权限的刷新令牌放进本地 `.env` 文件。访问令牌的有效期可能很短，但刷新令牌会让这个错误持续存在。提示注入不需要说服模型泄露密码，只要说服模型利用仍然有效的授权，发出一个在当前上下文中看似合理的请求即可。

边界应当位于凭据进入代理进程之前。代理既不应收到令牌值，也不应收到一个可以在其他地方兑换的假占位符。它得到的应是操作接口，例如获取这条 issue、创建这份草稿、读取这项部署状态，或打开到指定获批主机的 SSH 会话。操作层负责这些操作背后的协议细节。

## 必须由人拥有授权，而不只是批准一次提示

在 OAuth 连接建立前，就应明确谁可以批准刷新。“任何开发者都能点击允许”这种做法，在账户所有者离职、共享邮箱易手，或代理以他人身份重新连接服务时就会失效。

对于个人 SaaS 账户，账户所有者应完成初始授权，并在撤销或过期后批准重新连接。对于共享运维账户，应指定一名负责所有者和一名可以撤销授权的备份人员。对于机器身份，服务所有者应授权客户端注册和作用域。不要因为人和机器身份都能调用同一个 API，就把它们当成同一种身份。

把经常被一次浏览器点击合并的四个决定分开：

1. 谁可以创建最初的授权。
2. 哪个操作层可以保留由此产生的刷新令牌。
3. 哪些代理会话可以使用这项授权执行操作。
4. 谁可以撤销授权或批准新的连接。

最初的 OAuth 同意页面只回答第一个问题，有时回答得还不完整。它告诉提供商，账户持有人授权了一个具有指定作用域的客户端，却不会告诉你的本地系统：不受信任的仓库、新的代理子进程或夜间任务是否可以使用这项授权。

实用的所有权记录不应只有提供商账户邮箱。应保存一条本地授权记录，包含不透明的授权 ID、提供商名称、账户引用、作用域集合、所有者、备用撤销人、连接日期，以及允许请求该授权的操作。不要把刷新令牌复制到这条记录中。记录的作用是解释令牌，而不是成为第二个秘密存储。

共享访问是最棘手的情况。团队常常因为方便而连接一个管理员账户，然后让每个开发者的代理都使用它。这会破坏责任追踪。如果 API 支持服务账户、应用安装、委派身份或作用域更窄的项目令牌，应优先使用它们。如果不支持，就把操作层限制为少量获批操作，并记录共享账户的实际所有者。

不要让代理自行发起新的 OAuth 浏览器流程。代理可以展示合法的提供商页面，也可能把人引向权限更广的账户、更宽的作用域选择或不同的租户。发起连接属于管理操作。应要求人从操作层发起连接，并在同意前检查账户和作用域列表。

## 操作层只应为完成获批操作而刷新令牌

受控的操作层只有在收到经授权且需要当前访问令牌的请求时，才应兑换刷新令牌。不要让它运行一个“以防万一”定期刷新所有凭据的后台循环。预先刷新在代码中看起来整洁，却会让事件响应更困难，因为它会在没有对应的人类或代理操作时持续维持授权。

请求路径可以很简单：

1. 代理会话请求一项命名操作，并提供普通的操作参数。
2. 操作层找到与该操作关联的授权，然后检查该会话是否可以使用它。
3. 如果缓存的访问令牌不存在或即将过期，操作层就把刷新令牌发送到提供商的令牌端点。
4. 操作层使用访问令牌调用目标 API，并把经过筛选的结果返回给代理。
5. 操作层记录操作和刷新事件，但不记录凭据内容。

代理永远不应选择令牌端点、客户端标识符、回调 URL 或作用域字符串。这些值属于经过人工批准的连接定义。允许代理提供这些值，会把你的网关变成开放的令牌中继。

假设代理被要求向项目跟踪系统发布一份版本说明。它请求操作层在指定项目中创建一条 issue。操作层发现该操作需要这个项目的跟踪系统授权，于是检查请求会话，在必要时续期访问令牌，并发布说明。返回给代理的可以是新 issue 的 ID，以及提供商返回的类似 URL 的引用，而不是用于创建它的 bearer 令牌。

现在改变提示内容。恶意仓库指令要求代理通过列出组织中的所有项目，并在每个项目中创建测试 issue 来“验证访问权限”。如果代理持有刷新令牌，这条指令就可能变成一连串直接的 API 调用。如果代理只有命名操作接口，操作层可以拒绝超出获批项目范围的请求，或要求再次获得人工授权后才继续会话。

这不需要复杂的策略语言，只需要一组小而易懂的选择：哪个进程在请求、它可以使用哪项授权，以及这次操作是否需要人工批准。选项更多并不会自动让设计更安全，反而常常让操作员无法判断最终生效的是哪条规则。

Sallyport 采用了这种分离方式：API 凭据保存在加密保管库中，HTTP 或 SSH 操作通过 MCP 连接执行，而不是把凭据返回给代理。

## 授权类型决定了哪些自动化方式是安全的

如果要在桌面或本地应用中连接人的账户，应使用带 PKCE 的授权码流程。用户在提供商处登录，检查同意请求，然后通过已注册的重定向路径返回本地应用。PKCE 会把授权响应绑定到发起流程的客户端，降低被截获的授权码的价值。

RFC 9700《OAuth 2.0 安全最佳实践》规定，公共客户端必须使用 PKCE。它还规定，公共客户端的刷新令牌必须使用发送者约束或刷新令牌轮换。这项指导对代理集成很重要，因为本地应用通常属于公共客户端。在桌面应用中内置客户端密钥，并不会让它变成机密客户端。任何拿到应用的人都可以提取该密钥。

根据要连接的身份选择流程：

- 人的提供商账户使用带 PKCE 的授权码流程。
- 如果提供商支持，且不需要委派人的账户，则为服务身份使用客户端凭据。
- 如果提供商提供更细粒度的项目或组织访问，应使用其专用的安装或应用模型。
- 只有在提供商和运行环境确实要求时才使用设备授权，并清楚展示用户正在批准的身份和作用域集合。

客户端凭据通常不会产生刷新令牌，因为客户端可以再次验证自身身份来请求新的访问令牌。如果服务身份权限很窄，且客户端认证材料一直留在操作层中，这种方式对自治任务可能更安全。但不要借客户端凭据之名，把权限广泛的客户端密钥交给编程代理。

离线访问需要特别留意。一些 OpenID Connect 提供商要求 `offline_access` 作用域，才会签发刷新令牌。只有当操作确实需要在交互式会话结束后继续运行时，才请求它。如果每次操作都有人在场，短时访问令牌配合新的授权可能更合适。团队常常默认请求离线访问，因为这样不用处理过期问题，却把一次麻烦换成了长期凭据。

完全不要使用资源所有者密码凭据。RFC 9700 已将这种授权方式标记为过时，因为它会把用户密码交给客户端。操作层并不能让这种做法变得可以接受，只是多了一个丢失密码的位置。

## 只有在存储能正确处理替换时，轮换才有用

刷新令牌轮换会在每次成功刷新后用新令牌替换旧令牌，从而减少令牌被复制后的损害。提供商可以检测旧令牌的重复使用，并使受影响的授权令牌族失效。这种检测很有帮助，但如果自身的刷新逻辑不严谨，也可能把合法集成锁在门外。

最常见的问题是竞争条件。两个代理会话几乎同时需要访问令牌，并且都读到了同一个旧刷新令牌。第一个会话成功刷新并收到新值，第二个会话片刻后提交旧值。根据提供商的行为，第二个请求可能失败，也可能触发重复使用检测，使包括新令牌在内的整个令牌族失效。

每项授权只设置一个刷新所有者，就能避免这种竞争。操作层应按授权 ID 串行处理刷新任务。第二个调用者等待第一次刷新完成，然后使用新缓存的访问令牌，而不是再次发送令牌请求。这是正确性要求，不是性能优化。

在认为刷新成功、允许后续工作前，先保存替代令牌。安全的顺序如下：

1. 通过 TLS 将旧刷新令牌发送到令牌端点。
2. 验证令牌响应，并确认它对应预期的提供商和授权。
3. 在加密存储中通过一次持久化更新写入新刷新令牌和元数据。
4. 在本地状态中标记旧令牌不可用。
5. 使用新的访问令牌释放等待中的调用，或让它们重新发起请求。

如果进程在提供商轮换令牌后、而本地存储记录替代令牌前崩溃，你可能会失去这项授权。重试无法解决这个问题。恢复路径是由人主导的重新连接，这正是所有者和撤销人记录重要的原因。

有些提供商只在部分情况下签发新的刷新令牌，另一些会返回同一个令牌。代码必须同时兼容这两种行为，不能假定其中一种一定成立。在确认提供商响应和持久化写入成功前，只保留旧值。调试时绝不要记录任一令牌值。很多令牌泄露都始于临时调试语句，最后却跟着版本一起发布了。

发送者约束令牌可以把令牌绑定到客户端持有的加密密钥，从而降低重放风险。RFC 9449 定义的 DPoP 就是一种方法。但它并不能消除保管责任。如果代理同时可以使用刷新令牌和私有签名密钥，它仍然拥有持久权限。应把两类材料都放在操作层之后，并在依赖这种约束前测试提供商的实际行为。

## 撤销需要指定操作员和经过测试的路径

撤销不是一次启用后就不用管的设置。它是有人必须在压力下执行的操作，因为那时提供商控制台可能很慢，也没人记得到底哪个账户批准了这项集成。

为账户所有者和指定的备份人员提供直接的撤销路径。撤销授权时，操作层必须删除本地刷新令牌，使缓存的访问令牌失效，并停止仍能请求该授权的会话。只禁用代理界面、却把凭据留在存储中，不算完整撤销。

RFC 7009 定义了 OAuth 令牌撤销请求。提供商会公布自己的端点，但请求通常类似下面这样：

```http
POST /revoke HTTP/1.1
Host: authorization.example
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <client authentication>

token=<refresh-token>&token_type_hint=refresh_token
```

RFC 7009 要求服务器即使在提交的令牌已经失效或未知时，也返回成功响应。这可以防止攻击者利用该端点充当令牌有效性探针。这也意味着，操作员不能仅凭 HTTP 成功响应就认定授权仍然拥有活动访问权限。应记录已发送撤销请求，删除本地凭据，然后通过一次无害的提供商调用或提供商审计记录进行确认，如果服务提供此类记录的话。

应为以下事件准备撤销流程：账户所有者离职、怀疑代理会话遭到入侵、仓库指令导致意外外部调用、集成退役，或提供商报告令牌被重复使用。不要等到发生泄露后，才决定谁有权按下按钮。

已经签发的访问令牌可能会一直可用到过期。有些提供商会立即撤销它，有些不会。本地层可以立即停止签发新的操作，这是你真正控制的部分。除非提供商明确记录并且你已经测试过，否则不要承诺全局即时失效。

把提供商撤销和本地禁用分开。 本地禁用会阻止操作层使用授权，提供商撤销则要求提供商也拒绝它。事件处理中应按这个顺序执行：先切断自己的执行路径，再发送提供商请求。第一步由你控制，不应依赖外部网络调用。

## 审计记录必须解释意图，而不只是记录流量

HTTP 调用清单无法告诉你一次刷新是否合理。你需要一条记录，把人工决定、发起请求的代理会话、授权引用和最终的外部操作连接起来。

不要把刷新令牌、访问令牌、授权码、客户端断言或完整 API 正文写入审计日志。令牌字符串本身就是秘密。完整响应正文可能包含客户数据、仓库内容或个人信息。为方便而记录这些内容，会制造出第二个更混乱的凭据和数据存储。

有用的事件记录应包含事件 ID、时间、会话 ID、请求进程身份、授权 ID、连接账户引用、操作名称、提供商主机、请求资源、连接时记录的作用域集合、批准引用、结果类别，以及发生错误时的错误代码。对于刷新，记录发生过刷新以及是否成功即可，不需要令牌值就能调查。

操作记录和凭据记录的区别很重要。操作记录说明某个代理会话请求获取指定环境的部署状态，操作层允许了该操作。凭据记录说明哪个授权支持这次请求，以及谁拥有它。两者可以通过不透明的授权 ID 关联，但不要让所有能查看操作历史的操作员都能看到账户连接详情。

防篡改机制会改变调查质量。如果遭到入侵的本地进程可以修改自己写入的同一份日志，攻击者就能删除最重要的记录。应使用追加写入的事件处理和完整性检查，并由生成日志的进程之外的组件独立验证日志。

Sallyport 从加密、哈希链式的审计日志生成 Sessions 和 Activity 日志，`sp audit verify` 无需保管库密钥即可离线检查这条链。

应根据授权的权限安排相应频率的审查。个人 issue 跟踪器授权可能偶尔审查即可。能够修改生产基础设施的授权，则应在每次新连接、每次作用域变更和任何异常代理行为后审查。操作层应让记录足够易读，使所有者能回答：“哪个代理以什么目的使用了我的账户，又是在谁的批准下进行的？”

## 浏览器配置文件和通用令牌代理会形成隐蔽的绕过路径

浏览器配置文件不适合作为代理的凭据存储。它可能包含会话 Cookie、缓存的访问令牌、刷新令牌、账户选择器以及无关的浏览状态。让代理访问这个配置文件，比委派一项 API 操作宽泛得多，也更难清理，因为提供商状态和浏览器状态混在了一起。

如果通用令牌代理接受调用者任意提供的令牌端点参数，也会造成同样的问题。团队经常构建一个名为 `getToken(scope)` 的 API，因为令牌不再位于代理进程中，就觉得更安全了。如果任意会话都能请求任意已连接账户或任意作用域，这个代理仍然是令牌自动售货机。

让调用者请求操作，而不是请求令牌。“在项目 A 中创建版本说明”有所有者、目标和可检查的作用域要求。“给我一个 tracker.write 令牌”则把太多权限留给了调用者。

不要在还不了解操作之前，就用庞大的规则引擎解决问题。一份与指定授权和人工审批点绑定的简短获批操作目录，更容易审查，也更难绕过。只有真实的运行需求要求时，才增加复杂度。

还要避免在开发、预发布和生产环境之间共享同一个刷新令牌。分开的授权能让撤销影响更小，审计记录也更清楚。预发布代理不应因为两个环境碰巧使用同一个身份提供商，就保留通往生产环境的路径。

## 从所有权表和一次撤销演练开始

第一份有用的成果是授权清单，而不是代码。为每个代理可能触发使用的刷新令牌建立一行记录。包括提供商、账户引用、授权 ID、作用域、操作层位置、账户所有者、备用撤销人、创建方式、最近确认使用时间和本地禁用步骤。如果有一列填不出来，就说明你还没有真正控制这项授权。

然后使用低风险集成进行一次撤销演练。让所有者在本地禁用授权，在提供商处撤销它，再尝试执行一项普通代理操作。确认操作层会阻止请求，重新连接需要明确的人工操作，审计轨迹能识别之前的会话。这项练习会迅速暴露各种假设：缺失的提供商端点、未知的账户所有权、存放在旧开发者电脑上的令牌，以及超出本地预期仍然有效的缓存访问令牌。

为没有提供商强制期限的授权设置到期审查。无人值守工作有时确实需要长期访问，但无限期访问应当是经过明确决定的例外，并指定所有者。如果团队说不出所有者是谁，这项授权就不应继续可用。

应坚持一个简单标准：代理可以请求工作，但不能继承永久续期账户权限的能力。把刷新凭据放在人的控制范围内，让撤销成为经过练习的操作，而不是出事后在浏览器标签页中紧急寻找。
