# 为 AI 代理使用服务账户，同时避免责任共享

AI 代理需要服务账户，但服务账户绝不能成为你在不知道谁授权某项操作时用来搪塞的名字。为每个长期工作流设置独立身份，在身份背后指定一名明确的人类负责人，并在工作流停止后移除它。更弱的做法会造成责任共享，换句话说，发生错误后没有人能给出完整答案。

我见过这种失败以一种很常见的方式发生。团队创建一个名为 `automation` 的账户，赋予它足够多的权限，以便解决几项编程任务，然后把这个设置称为临时方案。几个月后，部署任务、依赖更新程序和修改基础设施的代理都在使用它。账户一直保持启用，因为关闭它可能会破坏某些东西。当它修改生产设置时，日志完美地记录了 `automation`，却几乎没有解释任何事情。

解决办法不是建立一套庞大的身份体系，而是划定几条硬边界：一个权限用途对应一个工作流身份；由一名明确的人类负责人批准或停止它；用证据把每次使用关联到具体运行；并且在账户获得权限前就设计好退出路径。

## 服务账户标识的是权限，不是操作者

服务账户说明 API 或系统接受了哪组权限，但不能证明请求由哪个代理、提示词、代码版本或人类触发。团队经常把这两种职责混在一起，直到发现审计记录无法解释某次事件。

假设 `release-publisher` 可以发布构建产物。一次以该账户完成身份验证的成功请求，只能说明发布权限被使用了。它无法告诉你，请求究竟来自经过批准的发布工作流、开发者在本地运行的脚本，还是代理在负责人下班后重试的一项旧任务。身份验证记录回答了“哪种权限？”，却回答不了“为什么这次运行现在发出了这次调用？”

应将各层信息分开：

- 工作流身份持有定义清晰的权限。
- 代理会话标识某次具体的进程执行。
- 人类批准或自动化触发器说明谁启动或允许了这次执行。
- 操作记录保存目标、请求的操作、结果和时间。

这种区分会改变调查方式。如果某次部署调用看起来不对，先禁用工作流身份，阻止后续继续使用该权限。然后检查会话记录，找出使用它的进程、运行的代码版本，以及授权该会话的人或系统。单个账户名称无法承载全部历史，否则它就会变成一个共享桶。

RFC 6749 在介绍 OAuth 2.0 客户端凭据授权时给出了一个有用但有限的说法：客户端代表自身行事时，可以将自己的凭据作为授权授予。这对于边界清晰的机器工作负载是正确的。但这不意味着所有能够提交该凭据的进程都拥有相同的合法用途。把授权授予当成完整的责任追踪，正是问题开始的地方。

代理的风险特征也不同于传统任务。传统任务通常沿着固定的代码路径运行。代理可能选择命令、构造请求、使用修改后的参数重试，或者受到它从代码仓库读到的材料影响。即使调用者的选择会变化，服务账户仍然需要稳定的用途。如果你无法用一句通俗的话写清楚用途，这个账户很可能已经积累了互不相关的工作。

## 一个工作流需要一个边界清晰的身份

当工作流拥有独立的用途、权限边界、负责人、环境或结束条件时，应为它设置独立身份。不要为每条提示词或每个短任务创建账户，那只会制造噪声，并不能带来更好的控制。应在可以撤销一整组权限、又不会停止无关工作的层级创建身份。

“更新获批代码仓库中的依赖清单”和“将签名构建产物发布到生产环境”不应使用同一个身份。前者修改源文件并提交评审请求，后者改变发布渠道。即使同一个代理可以发起二者，所需权限不同，应该批准它们的人可能不同，而对可疑操作的响应当然也不同。

另一种错误也会浪费团队时间：因为每次代理运行都创建一个账户名称，就把一个范围狭窄的工作流拆成几十个账户。运行标识符已经能够描述短期执行。服务账户身份应该描述长期授权用途。用会话记录表示单次运行，用账户表示跨运行持续存在的权限。

一个可行的命名模式是：

```text
<environment>.<product-or-repository>.<workflow-purpose>

prod.payments.release-publisher
dev.docs.dependency-updater
prod.data.backfill-reader
```

名称有助于人类理解，但名称不会强制执行范围。每个身份只能绑定工作流真正需要的操作。如果依赖更新程序只需要创建分支并提交评审请求，就只给它这些权限。不要因为它将来可能需要发布权限，就提前授予发布权限。“可能”所造成的长期权限，比任何真实需求都多。

为不同环境使用不同身份。开发代理可以进行实验、频繁重试并操作可丢弃的数据。这种行为不应放在生产身份下。把生产凭据复制到开发配置中，就等于颠倒了边界：控制最弱的环境反而拥有最强的权限。

严格分离有一个合理例外：两次调用可以共享一个身份，前提是它们属于同一个有文档记录的工作流，有相同负责人，使用相同权限集合，并在相同的退役条件下结束。判断标准很实际。如果禁用账户时你会问“这几个互不相关的任务中，究竟哪个坏了？”，说明账户覆盖范围太大。

## 负责人必须是能够停止工作的人

每个工作流身份都需要一名明确的人类负责人，由他承担回答问题的义务，并拥有相应的权力来停止工作。技术联系人可以协助运行工作流，团队也可以提供连续性，但二者都不能替代具体负责人。

负责人要做四件具体的事：确认工作流仍有用途，批准权限变更，在出现可疑调用时响应，并在工作结束后让身份退役。如果列出的负责人无法完成这些任务，这条记录就只是装饰。

NIST SP 800-53 Revision 5 中关于账户管理的 AC-2 控制项要求组织定义账户类型，确定组和角色成员资格的条件，并在账户不再与用户关联或不再需要时禁用账户。虽然人们常常只把它理解为员工账户指南，但这项控制同样适用于非人类账户。没有负责保管人的机器身份，就没有人来设定这些条件，也没有人决定账户是否仍然必要。

应将账户记录放在访问复核流程实际使用的仓库或登记表中。不要把它埋在一个与实际绑定关系逐渐偏离的 wiki 页面里。下面这份最小记录已经包含严肃复核所需的字段：

```yaml
identity: prod.payments.release-publisher
owner: "Morgan Lee"
technical_contact: "Release engineering on-call"
purpose: "Publish approved signed payment service artifacts to production"
allowed_actions:
  - "upload signed artifact to production release repository"
  - "read release metadata for the payment service"
environments:
  - production
permission_bindings:
  - "artifact-repository/publish-payments-prod"
credential_method: "short lived workload token"
source_repository: "payments-service"
review_after: "2026-06-30"
retire_when: "The payment service stops using this release workflow"
incident_contact: "Release engineering on-call"
```

这份记录会让模糊之处显现出来。如果 `allowed_actions` 变成一段包含多个系统和“管理”之类模糊动词的文字，就拆分工作流。如果 `retire_when` 写着“永不”或没有条件，说明账户已经进入永久权限堆积区。如果负责人字段写的是邮件列表，就在授予权限前指定一个人。

所有权变更需要单独的控制。员工离职后，不要因为转移记录麻烦，就让他继续作为名义负责人。要求新负责人接受账户，阅读用途和绑定关系，并设置下一次复核日期。如果没有人接受，就禁用账户。系统不会因为仍在运行，就获得免于明确负责人的资格。

## 共享账户会把小事件变成猜谜游戏

共享账户最明显的缺陷通常出现在看似普通的事件中，而不是戏剧性的入侵中。假设一个团队使用 `prod.agent-ops` 来支持三个工作流：发布代理、事件摘要代理和基础设施修复代理。该账户可以读取部署状态、修改环境变量并触发回滚。

16:20，有人发现一个环境变量被改成了无效值。API 日志显示请求来自 `prod.agent-ops`。发布团队说自己的运行早已完成。事件团队说它的代理只是在读取状态，不认为自己写入了设置。基础设施负责人说那天下午测试过一个修复提示词，但没有人保留确切会话。三种说法都可能是真的，而账户日志无法解决冲突。

通常的做法是搜索聊天消息、代码仓库历史、Shell 历史和模型记录。这样也许能找到答案，但过程缓慢且不完整。更糟糕的是，同一个账户仍然处于启用状态，因为禁用它可能会停止发布恢复工作。于是，一个事件把检测、遏制和无关的生产操作绑在了一起。

独立身份会改变处理顺序。如果 `prod.payments.release-publisher` 发出意外调用，就禁用这个身份。事件摘要账户和修复账户仍可保留各自权限。操作记录应包含工作流身份和运行引用，让调查人员能找到确切会话，而不必根据记忆争论。这就是为什么“一团队一个账户”不是折中方案，而是主动合并故障域。

有些团队为共享账户辩护，理由是集中管理更容易轮换凭据。由于只需要替换一个凭据，这种感觉似乎有道理。但运营上的节省很小，代价却会在需要定向撤销、权限复核或解释某次操作时出现。应自动化凭据签发和绑定管理，而不是为了方便就把账户边界扩大。

不要把共享账户和共享权限角色混为一谈。多个身份可以获得同一个定义清晰的角色，只要它们执行的是相同的允许操作。身份仍然彼此独立，因此日志和撤销仍然有效。复用角色可以保持可管理性；复用身份却会摧毁归因能力。

## 权限范围应遵循操作，而不是代理的野心

应根据打算允许的确切操作路径授予权限，然后要求代理证明它还需要其他权限。代理具备一般能力，并不能证明它应拥有一般权限。

从工作流的动词和对象开始。“读取代码仓库 A 中的未解决问题，并在代码仓库 A 中创建分支”是操作描述。“维护代码仓库 A”则不是。第一种说法能让管理员找到读取权限和创建分支权限，第二种通常会因为无法映射到精确权限而最终获得广泛写入权限。

同样的纪律也适用于 API 权限。如果工作流读取报告并发表评论，不要因为 API 把这些端点放在一个看似方便的宽泛角色中，就授予账户管理权限。如果服务商允许，应建立更小的权限集合。如果不允许，就在宽泛凭据前放置一个针对具体操作的中间层，或者重新考虑是否应让代理无人值守地执行该操作。

许多代理部署会在这里犯一个细微错误：代理需要先检查上下文再采取行动，于是团队就授予它广泛权限。读取上下文和修改目标属于不同权限。尽可能提供读取权限，再为状态变更要求独立身份或审批路径。模型可以读取生产配置，并不意味着它也需要编辑配置的权限。

在正式使用工作流前，用故意错误的请求测试边界。对于发布程序，尝试发布另一产品的构建产物、删除发布版本和修改代码仓库设置。每个请求都应在授权层失败。成功的正常流程测试只能证明你授予了足够权限；相邻操作被拒绝，才能证明你没有授予过多权限。

将测试结果保存在所有权记录中。它可以是一张简单的小表：

| 尝试 | 预期结果 | 复核结果 |
| --- | --- | --- |
| 发布获批的支付构建产物 | 允许 | 已确认 |
| 发布另一服务的构建产物 | 拒绝 | 已确认 |
| 删除生产发布版本 | 拒绝 | 已确认 |
| 修改代码仓库成员资格 | 拒绝 | 已确认 |

不要让代理从一组强大账户中自行选择身份。工作流运行器应自动附加与任务对应的身份。能够在互不相关的权限之间选择的代理，往往也能绕开你设计的边界。

## 凭据应在被遗忘的工作流之前过期

长期凭据会让退役变得困难，因为被遗忘的副本在你禁用可见任务后仍可能继续工作。优先使用签发给经过验证的运行时的短期工作负载凭据，或者让受控组件执行经过身份验证的操作，而代理只接收结果。

这是独立于身份设计的另一个问题。你可以拥有一个命名清晰、负责人明确的服务账户，但如果凭据留在代码仓库、本地环境文件、代理记录或构建日志中，仍然可能失去控制。账户记录说明谁可以使用权限，而凭据管理决定谁实际上能够提交它。

推荐的顺序很简单：

1. 验证调用运行时或代理会话。
2. 签发具有短期有效期和有限工作流范围的凭据，或代表代理执行请求的操作。
3. 记录请求的操作和授权决定。
4. 结束会话，并使仅属于该会话的权限失效。

不要仅仅因为代理需要调用 API，就把明文密钥交给它。这样一来，每条提示词、工具输出、跟踪记录和意外日志都可能变成凭据传播路径。对控制台输出中的密钥值进行遮蔽，有助于降低暴露后的影响，但无法防止代理接收并重新使用密钥。

对于支持的 HTTP 和 SSH 通道，Sallyport 采用第二种方式：代理通过 MCP 连接请求操作，应用将 API 和 SSH 凭据保存在加密保险库中，并自行执行操作。这样可以让凭据留在代理上下文之外，但并不能免除你为不同工作流设计独立身份和负责人的需要。

对于必须直接使用凭据的系统，应记录凭据在哪里签发、如何交付、最长有效期以及谁可以撤销它。轮换不应依赖某个人记得日历上的日期。应把签发和替换纳入工作流部署流程，并在账户变得重要之前测试一次轮换。

短期凭据并不意味着可以忽略日志。代理在短会话中也能造成实际损害。过期机制限制滥用后的持续时间；范围、审批和操作记录限制会话期间能够发生的事情。

## 审计记录需要把权限关联到具体运行

有用的审计轨迹能够让你重建一次操作，而不会把服务账户当成全部故事。应在调查人员可以关联的记录中保存工作流身份、代理运行标识符、发起触发器、授权决定、目标、操作、结果和时间戳。

使用一致的事件结构。下面的 JSON 不依赖具体服务商，但包含了人们在事后通常希望拥有的字段：

```json
{
  "event_id": "act_01J8Q7M6K4",
  "time": "2026-04-14T16:20:31Z",
  "workflow_identity": "prod.payments.release-publisher",
  "run_id": "run_8f3c1d",
  "trigger": {
    "type": "approved_ci_job",
    "initiator": "morgan.lee",
    "source_revision": "a1b2c3d4"
  },
  "authorization": {
    "decision": "approved",
    "approved_by": "morgan.lee",
    "approval_ref": "apr_31fa"
  },
  "action": {
    "target": "production artifact repository",
    "operation": "publish",
    "resource": "payments-service/2.4.1"
  },
  "result": "success"
}
```

不要因为想要完整的取证细节，就把凭据、包含敏感材料的完整提示词或不受限制的载荷放入审计记录。记录足够稳定的上下文来建立因果关系，然后对它应用与其他运营日志相同的数据处理规则。审计系统如果变成第二个秘密存储区，就会创造自己的事件路径。

完整性同样重要。代理或遭入侵的工作流可以修改的日志，无法解决争议。使用只追加存储，将写入权限与读取和管理权限分开，并定期验证完整性。即使账户退役后，也要保留证据。退役会移除未来权限，但不能抹去解释过去操作所需的历史。

Sallyport 的 Sessions 和 Activity 日志由一份只能写入、无法读取的加密哈希链审计日志投影而来，`sp audit verify` 命令可以离线检查哈希链，无需保险库凭据。这为它代理的操作提供了有用证据，但周边系统仍需保留工作流负责人、触发器和业务审批上下文。

复核应围绕能够在几分钟内回答的问题展开：哪个工作流拥有这项权限？当时谁是负责人？哪个运行使用了它？什么批准了这次运行？究竟哪个操作成功或失败？如果任何答案都需要从聊天历史中重新拼故事，你的记录就不完整。

## 退役是一套工作流，不是年度清理任务

当工作流结束、负责人无法替换，或重大变化使原有用途不再成立时，应让服务账户退役。年度复核可以发现过期账户，但对于已经知道的事件来说太慢了。

在最初的账户记录中写入退役条件。迁移工作流可以在迁移完成时退役；代码仓库自动化可以在仓库归档时退役；供应商集成可以在合同结束时退役。这些条件让决定不再依赖争论，因为账户一开始就有约定好的结束时间。

退役身份时应按以下顺序操作：

1. 禁止账户的新使用，并撤销有效凭据或绑定。
2. 在明确的观察期内监控预期失败，找出未记录的依赖。
3. 只有在确认存在真实依赖后，才恢复最低限度的权限；如果工作流仍然合理，还要指定新负责人并建立新记录。
4. 按照保留规则保存所有权记录、访问历史和操作日志。
5. 观察期结束后，移除身份及其剩余凭据。

不要一开始就删除账户。删除可能移除有用配置，让故障更难诊断。禁用可以先完成遏制，也能让普通监控暴露隐藏调用者。它还会迫使团队进行一次有用的讨论：如果某个工作流因此中断，谁会认领它？为什么它不在登记表中？

负责人离职需要立即处理。在其最后工作日前，只有在接替者接受责任后才能转移所有权。如果没有接替者，就禁用账户。团队以后可以因为有记录的运营需求重新启用它，但孤立账户不应因为“也许有人会需要”而继续拥有权限。

工作流扩展时也需要退役。如果依赖更新程序开始部署代码，不要在原有用途上继续添加内容，直到它描述了两个互不相关的工作。应降低或退役旧身份，然后创建一个拥有独立负责人、范围、测试和复核记录的部署身份。这样可以保留两个账户各自的审计含义。

## 只有复核能够撤销权限，责任追踪才能持续

服务账户复核只有在复核人员能够看到实际绑定、确认当前负责人，并且无需一周协调就能禁用访问时才有价值。在真正需要之前，就把这项权力纳入运营流程。

更频繁地复核风险最高的工作流，但不要把每次复核都变成文书仪式。应询问：记录中的用途是否仍然存在？列出的负责人是否仍有权限？近期操作是否符合用途？权限范围是否仍与实际使用相匹配？如果答案不清楚，就在负责人澄清期间减少或禁用账户。

用能够暴露被忽视权限的事实衡量健康状况：没有负责人的账户、超过复核日期的账户、在有意义的时间段内没有操作历史的账户、凭据接近或超过预定有效期的账户，以及近期操作超出记录用途的工作流。这些是复核队列，不是为了好看的指标。

第一项实际行动是导出现有代理身份，并在每个身份旁写下一句话：“这条工作流可以在条件 W 之前，由负责人 Z 为对象 Y 执行操作 X。”无法写出这句话的账户，正是隐藏着共享责任的账户。在继续增加代理能力之前，先禁用或拆分它们。
