# 面向 AI 智能体的可逆 SaaS 用户配置

AI 智能体绝不该用一条含糊的指令来配置 SaaS 用户，例如“把 Priya 加入公司账户，并授予常规工程权限”。这句话至少藏着三次状态变更：创建或邀请身份、分配账户级角色、添加群组成员资格。每次变更的风险、完成条件和撤销方式都不同。

可逆 SaaS 用户配置的起点，是保留这些边界。让智能体先提出操作序列，再逐次调用，记录服务返回的标识符；一旦观测状态与预期不符，就立即停止。多出的几次 API 调用成本很低，却能避免操作员在超时、邮箱错误或群组权限过宽之后，费力还原一个只完成一半的授权过程。

## 邀请、角色和成员资格是不同状态

待处理邀请不是用户，用户不是角色，角色也不是群组成员资格。配置系统经常混淆这些对象，因为供应商控制台把它们放在同一张表单里。底层 API 往往暴露不同资源或生命周期操作，而这种差别直接决定智能体能否安全恢复。

邀请通常表示意图，以及一个投递或兑换流程。收件人可能需要接受邀请，可能使用不同于预期的身份，也可能始终不回应。Microsoft Graph 对外部用户明确记录了这一点：创建邀请会返回邀请对象，受邀人则通过交互流程完成兑换。GitHub 组织成员资格在对方接受前也保持待处理状态。智能体若在邀请后立即记录“用户已创建”，写下的只是愿望，不是事实。

角色改变账户或组织级权限，可能让人成为所有者、账单管理员、管理员、访客或普通成员。群组成员资格通常间接授予项目、仓库、频道、应用或共享数据的访问权。移除群组可能撤销这类间接权限，却不改变账户角色；降低角色也可能保留群组带来的权限。

即使供应商提供能在一个 POST 中接受全部三项内容的便捷端点，也应分别建模。合理的内部记录如下：

```json
{
  "subject": "priya@example.test",
  "invitation": {"state": "pending", "id": "inv_8421"},
  "role": {"desired": "member", "observed": null},
  "groups": {
    "desired": ["engineering", "on-call-readers"],
    "observed": []
  }
}
```

拆开状态后，那个棘手的运维问题就有了明确答案：究竟要撤销什么？邀请存在但账户尚未兑换时，取消邀请；账户角色错误时，恢复原角色；添加一个群组后下一次调用失败时，只移除本次运行新建的成员关系。删除整个用户，通常只是用莽撞方式掩盖自己不知道什么发生了变化。

第一条设计规则因此很简单：一条日志操作应对应一次可从远端观测的状态转换。调用仍可能引发邮件等供应商副作用，但智能体和操作员应能说清主要转换，而不必补上一句“同时还会……”。

## 配置计划必须是数据，而不是散文

智能体应先把人的请求编译成类型明确的计划，再调用供应商。计划会暴露含糊的假设，并为执行器提供稳定输入。自由推理应止于执行之前，执行边界只接收乏味但明确的数据。

计划至少要包含主体标识、目标租户、邀请方式、所需角色、所需群组、前置条件和操作标识。它还应声明是否发送邮件。邀请投递是取消操作无法收回的外部副作用，把它藏在默认值后面很不妥。

```json
{
  "operation_id": "prov_2026_07_24_0187",
  "tenant": "acme-production",
  "subject": {"email": "priya@example.test"},
  "steps": [
    {"kind": "invite", "send_email": false},
    {"kind": "wait_for_acceptance"},
    {"kind": "set_role", "role": "member"},
    {"kind": "add_group", "group": "engineering"},
    {"kind": "add_group", "group": "on-call-readers"}
  ],
  "preconditions": {
    "account_absent": true,
    "allowed_email_domain": "example.test"
  }
}
```

把群组添加保留为独立步骤，不要把数组交给一个宽泛端点。这样执行器能逐项批准、重试和补偿。计划也能暴露顺序。如果供应商要求接受邀请后才能分配角色，`wait_for_acceptance` 就是真正的状态闸门，不是休眠指令。

在暴露凭据或发出网络调用前，用本地约束验证计划。确认租户是已知的精确标识；规范化邮箱域名但不改写本地部分；把供人阅读的群组名解析成供应商不可变 ID；除非请求明确写出，否则拒绝所有者或管理员角色。不要让智能体使用强力令牌搜索所有账户来猜租户标识。

预检读取必须捕获现有状态。按供应商记录的唯一属性查找主体，再读取直接角色和成员记录。把“未找到”和“读取失败”分开。403、超时或分页不完整都不能证明对象不存在。若搜索存在最终一致性或分页限制，应记录限制，并在创建前使用更强的查询。

批准后应冻结计划。如果智能体更改邮箱、角色、群组 ID 或投递标志，就生成新的操作 ID 并重新申请决定。否则，获批的话和真正执行的调用会悄悄分离。

## 先暂存邀请，再授予访问权

先创建或发送邀请，然后停下来，等待服务证明发生了什么。即使端点允许，也不要把特权角色和敏感群组塞进邀请。捆绑操作看起来效率更高，一次请求也只发一封通知，但多数 SaaS API 并不承诺身份创建、角色分配、通知和群组传播之间存在事务。

GitHub 的组织邀请端点很好地说明了这种诱惑。请求可以包含角色和团队 ID，所有者在控制台操作时很方便；自主执行器若一次提交全部内容，却会丢掉有用的检查点。验证错误可能拒绝所有内容，响应在接受后丢失则会让执行器不清楚哪些效果已经发生。人也可能在智能体运行结束很久后接受邀请，届时权限才被激活。

优先使用供应商允许的最低权限邀请。若邀请必须携带角色，就先用普通成员角色，稍后再提升；若必须带一个群组或频道，就选择无法访问敏感资源的等候区，在身份进入预期状态后再添加目标成员关系。有些 API 会限制这套做法。例如 Slack 记录的 Enterprise Grid 邀请方法至少需要一个频道 ID。这说明你该准备低权限到达频道，而不是在首次调用中附上所有工作频道。

记录供应商是否发信，并保存服务返回的邀请 ID、状态、创建时间和规范主体。不要在通用日志中存兑换 URL，因为它可能等同于持有者凭据。如果 API 返回 URL 供单独投递，应让最小的可信组件处理，并从智能体可见结果中删去。

暂存邀请需要明确终止条件。供应商若提供 `accepted`、`expired`、`cancelled` 和 `pending`，就直接使用；否则从公开记录的用户或成员字段推导，并标明这是推导值。绝不能把“邀请 POST 返回 201”解释成“收件人现在能访问生产数据”。

在本地操作记录中设定期限。期限到达后先检查当前状态，只有业务请求已失效且邀请仍待处理时才取消。不要安排盲目取消，因为对方可能刚在定时器触发前接受。先读取、比较，再行动。

邀请取消只有狭义上的可逆性。服务支持时，它能阻止稍后接受，却无法收回邮件，也无法抹去对组织存在的知情。把这个限制写进审批卡。操作员看到“可逆”指远端授权状态，而不是虚构所有后果都能消失，判断会更准确。

## 身份稳定后才能分配角色

等智能体能把请求绑定到稳定的远端用户 ID 后，再分配角色。邮箱适合查找，却不适合作为长期句柄。地址会变，别名会冲突，受邀人也可能通过已有账户兑换邀请。后续调用应使用供应商返回的用户 ID。

改角色前读取当前值，并把它存为补偿值。若目标值已经一致，记录空操作，不要再写一次。空操作是有用证据：它证明执行器检查了条件，也没有冒领其他参与者已完成的变更。

提升权限应区别于普通成员操作。智能体可以在会话批准下分配标准成员角色，而所有者、管理员或账单权限则要求逐次批准。控制边界应追随凭据使用的后果，而不是 HTTP 方法。同一个 `PATCH /users/123`，只因一个字段不同，就可能从日常操作变成灾难。

API 提供比较后设置时，应使用 ETag 加 `If-Match`、版本字段或供应商修订号，防止智能体覆盖预检后发生的人为更改。服务返回冲突时，重新读取并等待审核。不要读取新值后立刻强推原计划；并发变更可能正是操作员需要看到的事实。

角色日志应包含旧值、请求值、观测值、远端主体 ID、响应状态和并发令牌，但不应包含调用所用的持有者令牌。最小成功记录如下：

```json
{
  "operation_id": "prov_2026_07_24_0187",
  "step": 3,
  "action": "role.set",
  "subject_id": "usr_1938",
  "before": "guest",
  "requested": "member",
  "observed": "member",
  "http_status": 200,
  "undo": {"action": "role.set", "value": "guest"}
}
```

更新成功并不代表角色变更已经完成。再次读取用户或成员资源，确认有效值。供应商 API 可能返回 `202 Accepted`，异步应用变更，或分别暴露待处理和活动记录。在读取证明 `observed` 前，日志只能写 `requested`。

若补偿操作是降级，还要考虑降级本身是否需要批准。意外授予所有者后恢复 `guest` 往往比等待安全，但自动回滚可能与人的修正冲突。应提前规定：只有存储版本仍与本次操作创建的版本一致时，才允许自动补偿，否则停下并显示差异。

## 每个群组都应单独调用

每项群组成员资格都应拥有独立步骤、远端目标 ID、结果和撤销指令。宽泛访问经常藏在群组里。`engineering` 这样的名字可能控制源码仓库、部署控制台、事故频道和经同步分配的下游应用。配置智能体不能从友好名称推断影响范围。

从智能体提示之外维护的允许目录解析群组。目录应把显示名映射到租户范围内的不可变 ID，并说明成员资格属于直接、嵌套、动态还是同步类型。若两个群组同名，就拒绝操作；若群组由规则管理，也不要反复直接写入来对抗规则。

Google Admin SDK Directory API 清楚展示了资源边界：添加成员、更新成员和用 DELETE 移除成员各有端点。文档还提醒，嵌套群组成员可能延迟出现，成员循环会被拒绝。这些细节要求执行器检查观测状态，不能假定循环写入会立即一致。

SCIM 也提供了有用行为。RFC 7644 的 PATCH 添加成员示例指出，用户已经在群组中时，服务器应不做更改并返回成功。这便于重试，但不能假设每个 SCIM 实现都完全遵守示例。测试具体供应商，并保留用于核对的查询路径。

群组步骤应区分四种结果：`added`、`already_present`、`rejected` 和 `unknown`。`Already_present` 不得生成撤销项，否则回滚会删除本次操作前就存在的权限。`Unknown` 表示写入可能成功，但响应丢失或验证读取失败。此时需要核对，不能乐观重试。

按权限从低到高处理群组。先加基础协作组，再加生产管理组。这样无法消除失败影响，但执行中止时主体拥有的权限更小。遇到标记为敏感的群组边界时，即使普通群组已成功，也应重新批准。

不要只为降低延迟就并行写入成员资格。并行调用会打乱证据、增加限流处理难度，还可能按不可预测的顺序触发下游配置。依次多发几次调用的成本，远低于调查身份为何在受限数据协议记录前就获得应用许可证。

每次添加后读取直接成员资源，不要读取扁平化的有效成员视图。有效资格可能来自父群组，即使删除直接边后仍为真。回滚收据必须点明本次操作创建的那条边。

## 安全重试从观测状态开始

重试策略无法把任意 POST 变成安全操作。RFC 9110 按预期效果把 PUT、DELETE 和安全方法定义为幂等操作；它还规定，除非客户端知道语义确实幂等，或能判断原请求从未应用，否则不应自动重试非幂等请求。配置代码应严格对待这个警告。

最危险的情况，是邀请 POST 发出后超时。服务器可能已经创建邀请并发送邮件，只是连接随后失败。重复 POST 可能生成第二份邀请或通知。正确做法是按操作的主体和租户读取，再做三选一：接纳匹配的远端对象；仅在确证不存在时重试；结果含糊时停止。

API 若明确支持供应商幂等键，就使用它。从不可变操作 ID 和步骤号派生键，写入日志，并在同一逻辑尝试中重复使用。不要因为请求超时就生成新键，新键是在告诉服务器这是一项新操作。

没有幂等键时，应先为每类写入实现核对函数，再交给智能体。函数必须能用精确条件找到创建对象，不能模糊匹配。邮箱加租户可以标识待处理邀请，用户 ID 加群组 ID 可以标识成员边。如果 API 不支持精确查询，就把不确定结果后的写入归类为需要人工确认。

重试还需要预算。响应包含 `Retry-After` 时照做；对瞬时服务器错误使用有限退避；遇到验证、权限或冲突错误就停止。403 不是一个更慢的 200。重复展示同一个被拒调用，也会训练操作员不看内容就批准提示。

可靠的执行器使用这张决策表：

| 结果 | 下一步 |
|---|---|
| 明确成功且状态已验证 | 提交步骤收据 |
| 明确失败且状态未变 | 记录失败并停止 |
| 请求发出后超时 | 重试前先核对 |
| 响应成功但验证不符 | 记录差异并停止 |
| 限流且提供重试指引 | 在预算内等待 |

把传输尝试与逻辑步骤分开。五次 HTTP 尝试可能只对应一次成员添加。主日志应显示逻辑结果，关联的尝试记录再保留状态码和时间。否则审计人员可能把重复尝试误认为重复授权。

## 回滚是补偿，不是倒转时间

SaaS 配置很少支持分布式事务，因此回滚意味着按相反顺序执行补偿：移除本次运行创建的成员资格，恢复原角色，再在邀请仍待处理时取消它。每项补偿都是一次真实 API 调用，也可能失败、需要批准或遇到并发编辑。

补偿栈应从确认的变更构建，而不是从计划步骤构建。群组原本就存在时，不该移除；角色更新从未到达服务器时，不该恢复；验证结果未知时，不要猜。先核对，再决定是否增加补偿项。

有效收据会保存足够信息，以便尝试撤销并限制撤销范围：

```json
{
  "action": "group.add",
  "target": {"user_id": "usr_1938", "group_id": "grp_77"},
  "result": "added",
  "remote_version_after": "W/\"9012\"",
  "compensation": {
    "action": "group.remove",
    "only_if_direct_membership_matches": true
  }
}
```

版本保护很重要。假设智能体把 Priya 加入群组，随后经理在管理控制台独立确认了这项资格。盲目回滚会删除一个如今已有独立负责人的访问决定。有些服务无法在删除请求中表达条件，此时应读取当前边及其元数据，显示冲突并请人决定。

有些副作用无法补偿。邮件不能收回，审计事件不该删除；即使稍后移除成员，许可证分配也可能影响账单；下游身份提供商可能在源边消失后才传播群组变化。把这些写成操作结果里的残留效果，不要无条件宣称回滚完成。

回滚也应设期限和升级路径。凭据会过期，服务会不可用，原智能体进程也会退出。把收据保存在智能体上下文之外，让另一个可信执行器能继续。操作员需要看到 `rollback_pending`，而不是埋在对话记录里的轻描淡写失败信息。

在非生产租户中用真实 API 行为测试补偿。创建待处理邀请，取消后确认接受链接失效；添加和移除直接群组边，再检查有效访问；更改低风险角色，并在并发冲突下恢复。文档描述预期语义，这些演练会揭示服务真正暴露的行为。

## 日志条目必须证明因果关系

有用的日志能回答：谁请求了变更，哪个智能体进程执行，哪个凭据边界授权，哪个远端对象改变，以及执行器如何验证结果。工具调用对话记录还不够，智能体的叙述可能出错，原始 HTTP 正文也可能过于敏感或庞大。

为每项操作和步骤分配稳定标识。记录计划哈希、精确租户、规范主体、解析后的远端 ID、批准决定、请求指纹、响应状态、验证读取和补偿状态。仅在有助于说明结果时存储删减过的响应片段。对完整响应做哈希可支持日后比较，又不会把个人数据复制进日志。

区分声明与观测。`requested_role: member` 是意图声明，`response_status: 200` 是传输观测，`observed_role: member` 是已验证远端状态。把三者塞进一个 `success` 布尔值，会丢掉事故处理中所需的证据。

命令行视图应明确展示部分完成：

```text
$ provision status prov_2026_07_24_0187
STEP  ACTION                 RESULT            UNDO
1     invitation.create      accepted          unavailable
2     acceptance.wait        observed          n/a
3     role.set               changed           ready: guest
4     group.add engineering  added             ready
5     group.add on-call      denied            none
STATE partial_failure
```

这段输出告诉操作员：账户已存在，角色已改变，一个成员资格成功，最后一个群组失败。它没有把整个运行压成“配置失败”。这一区别能指导回滚，也能帮助人在修正授权后决定继续执行。

应防止执行操作的智能体篡改日志。智能体能改写自己的证据时，记录几乎没有意义。Sallyport 将智能体会话和单次调用作为两个日志视图，底层来自同一份加密、哈希链式日志；`sp audit verify` 可以不需要密钥，直接离线验证密文链。这不能取代供应商侧审计日志，却能为本地操作员提供独立的凭据调用序列。

响应提供供应商请求 ID 时，把它与本地步骤 ID 关联。支持工单可用供应商 ID 查找服务端轨迹，本地记录则解释意图和批准。保留时间戳，但用本地单调序列排列事件，因为时钟与异步供应商事件可能不一致。

保留策略必须有明确设计。配置证据可能含邮箱、群组名和角色历史。只保留问责所需的最少字段，进行加密并限制读取者；若政策允许，辅助响应数据应早于核心操作记录过期。

## 批准应放在后果边界上

当批准卡只描述一个具体后果时，人工批准才有效。“允许配置”过于宽泛，“不发邮件，邀请 priya@example.test 加入 acme-production”则可以审核。“把 usr_1938 从 guest 改为 member”和“把 usr_1938 加入 production-deployers”若风险不同，就应分别决定。

显示已解析标识和当前状态，不要只展示智能体的措辞。批准卡应列出租户、规范主体、操作、变更前后值，以及步骤是否有自动补偿。待处理邀请应说明是否发信；群组访问应在显示名旁写出不可变 ID。

会话授权可覆盖重复的低风险调用，敏感凭据或操作则可要求每次使用都批准。Sallyport 的固定决策阶梯支持这一区分：保险库必须解锁，新智能体进程默认先获得会话授权，逐密钥标志则能要求每次调用都批准。把拥有特权配置能力的凭据放进严格边界，不要让模型自己约束自己。

批准不能弥补薄弱的执行语义。人可能批准了正确群组，却仍遇到超时后的重复 POST。执行器仍负责幂等性、验证和补偿。同样，再完善的日志也不能让权限过宽的凭据变安全。

通过去掉没有决定意义的提示来避免批准疲劳。只暴露有限元数据的读取可以放在会话边界内；确认无变化的空操作应直接记录，不要让人“批准”一项不会发生的变更。只有在界面仍能展示每个目标、回滚引擎仍保存独立收据时，才可以合并真正相同的低风险成员操作。

撤销应停止未来步骤，但不能伪装成已经逆转完成步骤。操作员在第四步后撤销智能体会话时，执行器应取消排队调用、把操作标为中断，并提供补偿计划。它不应偷偷换凭据或开启新会话。

## 失败的运行也必须容易理解

设想智能体要为承包商开通普通账户和两个群组。预检找不到账户；邀请调用在发出后超时；核对找到一份待处理邀请，执行器接纳其 ID。承包商接受邀请，角色更新成功，第一个群组添加成功，第二个群组调用则因令牌无权操作而返回 403。

这是部分结果，不是一个无法区分的错误。此时存在一个活动成员账户和一项直接群组资格。智能体应停止，明确展示被拒步骤，并提供两个有效选项：保留已确认状态，等待操作员取得剩余群组权限；或补偿第一个成员资格，并在处理账户前恢复早先角色。

它不该删除用户，因为删除可能移除数据、破坏承包商已接受的身份，或与下游配置冲突。它不该重试 403，也不该声称已经投递的邀请邮件可以回滚，更不该用权限更宽的群组替代被拒群组。

日志让稍后继续执行变得安全。新执行器加载不可变计划和收据，读取远端状态，检查账户、角色和第一个群组是否仍与观测结果一致。一致时，它只需为剩余成员资格请求批准。若经理期间更改了角色，原计划已经过期，需要重新决定。

这套模式也适用于入职以外的流程。离职操作应拆开会话撤销、群组移除、角色变更、账户暂停和删除，因为它们的紧迫性与可逆性不同。供应商若把许可证分配与群组成员资格分开暴露，两者也应分开。规则始终一致：在智能体计划和证据中保留远端系统有意义的状态边界。

多出的调用是一种有意设置的摩擦。它们让执行器有机会验证身份、约束权限、在差异出现时停止，并且只撤销本次操作造成的变化。自主智能体的工作能在这些边界上接受检查，它才配获得更多自由。如果配置 API 强迫执行器在一次不可逆调用中承担多个后果，就如实标记这项调用，并让人站在它前面。
