授权可扩展性——门票与工牌
读者
正在判断 Plexus 1.0 的授权模型能否不经破坏性重写就长出任务级与企业级授权的人——以及日后动手扩展、需要知道哪些接缝是刻意留下的人。已锁定为 ADR-020,见决策记录;仓库内的 SSOT 是 authz-extensibility.md。
一句话版本:1.0 交付完整的工牌和一张原型门票;完整的门票被刻意推迟——但它将来需要的每一个联结键、预留字段和收口点,如今都已存在,并在此处得到保证。
授权的两种单位——门票与工牌
用户以任务为单位思考;网关以 capability 调用为单位执行。审批疲劳,就是让人在调用粒度上做任务粒度的决策——二十张一模一样的卡片会把每一次决策磨成肌肉记忆。Plexus 的答案是把两种凭证性质分开:
- 工牌——你是谁。持久的、按 agent 独立的 PAT:长生命期的身份,可单独撤销,本身永远不构成权限(见安全模型)。
- 门票——你此刻可以做什么。以任务为界的授权同意:任务开始时一次批准,任务进行中生效,任务结束即关闭,并能作为一个完整故事被讲述("这个 agent 在这份授权下整理了这个 vault")。
把授权同意从按次放粗到按任务之所以安全,是因为两项补偿控制的密度不减:每次进出都有日志(逐次调用的审计轨迹),以及撤销是一个动作、立即生效(杀掉门票,所有成员一起死)。授权粒度、审计密度、撤销速度构成一个三角取舍——1.0 把后两者钉死,第一项才得以放宽。
1.0 交付完整的工牌,门票则以原型形态交付:任务 bundle。 一个 bundle 是 N 条共享 bundleId 标签的普通常驻授权——一次批准、一次撤销、附带上下文,不构成新的权限类别。它尚未拥有的是生命周期:没有开票/关票,没有任务边界,没有门票级的叙述。那个对象被推迟了(ADR-020)——下面这些接缝就是它未来的建材。1.0 管理控制台暂不呈现 bundle 创建界面——端点与 grant-service 耦合作为保留的机制交付,bundle 成员就作为一条普通常驻授权显示——待门票生命周期落地后再把这层界面重新引入。
接缝——1.0 锁定了什么
S1——bundle 联结键比 grant 行活得久
撤销时 grant 行会被删除,这是设计使然——正因如此撤销才是终局(refresh 无法再铸出 token)。于是"哪个任务之下授权了什么"的持久记录在审计日志里,而不在 grant 存储里——所以 bundleId 联结键随每一条授权生命周期审计事件一同记录,并且在行被删除之前盖章:
| 生命周期阶段 | 事件携带 |
|---|---|
| agent 请求一个具名 bundle | grant.pending + bundleId + bundleName |
| 人批准该 pend | grant.allow + bundleId + bundleName |
| 成员在范围内的普通再铸请求 | grant.allow + bundleId(绝不悄悄脱离 bundle) |
| 管理员一步建 bundle | grant.allow + bundleId + bundleName |
| 按对 / 按 agent / 按 bundle 撤销 | grant.revoke + bundleId(删除前捕获) |
因此一个任务 bundle 的完整故事——pend → allow → 再铸 → 撤销——在账本里已经查无此行之后,仍能单凭审计日志重放。诚实的边界:审计日志是只追加的 JSONL,默认保留 90 天;需要更长重放窗口的部署调大保留期即可,其余一概不变。
S2——Attribution 预留了企业级的"谁/为何"字段
每条审计事件都可以携带 Attribution——agent(谁做的)、principal?(代表谁——企业级)、grantRef?(哪条授权准许的)、policyRef?(哪条策略规则裁定的——企业级)。全部可选;单网关的 1.0 部署既不写也不读它们。未来的门票/策略层填入这些字段时,事件形状纹丝不动。
S3——信任窗口按加法扩展
TrustWindowKind 是一个闭合联合——once | 1h | 1d | 7d | until-revoked | custom——过期解析只在一个地方收口。将来新增一种窗口(例如 until-task-closed,其过期由事件而非时间戳决定)只是一次加法性的协议小版本号,外加一个函数里的一个分支。已存的授权、线上的客户端、管理 UI 都不会坏:新窗口在被提供之前根本不会出现。
S4——授权器是可插拔的策略接缝
授权决策跑在 Authorizer 接口之后(ADR-007——"换策略不动线上契约")。1.0 内置 auto-approve(受信管理员路径)和 confirm-risky(默认的 pend 策略)。企业级策略引擎——对照门票、角色、组织规则来裁定——作为第三个实现插进来,并经由 policyRef(S2)汇报其裁定,线上契约零改动。
S5——门票永远不能放大权限
无论未来的门票对象长成什么样,它都继承 bundle 的结构性规则:分组不赋予超出其成员的任何权限——每个成员都是一条走过普通审批闸门的普通常驻授权,整组随 connection-key 轮换一起失效,与任何授权无异。门票是一个同意与叙述对象,永远不是与授权、token、暴露并列的第四种权限类别。有效访问始终是已授权 ∧ 已暴露,按 capability 计,在管线里实时强制执行。
门票之内的 execute——已部分回答(ADR-023)
execute → once 的天花板最初被表述为结构性的:一项 execute capability 永远搭不上常驻授权,管理员给出信任窗口也不行(见信任模型)。完整的门票模型与它正面相撞——"帮我整理这个 vault"可能名正言顺地包含一段 claudecode.run:门票之内每次 execute 仍要弹审批,是一张不完整的门票;门票悄悄抬掉天花板,则是一个洞。
ADR-023 部分回答了这个问题:它把 execute → once 从绝对规则放宽为默认值 + 拥有者覆盖——拥有者可以为特定 agent 把特定的 execute capability 开启为常驻授权(默认关闭、连接时双重确认)。这正好以下面第一条约束所要求的严格、逐项自愿开启的方向解决了它;门票对象本身仍未构建。任何未来的门票答案仍必须满足这些约束:
- 常驻 execute 永远不是 agent 能自选或自我提权的,也永远不是默认——它只能作为一次刻意的、经警示的按 agent + 按 capability 的拥有者覆盖存在(ADR-023)。默认下限(execute 逐次挂起审批)不变。
- 任何放宽都必须按 capability 逐项自愿开启(由拥有者声明常驻 execute 对某一个 agent 可行),绝不是门票的通用特权。
- 门票范围内的 execute 不搭超出有界窗口的
until-revoked——除非拥有者在那次逐项开启中明确选择了until-revoked。
未来的门票对象会补上什么(草图,非规范)
从上述接缝直接物化,无需迁移:
- 生命周期——开票(批准动作)→ 生效中 → 关票(用户动作、任务结束或窗口过期)。"关闭"是新增的状态;由 S3 作为一种窗口类型承载。
- 边界——任务的范围约束 + 上下文,bundle 今天已经把它们编成组。
- 叙述——按门票分组的授权页与审计视图:一张任务一张卡,成员列在其中,完整调用史经 S1 联结。
这些都不是 1.0 的工作。本页的保证只有一句:当它成为工作时,那是组装,不是外科手术。