你留下了令牌,却没有留下那份同意。
当一位用户把自己的邮箱连到你的智能体上,被创造出来的是两样东西:一个访问令牌——每个系统都会存它;以及一份同意——某个具体的人,在某个具体的时刻,声明某个具体的权限范围可以用于某个具体的用途——而这个几乎没人存。令牌回答的是"我们能不能调这个 API"。而你真正会被问到的每一个问题,无论提问的是用户、审计人员还是客户的安全团队,答案都在那份被你扔掉的记录里。
委托会产生一份记录,而令牌不是它。
一个代表用户行事的智能体,行使的是被委托的权限。这是个好设计——远好过让一个服务账号持有所有人权限的并集,正如智能体身份与权限所论证的。但委托是一段带条款的关系,而那份凭据只是它的副产品。
条款是五个事实:谁授予的、授予了什么范围、在什么时候、代表谁——是他自己,还是他管理的某个组织——以及他被告知这将用于什么用途。一个 refresh token 至多编码了其中第二项。去问一个典型的智能体部署"哪位用户在哪一天同意了哪个范围",它会通过翻查凭据存储来作答——也就是从一个每次刷新都被静默覆写的对象去推断过去。
现实后果会在你第一次被客户问到"你们的智能体在我们租户里被允许做过什么"时到来,而诚实的回答是一份当前有效令牌的清单,而不是一段决定的历史。
四个问题,以及回答它们的字段。
从"会被问到什么"倒推着设计这份记录。下面四个会被例行问到,而每一个要么对应你在授权时写下的东西,要么就是你重建不出来的东西。
- "这是谁允许的?"——来自数据被动过的客户。需要授权人的身份、他是为自己行事还是作为管理员为他人行事,以及他当时而非今天所持有的权限。
- "这次动作是在哪份许可下做的?"——发生在任何有争议的写入之后。需要在动作记录上打上一个同意标识,这意味着审计留痕必须把它从授权一路带到调用。
- "当时告诉他的用途是什么?"——来自监管方、隐私评审,或一位觉得意外的用户。需要当时展示的那段用途文本,并带版本。范围字符串不是用途;没人是对着
https://www.googleapis.com/auth/gmail.modify点头的,他们点头的是你写在它旁边的那句话。 - "现在还允许吗?"——这个看起来无足轻重、其实不然。需要当前状态和全部状态转移:已授予、已扩展、已过期、被用户撤销、被管理员撤销、被你撤销。
注意四个里有三个问的是过去,而过去恰恰是凭据存储不保留的东西。同意台账要做成只追加的,理由和总账要只追加是同一个:当前余额可以推导出来,历史不行。
授予的范围、实际行使的范围,与被理解的用途。
三件不同的事常被混为一谈,而投诉就住在它们之间的缝隙里。
授予的范围是 OAuth 提供方签发的东西。实际行使的范围是你的智能体真正执行的那组操作——通常是一个很小的子集,因为提供方给的范围很粗,而你挑了能覆盖你场景的那一个。被理解的用途是用户以为自己在同意的东西,它来自你写的那句话,而不是提供方的对话框。
两项纪律能合上这些缝隙。第一,记录实际行使的范围,而不只是被授予的:一份月度报告,显示某个被授予了完整邮箱修改权限的智能体从头到尾只做过读取与打标签,这是面对安全评审时最有力的回答,而它的代价只是在你本来就有的数据上做一次聚合。第二,把用途当作与授权绑定的、带版本的文本,这样当产品下个季度做得更多时,你能分清哪些用户同意的是旧那句、哪些同意的是新那句。
然后依着第一项纪律行动:如果实际行使的范围整整一个季度都是严格子集,那就去申请更窄的那个范围。这就是受限凭据一直在要求的最小权限的具体而朴素的形态,而使用数据正是让它成为一个容易的决定、而不是一次猜测的东西。
撤销是那条没人测过的路径。
一位用户打开自己的 Google 或 Microsoft 账号设置,断开了你的应用。接下来你的系统里发生什么,通常是没有被设计过的,而这也是这个领域里最常见的那个缺口。
令牌失效了,而你会在某次调用返回 401 时才发现——可能是几小时之后,也可能是在一次无人值守的运行途中。那还是容易的部分。难的部分在于:撤销针对的是权限,而权限早已扩散出去了:
- 派生数据比凭据活得久。智能体读过的一切现在都在一份对话记录、一份摘要、一条被抽取的记忆、一个向量索引和一层缓存里。提供方那个撤销按钮碰不到其中任何一样,而按下它的用户会合情合理地以为它碰得到。
- 排队与定时的任务仍然带着意图。一次在撤销之前入队的后台运行,会在撤销之后照样执行——除非有什么去检查。每一个代表用户行事的任务,都必须在执行时而不是入队时重新校验同意。
- 多人共享的场面会悄悄变得不对称。在一个共享工作区里,一位成员的撤销改变了智能体能为所有人做什么,而界面上没有任何地方说了这件事。
- 下游后果不会回卷。它提过的工单、发出去的消息都还在。撤销停掉的是未来的权限;它不会逆转已发生的行动,而向用户暗示相反的事就是那个错误。
所以把这次扩散明确写下来:撤销时停掉后续调用、取消或失败该授权下排队的任务、决定并记录派生数据将如何处置,并把这次撤销作为一个带时间戳的事件记下来。然后像演练恢复那样演练它——一条从未被走过的撤销路径,和一份从未被恢复过的备份处境相同,而两者都会在最糟的时刻被发现。
重新征求同意是一个真实事件,有三个常见触发点。
同意并非永久,而需要重新征求的那些时刻,可预测到足以提前规划。
- 范围扩展。智能体获得了原授权未覆盖的能力。这需要一次新的授权,而不是悄悄放宽——而且台账里两者都该在,好让"我们是从什么时候开始能发邮件的"有个答案。
- 用途变更。范围没变,用法是新的:原本为工单分诊读取的数据,如今用来训练一个建议功能。技术上并不需要走 OAuth 流程——而这恰恰是它被跳过的原因,也是隐私监管方最在意的那一个。
- 更换 OAuth 客户端。令牌是针对某个特定客户端签发的,所以更换集成厂商——或者从平台的共享客户端换成你自己的——会让每一份既有授权失效,逼着每一个用户重新走一遍同意。这是团队很晚才发现的迁移成本;为什么"采纳当天就注册自己的客户端"是一天的活却省下一个季度的活,见智能体连接器平台。
管理员同意值得有自己的记录形态。当一位 IT 管理员为整个租户授权你的应用时,授权人与被授权覆盖的主体是不同的人,范围覆盖了从未见过任何对话框的用户,而撤回路径要经过管理员而非个人。把它当成普通用户同意来存,正是智能体最终会代表一个从未同意过任何事的人行事的原因。
做成一份可查询的台账,并按动作来定它的保留期。
实现比讨论小得多。一张只追加的表、一个标识、一条关于保留期的规则。
- 一份属于你自己的同意台账,无论令牌握在谁手里。如果是某个连接器平台在跑你的 OAuth,那它的看板是它的记录而不是你的,而且换厂商时它不会跟着走。在每一次授权事件上留下你自己的一行:授权人、主体、提供方、范围、用途版本、客户端 ID、时间戳与结果。
- 把同意 ID 打在每一次动作上。trace 上的一个字段,紧挨着工具调用记录。它把"这是在哪份许可下做的"从一次重建变成一次 join——而这与你为智能体自身选择所保留的决策回执是同一套纪律。
- 同意记录的保留期要与它所授权的动作一样长。授权到期就删掉授权记录,留给你的是一批权限来源无法解释的动作。同意记录是该动作证据的一部分,因此它继承那个动作的保留期,以及任何法务保全。
- 把它展示回给用户。一个列出"连了什么、它实际做过什么、以及一个真能用的断开按钮"的界面,既是好产品,也是发现你那条撤销路径已经坏掉的最便宜的办法。
从两个字段和一次演练开始。在每次授权时写一行同意记录——授权人、主体、范围、用途版本、客户端 ID、时间戳——并把它的 ID 打在智能体据此采取的每一次动作上;这一对就回答了 STEP 2 里四个问题中的三个,代价是一个下午。然后在预发环境用一个真实账号端到端跑一次撤销,看着你的系统如何处理那个排队任务和那些缓存数据:团队正是在这场演练里发现缺口的,而由你来发现,远好过由那个按下按钮的用户来发现。
延伸:审计留痕讲这份记录挂靠的那条主线,数据治理讲智能体读到的东西后来怎么办,问责与角色讲当授权人与运营方不是同一方时由谁来答,面向智能体的欧盟 AI 法案讲与之并肩的那些披露义务。