12 治理:审计、最小权限、human-in-the-loop
一个 agent 在凌晨发起 1.25 万元退款时,谁有权批准、批准依据是什么、事后凭什么复原?审计事件要记哪几个字段,哈希链到底能证明什么、不能证明什么?最小权限为什么要同时管工具、数据、时间和环境四层?审批为什么会在大量低风险请求里失效,超时之后应该降级还是放行?策略为什么要和代码一起版本化,合规条款又该落到哪些可执行产物上?
为一个高风险动作画出「请求 → 策略 → 审批 → 执行 → 审计」的决策链;写出可重建动作的审计事件与保留策略;为工具设计带 TTL 的权限租约与 break-glass 流程;把审批点、四眼、超时和审批卡落到状态机;用 OPA/Cedar 的思路组织策略测试与决策日志;把 NIST AI RMF、ISO 42001、EU AI Act 的条款映射成工程证据;跑通一个策略引擎与审批工作流。
第 11 章把安全边界压到「不可信数据不得获得执行权」。边界回答的是「什么不允许发生」;治理回答的是另一半问题:如果某件事必须发生,谁在什么条件下批准它,留下什么证据,事后谁来复原。一个系统可以没有漏洞,却仍然不可治理:权限是永久的管理员,审批是聊天窗口里的一句「可以」,日志只存在应用 stdout 里,出了事只能靠当事人回忆。这种系统在演示里很安全,在事故里很脆弱。
治理常被写成一叠制度文档:安全委员会、AI 伦理准则、责任声明。文档不是没有价值,但它不改变任何一次工具调用的结果。本章只讨论可执行的部分:一个请求如何被策略判定,一个批准如何被身份约束,一个权限如何到期,一条证据如何被验证。制度只有落到这四件事上,才在系统里存在。
这一章在全书路径的末端:第 9 章给了评估,第 10 章给了 trace,第 11 章给了安全边界。治理把三者接起来——评估告诉我们风险在哪,trace 提供事实,安全边界提供拒绝能力,治理决定「例外由谁授权、证据留多久」。第 13、14 章的两个项目会把这里的策略引擎、审批流与审计日志接进真实执行器。
12.1 治理对象:谁批准、谁负责、留什么证
先看一次具体动作。退款 agent 在凌晨 02:17 发起一笔 1,250,000 分的退款,收款方是三天前刚更换的账户,订单来自生产租户 acme。系统有四个问题必须能回答,而且答案不能依赖任何人的记忆:
- 谁批准:这笔退款经过了谁?是自动规则、单个值班人,还是两个不同的人?
- 谁负责:如果这笔退款是错的,责任落在模型提供方、agent 产品方、审批人,还是平台运维?
- 留什么证:事后能否拿出请求参数、策略版本、审批记录、执行回执,证明「当时确实批准过」?
- 多久:这些证据保留多久?权限多久回收?审批多久超时?
四个问题对应治理的四个对象。它们不是四份文档,而是四组必须能相互校验的系统状态。
| 治理对象 | 要回答的问题 | 没有它的症状 | 最小产物 |
|---|---|---|---|
| 决策权 | 谁在什么条件下可以批准哪个动作? | 谁都能点「同意」;或没人敢点 | 策略表 + 审批角色 + 授权边界 |
| 责任 | 动作出错时,哪个角色对哪个决定负责? | 事故复盘变成「模型抽风」 | 责任矩阵 + 值班表 + 升级路径 |
| 证据 | 事后凭什么重建这次动作? | 只有最终答案,没有中间记录 | 审计事件 + 执行回执 + 证据链 |
| 时效 | 批准、权限、证据各自活多久? | 临时权限变永久;日志过期才发现要查 | TTL + 保留期 + 复核期限 |
这张表说明什么:四者缺一不可。只有决策权没有证据,批准无法被证明;只有证据没有时效,日志会无限膨胀,临时权限会永久留存;只有责任没有决策权,复盘只能找人背锅,改不了系统。
一次 agent 动作的治理配置可写成
其中:
- 是决策权映射:对主体 、动作 、资源 与上下文 ,给出允许、拒绝或需要谁批准;
- 是责任分配:把 中的每个决定绑定到一个可联系的角色与升级路径;
- 是证据模式:定义一次动作要记录哪些事件、事件之间的关联键、完整性与保留要求;
- 是时效约束:审批截止时间、权限租约到期时间、证据保留期与事后复核期限。
称 是可执行的,当且仅当 能被策略引擎求值、 能被写入不可变或可验证的存储、 能被执行器检查,且 中的每个角色都有明确的接管人。四元组中任何一项只存在于文档里,都不构成可执行治理。
这个定义故意把「责任」放在「决策权」旁边。很多系统只实现了一半:策略引擎能拒绝,审批流能放行,但没有人对「为什么这条规则是这样」负责。规则漂移、阈值被临时调高、审批人长期离线,这些问题不会让引擎报错,却会让治理慢慢失效。责任分配的作用,是给每个自动决定配一个能改它、也能为它解释的人。
正常路径谁都能写进代码:读文件、查订单、发通知。治理真正要设计的是例外:这笔退款为什么可以绕过限额,这个服务为什么可以临时拿到生产库权限,这次删除为什么没有走常规审批。例外如果没有固定入口,就会走聊天窗口;走聊天窗口的例外没有证据,也没有时限。治理的质量,几乎等于「例外路径的质量」。
请求 策略 审批
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ request_id │ │ policy_version │ │ approval_id │
│ actor / tenant │─────▶│ rule_id │─────▶│ approver(s) │
│ action / resource │ │ allow / approve / │ │ four_eyes? │
│ amount / env │ │ deny + obligations│ │ deadline │
└──────────────────┘ └──────────────────┘ └──────────────────┘
│ │ │
│ │ require_approval │ approved
│ ▼ ▼
│ ┌──────────────────────────────────────┐
│ │ 执行:scoped credential + 幂等键 │
│ │ execution_id / receipt / result │
│ └──────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ 审计:request → policy_decision → approval → execution → review │
│ 每条记录带 prev_hash/hash;链头定期锚定到 WORM 或外部时间戳 │
└─────────────────────────────────────────────────────────────────────┘
│
▼
证据链:可重建 / 可验证 / 可保留
读图:一条高风险动作至少产生五条独立记录——请求、策略决策、审批、执行回执、事后复核。它们由不同组件写入,通过 request_id / approval_id / execution_id 关联。五条记录齐全时,审计员可以在不知道当事人记忆的情况下回答「谁在什么时候、依据哪版策略、批准了什么参数、执行结果是什么」。缺任何一条,结论就会降级:缺审批记录,无法区分「自动放行」与「人工批准」;缺策略版本,无法复现当时的判定;缺执行回执,无法证明批准的动作真的执行了,或执行的是不是同一个动作。
12.1.1 决策权:不是「谁能用」,而是「谁能批」
决策权经常被写成角色权限:admin 可以退款,support 只能查询。这不够。退款动作至少有三个不同的权力:发起、批准、执行。把它们混在一个角色里,等于让同一个人写申请、批申请、再执行,四眼原则从一开始就不存在。
一个可执行的决策权表至少要有五列:
| 动作类 | 发起者 | 批准者 | 执行凭证 | 约束 |
|---|---|---|---|---|
| 只读查询 | 任意已认证 agent | 无需批准 | 短期只读 token | 数据范围按租户过滤 |
| 小额退款 | 客服 agent | 自动规则(限额内) | 支付工具 scope | 单笔与日累计上限 |
| 大额退款 | 客服 agent | 财务 + 风控(两人) | 一次性支付凭证 | 收款账户变更需额外复核 |
| 生产删除 | SRE agent | SRE 负责人 + 安全 | break-glass 或变更单 | 必须先有备份与回滚方案 |
| 跨租户导出 | 数据 agent | 数据所有者 + 安全 | 一次性导出凭证 | 含 PII 时必须脱敏与水印 |
这张表说明什么:批准者与发起者必须是不同主体;执行凭证必须是这次动作专用的,不能是发起者手里长期持有的密钥。很多越权事故不是因为策略写错,而是因为批准之后,执行器仍然用 agent 的长期凭证去调用下游——批准只改了工单状态,没有改凭证。
12.1.2 责任:模型是组件,不是责任主体
出事后最常见的归因是「模型自己决定这么做」。这句话在工程上不成立。模型输出的是 token;把它变成工具调用、给它凭证、让它访问生产数据、允许它在没有人复核的情况下执行的,都是宿主系统。模型没有法律人格,不理解组织政策,也不能持有或归还权限。能绕过审批的从来不是模型,而是宿主给模型留的那条路径。
模型行为是系统行为的一个输入,不是并列的责任主体。责任要按「谁能改变这个决定」来分,而不是按「谁离故障最近」来分:
| 行为/决定 | 直接产生者 | 负责角色 | 常见错误归因 |
|---|---|---|---|
| 生成退款意图与参数 | 模型 | agent 产品方:提示词、工具契约、参数校验 | 「模型幻觉」 |
| 选择可用的工具与 scope | 宿主/工具注册表 | 平台方:工具目录、scope 审批 | 「模型调用了不该调的工具」 |
| 批准高风险动作 | 审批人 | 审批人 + 其所属业务线 | 「系统自动批的」 |
| 签发执行凭证 | 策略引擎/凭证服务 | 平台安全:策略版本、凭证 TTL | 「模型拿到了 token」 |
| 记录审计事件 | 执行器/日志管道 | 平台运维:完整性、保留、访问控制 | 「日志没开」 |
| 事后复核 | 复核人 | 风控/审计:抽样、结论、整改 | 「没人想到要看」 |
这张表说明什么:模型只占第一行。后面五行的每一个「是」,都是组织里某个人可以改的。把责任推给模型,实际效果是让真正能修复的环节免于被检查。模型提供方仍有责任:能力声明、已知局限、版本变更与安全公告;但授权、执行与证据属于宿主和组织,不能由提供方代为决定。
- 「治理就是写制度」——制度只有落到策略、凭证、日志、复核四个机制上才会改变行为。没有求值器的制度,只是一份愿望清单。
- 「审批就是多一个人点同意」——审批的有效性取决于:审批人是否独立、是否看得懂证据、是否有时间看、拒绝是否真的能阻止执行。缺一条,审批就是橡皮章。
- 「审计就是多打日志」——日志回答「发生了什么」,审计要回答「凭什么相信这条记录」。没有 schema、关联键、完整性与保留策略的日志,事故时不可用。
- 「最小权限就是给低权限账号」——低权限账号仍然可能持有过宽的 scope、永久凭证或跨租户数据访问。最小权限是四层约束的组合,不是账号等级。
- 「合规就是拿证/过审」——证书证明某个时点的管理体系存在,不证明今天的这次高风险动作被正确批准。合规检查的是证据,不是口号。
12.1.3 时效:治理有四个时钟
治理失败经常不是「没有规则」,而是「规则过期了」。四个时钟要分别设计,不能用一个「定期 review」糊过去:
| 时钟 | 约束什么 | 典型值(示例,按风险定) | 超时行为 |
|---|---|---|---|
| 审批时钟 | 一次待批请求活多久 | 15 分钟–24 小时 | fail-closed,拒绝并通知 |
| 权限时钟 | 一次授权活多久 | 5 分钟–8 小时 | 自动过期,重新申请 |
| 证据时钟 | 一类事件保留多久 | 90 天–7 年 | 到期归档/删除,保留法律冻结 |
| 复核时钟 | break-glass 后多久复核 | 24 小时–5 个工作日 | 升级到安全负责人 |
这张表说明什么:四个时钟的默认方向不同。审批和权限要短,短到能限制暴露窗口;证据要长,长到能覆盖调查与追责周期;复核要紧,紧到当事人还记得上下文。把它们都设成「一年」或都设成「一天」,都会在某一边出问题。
12.1.4 最小可运行治理清单
治理不需要一次建完。一个团队可以先落地八件事,覆盖「决策、证据、时效」的最小闭环:
| # | 产物 | 最小内容 | 负责人 | 没有它的直接后果 |
|---|---|---|---|---|
| 1 | 策略包 | 一份带版本号的规则文件 + 默认拒绝 | 平台/安全 | 放行靠口头,无法复现 |
| 2 | 审批矩阵 | 动作 × 风险 × 审批人/人数 | 业务负责人 | 谁都能批,或没人敢批 |
| 3 | 审计 schema | 六问字段 + 关联 id + 完整性 | 平台运维 | 事后无法重建 |
| 4 | 保留策略 | 按数据类分层 + 删除/冻结规则 | 安全/法务 | 日志要么过期,要么成 PII 仓库 |
| 5 | 租约签发 | TTL、scope、数据谓词、撤销接口 | 平台安全 | 临时权限永久化 |
| 6 | break-glass | 事件号、通知、24h 复核 | 值班负责人 | 例外走聊天窗口 |
| 7 | 漂移查询 | 授予 vs 使用,日更 | IAM/平台 | 权限慢慢腐烂 |
| 8 | 复核节奏 | 抽样、指标、整改跟踪 | 风控/审计 | 审批退化成盖章 |
这张表说明什么:八件事里,第 1、3、5 件是系统能力,其余五件是流程与责任。很多团队先买日志平台,却没有人定义 schema 与保留策略,结果只是把不可用的日志存得更久。顺序应该反过来:先定事件与决策,再选存储。
12.1.5 先做哪三件
如果只能先做三件,按这个顺序:
- 给高风险动作加审批与审计:选一个不可逆、金额大的动作,写清谁批、批什么、留什么。它立刻降低最坏损失。
- 给临时权限加 TTL:所有非长期授权必须有到期时间;没有 TTL 的授权视为缺陷。它降低暴露窗口。
- 建立每周一次的漂移对账:授予与使用的差集,先手工跑,再自动化。它让前两件不会慢慢失效。
三件都做完,再考虑策略语言、WORM、合规映射。反过来,先上策略引擎却没有审批人与证据,只会把「没有控制」变成「有代码但没有控制」。
12.2 审计:事件、不可篡改、保留期与证据链
审计的第一性原理不是「多记」,而是事后能否在不知道当事人记忆的情况下重建一次动作。这要求每条记录回答六个问题:谁(who)、做了什么(what)、何时(when)、为什么(why)、依据哪版策略(policy_version)、结论是什么(decision),并留下能指向原始证据的引用(evidence)。少一个问题,重建就会断。
一条审计事件是一个八元组
其中:
- 是主体标识、角色与认证方式,不是自由文本姓名;
- 是动作、资源标识与参数摘要(含哈希,敏感字段脱敏);
- 是墙钟时间与单调时钟,二者都要有;
- 是请求理由、工单/事件号与策略命中的规则;
- 是策略版本与策略包哈希;
- 是决策(allow / require_approval / deny)与附带的义务(obligations);
- 是证据引用:trace_id、审批 id、执行回执 id、diff 或快照地址;
- 是完整性字段:前一条的哈希与本条哈希。
一条事件只描述一个决定或一次状态转移;不要把「提交、批准、执行」塞进同一条记录。拆分后才能定位是哪一步缺失。
字段不能凭感觉定。下面这张表给出一组最小字段与缺失后的直接后果:
| 字段 | 示例 | 必填 | 缺失后果 |
|---|---|---|---|
event_id / seq | evt-000042 / 42 | 是 | 无法稳定引用;重放时顺序不确定 |
recorded_at | 2026-10-19T02:17:03Z | 是 | 无法建立时间线;跨系统因果断裂 |
monotonic_ns | 884219003311 | 是 | 墙钟回拨时无法判断先后 |
event_type | policy_decision | 是 | 无法区分决策、批准与执行 |
actor / roles | support-3 / support | 是 | 无法判断是否四眼、是否越权 |
action / resource | refund / invoice:INV-2091 | 是 | 不知道发生了什么 |
params_digest | sha256:9f2c... | 是 | 无法证明执行的是批准的那组参数 |
policy_version | 2026.10.1 | 是 | 无法复现当时的判定 |
decision / rule_id | require_approval / require-approval-high-value-refund | 是 | 不知道为什么被拦或放行 |
reason / ticket_id | 大额退款复核 / CHG-1042 | 是 | 不知道为什么发起、对应哪张工单 |
evidence_ref | trace:8f3a、approval:apr-003 | 是 | 无法跳到原始证据 |
prev_hash / hash | ... / ... | 是 | 无法发现篡改或删除 |
这张表说明什么:policy_version 与 params_digest 是最容易被忽略、却最影响复现的两列。没有版本,你只能知道「当时被批准了」,不知道「当时的规则是否允许」;没有参数摘要,你无法证明执行的回款账户与批准时看到的是同一个。
一条真实形态的事件如下(来自本章实验的 JSONL,字段略有删节):
{
"seq": 2,
"event_id": "evt-000002",
"recorded_at": "2026-10-19T09:00:01Z",
"event_type": "policy_decision",
"actor": "support-3",
"action": "refund",
"resource": "invoice:INV-2091",
"resource_tenant": "acme",
"policy_version": "2026.10.1",
"decision": "require_approval",
"obligations": ["four_eyes", "finance_ticket"],
"evidence": {
"request_id": "req-002",
"reason": "退款金额超过 500000 分,需要人工审批",
"matched_rule": "require-approval-high-value-refund",
"context": {"amount_cents": 1250000, "environment": "prod", "reversible": false}
},
"prev_hash": "6a1f...",
"hash": "b73c..."
}
这条事件能回答「谁、做了什么、为什么需要审批、按哪版策略」,但它不能单独证明「谁批准了」。批准是另一条事件,带自己的 actor、approval_id 与 wait_s。把两者混在一条记录里,四眼原则就无法被机械校验。
12.2.1 append-only、WORM 与哈希链:三层不是一回事
「不可篡改」经常被当成一个开关:买个 WORM 桶,或者加个哈希链,就宣称审计不可篡改。实际上这是三层不同的保证,成本与威胁模型都不同:
| 层 | 机制 | 能防什么 | 防不了什么 | 成本 |
|---|---|---|---|---|
| 追加写 | 应用只 append;文件/表禁止 update/delete | 误删、覆盖、应用 bug | 有存储写权限的人重写 | 低 |
| WORM | 对象锁、合规保留、删除需等保留期 | 管理员误删、内部人删证据 | 保留期外的删除;写错内容 | 中 |
| 哈希链 + 外部锚定 | 链式哈希;链头定期写入独立存储/时间戳 | 静默改单条、删中间记录 | 重算整条链并改写链头 | 中高 |
这张表说明什么:三层要叠加。只有追加写,内部人可以直接改文件;只有 WORM,写进去的错误内容仍然错误;只有哈希链,能重写整条链的人可以重算所有哈希。外部锚定是关键:把链头哈希定期写到另一个信任域(独立账号、时间戳服务、纸质归档),攻击者要同时攻破两个域才能自洽。
设事件序列为 ,链定义为
其中 是 的规范编码, 是抗第二原像的哈希函数。若验证方持有可信的 (例如外部锚定的链头),则任何对某个 ()的修改,只要不同时改变 ,都会以压倒性概率被验证发现。
设被修改的最早事件下标为 。由于 改变,且 未变, 以压倒性概率不等于原 (否则找到 的第二原像)。对 归纳:若 已不同,则 也不同。故重算得到的 与可信链头不同,验证失败。若攻击者能同时改写可信链头,则本定理的前提不成立;这正是需要外部锚定的原因。∎
∎定理的边界比结论更重要。它假设:哈希函数抗第二原像;验证方拿到的链头确实可信;链没有在尾部被截断。截断是哈希链的常见盲点:攻击者删掉最后几条记录,剩下的链仍然自洽。对策是记录「预期条数/最后序号」并一起锚定,或者用外部系统记录每个时间窗口的链头。另一个盲点是密钥与写权限:如果签名密钥和执行器在同一台机器上,攻击者可以签一条新的历史。
哈希链证明「记录之间一致」,不证明「记录完整」。WORM 证明「这段时间内删不掉」,不证明「写进去的是真话」。审计的可信度来自三件事的组合:写入端可信(执行器不能伪造结果)、存储端不可改(WORM + 锚定)、读取端可验证(独立验证工具)。三者缺一,审计就只是「一份比较难改的日志」。
12.2.2 保留期与 PII 最小化:记多久与记多细
审计日志本身也是数据,也会包含个人数据。保留期太长,违反最小化;太短,事故发生时已经过期。可行的做法是按证据价值分层,而不是按「全部保留七年」一刀切:
| 数据类 | 内容 | 保留期(示例) | 到期动作 |
|---|---|---|---|
| 决策元数据 | 谁、何时、动作、决策、策略版本 | 1–7 年,按适用要求 | 归档;保留聚合统计 |
| 审批记录 | 审批人、时间、意见、证据快照 | 与决策同级 | 归档;不可单独删除 |
| 执行回执 | 下游 id、幂等键、结果码 | 1–3 年 | 归档;保留引用 |
| 调试日志 | 完整请求/响应、堆栈 | 7–90 天 | 删除或降采样 |
| 敏感原文 | 证件号、账户、自由文本 | 默认不写 | 写哈希/令牌化;原文另存受控库 |
这张表说明什么:审计事件里不要放自由文本和完整参数。需要原文时,存引用而不是副本:evidence_ref 指向一个访问受控、保留期更短的 trace 存储。这样审计日志可以长期保留而不成为 PII 仓库;调查时再按权限取原文,并记录「谁在何时看了哪条证据」。
PII 最小化的三个具体动作:
- 字段白名单:审计写入器只接受预定义字段,拒绝任意
dict。实验里的sanitize_context()只保留金额、环境、是否可逆等策略字段,就是这一条的最小实现。 - 令牌化:主体 id 用内部 pseudonym,资源 id 用不可逆哈希或代理 id;需要映射时走独立权限。
- 访问审计:读取审计日志本身也要记事件。否则「谁看过这笔退款的证据」永远无法回答。
12.2.3 不可抵赖:哈希链不证明「是他签的」
审计的最后一个要求是不可抵赖(non-repudiation):当事人事后不能否认自己做过这个决定。哈希链只证明「这条记录没有被改」,不证明「这条记录确实由声称的人写下」。两者之间隔着身份、签名与意图。
| 条件 | 问题 | 工程做法 | 伪实现 |
|---|---|---|---|
| 身份强度 | 怎么证明是这个人? | 强认证、硬件密钥、短时会话 | 共享账号、代签、截图 |
| 签名 | 记录能否被伪造? | 审批人私钥签名,或由独立服务签收 | 应用自己写 actor 字段 |
| 意图 | 他批准的是这组参数吗? | 对冻结的参数摘要签名 | 批准一段可后续修改的文本 |
| 时间 | 他何时批准的? | 可信时间戳 + 单调时钟 | 客户端本地时间 |
这张表说明什么:actor 字段只是声称,不是证据。没有强认证与签名时,最坏情况是「有人用共享账号点了同意,事后所有人都否认」。高风险动作的审批记录应该由审批服务签名,签名覆盖 approval_id、参数摘要、审批人、时间与决定;验证方用公钥独立验证,而不是相信数据库里的 approved_by 列。
不可抵赖不是所有动作都需要的成本。低风险自动批准只需可追溯;高风险、对外、涉钱的审批才值得上签名与强认证。判断标准与 12.4 的审批点一致:越不可逆、越跨边界,越需要「无法否认」而不是「大概记得」。
12.2.4 证据链:重建一次高风险动作
某退款在事后被投诉「未经本人同意」。审计员拿到五条记录:
req-8842 request support-3 refund invoice:INV-2091 amount=1250000 env=prod
dec-8842 policy policy-engine require_approval rule=high-value-refund v=2026.10.1
apr-8842 approval finance-2 -> finance-4 two distinct approvers wait=150s
exe-8842 execution payments.refund idempotency=refund-INV-2091-v1 receipt=pay_7f31
rev-8842 review risk-1 sampled=true conclusion=valid evidence=trace:8f3a
重建过程:请求记录确定「谁发起、对哪个订单、多少钱」;策略记录确定「为什么需要审批、按哪版规则」;审批记录确定「两个人分别是谁、间隔多久」;执行记录用幂等键与回执确定「实际打款的是哪一笔」;复核记录确定「事后有没有人看过」。
如果缺 apr-8842:无法证明这是人工批准而不是自动放行,投诉无法反驳。
如果缺 dec-8842:无法证明 1.25 万在当时确实超过阈值;策略可能已经改过。
如果缺 exe-8842:无法证明批准的动作与实际执行的是同一笔;参数可能在批准后被替换。
如果只有应用日志:字段不全、可能被轮转删除、没有哈希链,任何一条都无法独立验证。
结论:证据链不是「日志很多」,而是「五条记录、至少三个独立系统、一个可验证的链头」。缺一条,重建就退化成推测。
- 先写问题,不写字段:列出事后必须回答的五个问题(谁发起、谁批准、依据什么、执行了什么、谁复核)。
- 给每个问题配一个标识:
request_id、approval_id、execution_id、policy_version、trace_id;确保跨系统可关联。 - 定义参数摘要:对会影响结果的参数做规范化 JSON 后哈希;敏感字段先脱敏再入日志。
- 定义完整性:
prev_hash/hash、链头锚定周期、预期条数;把验证脚本放进 CI。 - 定义保留与访问:按数据类分表;原文放受控存储;读取审计日志也记事件。
- 做一次重建演练:随机抽一条历史动作,只给审计记录,要求在 10 分钟内回答五个问题;失败就补字段。
Q1 为什么不把完整 prompt 和工具返回都写进审计事件?
答案
因为审计事件要长期保留,而 prompt/返回可能含 PII、密钥、第三方内容和注入文本。正确做法是分层:审计事件存 trace_id、参数摘要与决策理由;完整原文存在访问受控、保留期更短的 trace 存储里;调查时按权限取用,并记录读取行为。这样既保留可重建性,又不把审计日志变成 PII 仓库。
Q2 有了哈希链,是否还需要 WORM?
答案
需要。哈希链只保证「记录之间一致」,不能阻止有写权限的人重写整条链并重算哈希。WORM 提供保留期内的删除保护;外部锚定提供链头的独立可信副本。三者解决不同威胁:哈希链发现单点修改,WORM 阻止删除,锚定发现整链重写。少了任何一个,攻击面都会扩大。
12.3 最小权限:工具级、数据级、时间级与 JIT
第 4 章讲的是内核机制:uid、capability、seccomp、landlock。它们回答「进程能做什么」。这一节讲的是授权生命周期:一个 agent 在什么条件下、对哪部分数据、在多长时间内、在哪个环境里,能调用哪个工具。内核边界再严,如果执行器给 agent 一个永久的生产管理员 token,最小权限仍然是空话。
最小权限要同时收四层。只收一层,另外三层会把它抵消:
| 层级 | 约束什么 | 具体做法 | 失效模式 |
|---|---|---|---|
| 工具级 | 能调用哪些动作 | 一个工具一个 scope;禁止 payments:* 这种通配 | 工具复用导致权限连坐 |
| 数据级 | 能碰哪些行/字段/租户 | 行级谓词、字段掩码、租户隔离 | 同一工具读到全部租户 |
| 时间级 | 权限活多久 | JIT 租约、短时凭证、到期自动回收 | 临时权限永不回收 |
| 环境级 | 在哪个环境生效 | prod/staging 凭证分离、数据脱敏 | 开发凭证能打生产 |
这张表说明什么:四层是乘法关系,不是加法。工具级收了,但数据级没过滤,agent 仍可读全量;数据级过滤了,但凭证永久有效,攻击者可以慢慢利用;环境级分离了,但 staging 用生产数据副本,风险又回来了。
12.3.1 权限租约:把「授予」变成「有期限的借用」
角色(role)适合描述长期职责,不适合描述一次动作。agent 的一次退款、一次导出、一次排障,需要的是参数化、短时、可审计的授权。这就是权限租约(lease)。
一条权限租约是一个八元组
其中:
- 是主体(agent 实例或工作负载身份);
- 是工具/动作 scope;
- 是数据谓词(租户、行范围、字段掩码);
- 是环境(prod/staging);
- 是生效窗口, 是 TTL;
- 是签发者与策略版本;
- 是义务(最大金额、幂等键、必须四眼、必须水印等)。
一次调用被授权,当且仅当当前时间在窗口内、数据谓词为真、环境匹配、租约未撤销,且调用参数满足全部义务。租约过期后,执行器必须拒绝,不得用缓存或旧凭证续期。
租约与角色的区别,用一张表最清楚:
| 维度 | 角色 | 租约 |
|---|---|---|
| 生命周期 | 月/年 | 分钟/小时 |
| 参数 | 粗粒度(finance) | 细粒度(租户、金额上限、单据) |
| 审批 | 入职/转岗时 | 每次高风险动作 |
| 回收 | 依赖流程 | 到期自动 |
| 证据 | 角色分配表 | 租约 id + 每次使用记录 |
这张表说明什么:角色不是没用,它是租约的上限。租约不能超过角色允许的 scope;角色决定「你能申请什么」,租约决定「这次你能做什么」。两层都要有,缺一层就会出现「角色很大但没人用」或「租约很细但可以无限续」。
一个租约的 JSON 形态如下:
{
"lease_id": "lease-7f3a91",
"subject": "agent-refund-3",
"tool": "payments.refund",
"data": {"tenant": "acme", "invoice_prefix": "INV-2"},
"environment": "prod",
"not_before": 1792376100,
"expires_at": 1792377000,
"issued_by": "policy-engine@2026.10.1",
"obligations": {"max_amount_cents": 500000, "idempotency_key_required": true}
}
租约里的时间统一用 Unix 秒,避免时区与字符串比较。执行器侧的检查必须是 fail-closed 的。下面这段是示意,不是本章实验的一部分;重点是检查顺序与默认值:
def lease_allows(lease: dict, request: dict, now: int, revoked: set[str]) -> bool:
if lease["lease_id"] in revoked:
return False
if not (lease["not_before"] <= now < lease["expires_at"]):
return False
if lease["tool"] != request["tool"] or lease["environment"] != request["environment"]:
return False
if lease["data"]["tenant"] != request["tenant"]:
return False
if request["amount_cents"] > int(lease["obligations"].get("max_amount_cents", 0)):
return False
return True
四个细节:时间用同一时钟源;revoked 集合要能被策略引擎写入;义务检查失败必须拒绝而不是降级;每次检查都要把 lease_id 写进审计事件。少了最后一条,事后无法证明「这次调用用的是哪条租约」。
12.3.2 JIT:权限在需要时出现,在不需要时消失
JIT(just-in-time)不是「申请更快」,而是「默认没有,用时才发,到期就收」。一个可执行的 JIT 流程有六步:
- 请求:agent 提交动作、资源、理由、预计时长;不接受「给我 admin」这种无参数请求。
- 求值:策略引擎按主体、动作、资源、上下文给出 allow / require_approval / deny。
- 批准:高风险动作走 12.4 的审批;低风险可按规则自动。
- 签发:凭证服务签发短时租约;TTL 由风险等级决定,不由申请人决定。
- 使用:执行器每次调用检查租约与义务,并写使用记录。
- 回收:到期自动失效;提前完成则主动撤销;异常使用触发撤销与复核。
JIT 的关键指标不是「签发速度」,而是暴露窗口:
窗口越短,凭证泄露后可利用的时间越短。但窗口不能短到正常任务无法完成,否则会诱导用户申请更长的 TTL,反而扩大风险。合理的做法是按任务类型设定上限,并在任务完成时主动撤销,而不是让租约自然到期。
假设:(1) 执行器在每次调用前检查租约窗口与撤销状态;(2) 租约不能在没有新策略决策的情况下延长;(3) 时钟单调且执行器与签发者使用同一时间基准。则对任意租约 ,在 且没有新租约的情况下,任何依赖 的调用都会被拒绝。
由 (1),调用被允许蕴含当前时间 且 未撤销。由 (3), 时窗口条件为假,检查返回拒绝。由 (2), 的 不会被执行器之外的路径改写;任何延长都必须产生新的策略决策与新租约。因此旧租约在 之后不再授权任何调用。若执行器缓存了授权结果或跳过检查,则前提 (1) 不成立,结论不成立——这正是「租约存在但执行器不查」的故障模式。∎
∎这个定理看起来近乎同义反复,但它的三个前提正是事故现场最常见的缺口:执行器缓存了「已授权」的布尔值;续期接口只要一个 refresh token;容器时钟漂移导致过期判断晚了几分钟。定理的价值不在证明,而在把这三个前提变成检查项。
12.3.3 break-glass:把例外变成可复核的流程
break-glass 是治理里最危险、也最容易被滥用的机制。它的目的不是「绕过审批」,而是在事故中把审批挪到事后,同时提高证据等级。一个可执行的 break-glass 至少要满足六条:
| 要求 | 机制 | 反例 |
|---|---|---|
| 有事件号 | 必须关联 incident_id,无号拒绝 | 「先开了再说,事后补单」 |
| 范围最小 | 只开当前故障需要的 scope 与资源 | 直接给 prod:admin |
| TTL 短 | 15–30 分钟,到期自动失效 | 开到「问题解决为止」 |
| 强制通知 | 签发即通知安全/值班/负责人 | 只在本地日志留一行 |
| 事后复核 | 24 小时内由独立角色复核并结论 | 事故结束就没人再看 |
| 不可静默 | 使用时打标,指标与告警可见 | 与普通调用无区别 |
这张表说明什么:break-glass 的成本要高于正常审批,而不是低于。如果 break-glass 比走审批更快、更省事,它就会成为默认路径;那时它不再是例外,而是绕过的后门。
12.3.4 权限漂移:实际使用与授予的差集
权限不会一次性配错,它会慢慢漂移:临时授权忘了收、转岗后角色没换、工具升级后 scope 变宽、共享账号被多人使用。检测漂移不需要等审计,只要定期对比两个集合:
- :当前授予的权限集合(按主体、scope、资源);
- :窗口期内实际使用的权限集合。
两个差集分别对应两类问题:
前者是回收候选:授予了但从未使用,说明权限过宽或主体已不需要。后者是事故候选:使用了但从未授予,说明越权、旁路、共享凭证或日志缺口。两类都要查,但优先级不同:未授予使用优先,因为它可能正在发生。
| 漂移类型 | 证据 | 根因 | 处置 |
|---|---|---|---|
| 过度授予 | unused 持续 30 天 | 按角色批量授权,未按任务收紧 | 回收;改为 JIT |
| 孤儿授权 | 主体已离职/下线,G 仍有记录 | 生命周期事件没触发回收 | 自动禁用;责任人确认 |
| 影子访问 | U \setminus G 非空 | 共享密钥、旁路脚本、日志缺失 | 立即调查;轮换凭证 |
| scope 蔓延 | 同一工具 scope 逐月变宽 | 为个别需求改全局策略 | 拆分工具;按租户/动作细分 |
这张表说明什么:漂移检测的输出不是一张报表,而是工单。每条 unused 要有回收期限与责任人;每条 ungranted 要有调查结论。没有处置的检测,只是让报表变长。
实验里的 detect_permission_drift() 就是这两个差集的最小实现:
python3 - <<'PY'
import sys
sys.path.insert(0, "experiments/ch12")
from policy_engine import detect_permission_drift
report = detect_permission_drift(
{"agent-1": {"repo:read", "prod:deploy"}},
{"agent-1": {"repo:read"}, "agent-2": {"secret:read"}},
)
print(report.to_dict())
PY
{'clean': False, 'unused': {'agent-1': ['prod:deploy']}, 'ungranted': {'agent-2': ['secret:read']}}
真实系统里,G 来自 IAM/租约库,U 来自执行器的使用日志;两边都要有稳定的主体 id 与 scope 命名,否则差集会充满假阳性。漂移检测的第一项工程任务不是写查询,而是统一命名。
12.3.5 权限评审:从定期 review 到持续对账
季度权限 review 的典型失败是「看表格点确认」:表格列的是角色与授予,不列实际使用;评审人没有上下文,只能全部保留。把 review 改成持续对账,能同时降低风险和评审成本:
| 节奏 | 动作 | 输入 | 输出 |
|---|---|---|---|
| 每日 | 差集计算 | 授予快照 + 使用日志 | unused / ungranted 工单 |
| 每周 | 高风险租约复核 | 本周 break-glass、超时、异常放行 | 复核结论与整改项 |
| 每月 | 孤儿主体清理 | 离职/下线事件 + 授予 | 自动禁用清单 |
| 每季 | 角色与策略重审 | 命中率、漂移趋势、事故 | 策略变更单 |
| 事件驱动 | 异常使用 | 新 IP、非工作时间、跨租户 | 立即调查或撤销 |
这张表说明什么:review 的对象要从「权限清单」变成「差异与事件」。前者只会得到「都还需要」;后者会得到具体的回收、调查与策略变更。评审人不需要看几百行授予,只需要看几十条有差异的记录。
12.4 Human-in-the-loop:审批点、四眼原则与超时
Human-in-the-loop 最常见的失败不是「没有人」,而是「人出现在错误的位置」。如果审批发生在动作已经执行之后,人只能事后追认;如果审批请求里只有一句「是否同意」,人只能凭感觉点;如果每个低风险动作都要审批,人会在第 200 次点击时失去注意力。审批点的选择,比审批界面的设计重要一个数量级。
12.4.1 在哪里放审批点
审批点应该放在不可逆、影响面大、信息不对称的地方。四个判据最常用:动作是否可逆、金额/影响是否超过阈值、是否跨租户或跨边界、是否首次执行。任一为真,就值得一个人看一眼;多个为真,就需要两个人。
| 场景 | 典型例子 | 默认策略 | 审批强度 |
|---|---|---|---|
| 不可逆 | 删除、打款、发送对外邮件、物理动作 | 必须审批 | 1–2 人,含业务负责人 |
| 高金额/高影响 | 退款、折扣、资源扩容、批量导出 | 超过阈值审批 | 财务/数据所有者 |
| 跨租户/跨边界 | 跨 tenant 查询、跨 region 复制 | 默认拒绝 | 数据所有者 + 安全 |
| 首次执行 | 新工具、新收款方、新部署目标 | 首次审批,之后可自动 | 工具 owner + 业务 |
| 低风险高频 | 只读查询、状态轮询、格式化 | 自动 | 抽样审计,不逐次审批 |
| 已有回滚 | 可幂等重试的 staging 变更 | 自动 + 观察 | 异常时人工 |
这张表说明什么:审批强度要和可逆性成反比。可逆动作可以自动,因为错了能回滚;不可逆动作必须审批,因为错了只能赔偿。把审批均匀撒在所有动作上,等于把最需要注意力的人耗在不需要看的事情上。
12.4.2 审批请求:给证据,不给一大段日志
审批人不是调试者。他在 30 秒内要判断的是「这个动作是否应该发生」,而不是「这个 agent 的内部状态是什么」。因此审批请求必须是一张证据卡,而不是日志导出。
一条可执行的审批请求是一个九元组
其中:
- 是待执行动作与资源标识;
- 是动作将改变的具体差异(金额、目标账户、删除范围、部署版本);
- 是影响面(blast radius):涉及多少记录、哪些租户、是否对外;
- 是可逆性与回滚方案;无法回滚时明确写「不可逆」;
- 是环境(prod/staging)与执行窗口;
- 是策略版本、命中的规则与风险等级;
- 是证据链接:trace、diff、测试结果、历史同类动作;
- 是截止时间与超时行为;
- 是请求者、替代方案与「不做会怎样」。
审批请求必须绑定一个冻结的参数摘要。批准的是摘要,不是一段可被后续修改的自然语言。
九项里最容易被删掉的是 与 。只写「申请退款 1.25 万」,审批人无法判断收款账户是否变更;只写「回滚服务」,审批人无法判断回滚到哪个版本、是否会丢数据。一个实用的审批卡长这样:
动作 refund invoice:INV-2091 (acme / prod)
金额 1,250,000 分(¥12,500.00)
差异 收款账户 2026-10-16 由 A 改为 B;B 为新账户(3 天)
影响 单笔;该客户本月第 1 次退款;不可逆
回滚 不可回滚;可发起追回工单,预计 3–5 个工作日
依据 政策 v2026.10.1 / high-value-refund;风控分 0.82(高)
证据 trace:8f3a diff:acct-change history:INV-1880 同类已退
超时 15 分钟未批 → 拒绝;拒绝后可补充材料重新提交
请求者 support-3;替代方案:先冻结账户,等待 24h 人工核验
这张卡的信息量大约是一屏。它不包含 prompt、不包含模型自述、不包含几十行工具日志;它包含审批人真正需要的六个判断:做什么、改什么、影响多大、能不能回滚、凭什么、来不及怎么办。
盖章的特征是:不读、不问、不承担。审批的特征是:读了差异、问了疑点、签字后要能解释。让审批有效,不是加一个「同意」按钮,而是让「拒绝」有明确后果、让「同意」有明确依据、让「不处理」有默认结果。三者缺一,审批就退化成盖章。
12.4.3 四眼原则:两个独立的人,而不是两个会话
四眼原则(four-eyes principle)要求至少两个独立主体参与批准。工程上,「独立」至少包含四层:
| 独立性 | 要求 | 常见伪实现 |
|---|---|---|
| 身份独立 | 两个不同的认证主体 | 同一人用两个账号;共享账号 |
| 职责独立 | 发起者不能批准;最好不同团队 | 同一人先发起再「转交」自己 |
| 信息独立 | 两人看到同一份冻结证据 | 第二人只看到第一人的结论 |
| 时间独立 | 两次批准有最小间隔 | 一键「全部批准」 |
这张表说明什么:两个会话不等于两个人。共享账号、代点、批量批准都会让四眼退化成一眼。技术上的最小校验是:approver != requester、approver 去重、审批对象是参数摘要的哈希、两次批准记录各自带时间与身份。更强的做法是要求不同认证器(密码 + 硬件密钥),或不同团队的角色。
四眼不是越多越好。三个人审批一个低风险变更,不会提高安全性,只会提高「找两个人一起点」的社交成本。审批人数应与风险等级挂钩:中风险 1 人,高风险 2 人,关键动作 2 人且其中一人来自安全/风控。人数之外,还要规定谁不能批:发起者、受益人、直接下属、与被审批动作有利益冲突的人。
12.4.4 超时与降级:默认拒绝,而不是默认放行
审批一定会遇到「没人看」。超时行为必须在设计时写死,不能留给值班人临场决定。三种可选策略:
| 策略 | 行为 | 适用 | 风险 |
|---|---|---|---|
| fail-closed | 超时即拒绝,需重新申请 | 高风险、不可逆 | 业务延迟;可能促使绕过 |
| escalate | 超时升级到下一级/值班主管 | 中风险、有时限 | 升级链断掉时仍会卡住 |
| degrade | 降级为只读/预览/排队,不执行副作用 | 可分解动作 | 实现复杂;需保证降级路径真的无副作用 |
这张表说明什么:不能自动放行高风险动作。超时的默认结果必须是「没有发生」,而不是「视为同意」。如果业务无法接受拒绝,正确做法是提前扩容审批人、缩短审批链、把动作拆成可逆的小步,而不是把超时改成同意。
一个审批状态机至少要有五个状态:pending、approved、denied、expired、cancelled。每个状态转移都要写一条审计事件,带 wait_s 与 degraded_to。超时事件要记录「当时还差几个人」「最后一次提醒在何时」「升级给了谁」,否则事后无法区分「没人看」与「系统没通知」。
12.4.5 审批疲劳:注意力是有限预算
审批疲劳不是态度问题,是负载问题。当一个审批人每小时收到 40 条请求,每条只有 7 秒处理时间,他的最优策略就是点「同意」——不是因为不负责,而是因为拒绝需要调查,而调查没有时间预算。疲劳的根因通常不是人少,而是审批点放得太多、请求信息太差、批量操作太方便。
可观测的疲劳指标:
| 指标 | 定义 | 危险信号 |
|---|---|---|
| 审批负载 | 每人每小时待批数 | 持续 > 20 |
| 停留时间 | 打开到决定的秒数 | 中位数 < 10 秒 |
| 批准率 | approved / decided | > 95% 且高风险也如此 |
| 批量比例 | 批量批准 / 总批准 | 高风险出现批量 |
| 非工作时间 | 夜间/周末审批占比 | 持续上升 |
| 推翻率 | 事后复核推翻 / 抽样 | > 5% |
这张表说明什么:批准率高不一定是好事。如果高风险请求也以 98% 通过、中位停留 6 秒,说明审批没有产生信息,只是在产生记录。此时应该做的不是加人,而是把低风险请求移出人工队列:提高自动批准上限、按风险路由、对低风险做抽样而不是逐条审批。
批量批准要单独限制。低风险的批量操作可以保留,但高风险动作必须逐条批准,且每条都绑定自己的参数摘要。一个常见的反模式是「全部批准」按钮把 30 条请求一起通过,其中一条是跨租户导出;审计记录上看似有 30 个批准,实际上只有一次注意力。
- 看差异:金额、账户、范围、版本是否与请求描述一致;不接受「详见附件」。
- 看影响面:涉及多少记录/租户/用户;是否对外;是否触及敏感数据。
- 看可逆性:能否回滚;回滚需要多久;最坏情况损失是多少。
- 看依据:策略版本、风险分、历史同类动作;风险分高在哪里。
- 看替代方案:不做会怎样;有没有更小的动作能达到目的。
- 做决定:批准、拒绝、要求补充证据;拒绝要写理由,批准要确认参数摘要。
- 留记录:审批意见进入审计事件;高风险动作标记事后复核。
Q1 一张审批卡写了「退款 1.25 万,已通过风控」,但没写收款账户变更。审批人应该怎么做?
答案
拒绝或要求补充,而不是批准。收款账户变更是退款欺诈最常见的信号之一;「已通过风控」是结论,不是证据。审批人需要看到账户变更时间、新账户历史、与请求者的关系,以及是否有回拨机制。缺少这些信息时,正确动作是「暂停 + 补充材料」,并记录拒绝理由,而不是用自己的经验替系统补证据。
Q2 团队把高风险请求也接入了「全部批准」,理由是「每天只有三五条」。这样做的问题是什么?
答案
问题不在数量,而在注意力与证据的绑定。批量批准把多条请求压缩成一次点击,审批人实际上没有逐条检查差异;一旦其中一条被替换参数或跨租户,记录上仍然显示「已批准」。高风险动作必须逐条批准,每条绑定独立的参数摘要与证据;批量只适用于低风险、同质、可抽样的请求。
12.5 策略即代码:OPA/Cedar 思路与决策日志
当策略只存在于文档或代码的 if 分支里,它会有三个问题:无法独立测试、无法解释一次决定、无法安全地灰度。策略即代码(policy as code)要解决的就是这三件事:把策略从业务代码里抽出来,变成有版本、有测试、有决策日志的构件。
12.5.1 三种形态:硬编码、外部策略引擎、授权语言
| 形态 | 例子 | 优点 | 缺点 |
|---|---|---|---|
| 硬编码 | if amount > 500_000: require_approval() | 简单、快 | 改策略要发版;难以审计;易散落 |
| 外部策略引擎 | OPA/Rego、Cedar、自研 DSL | 策略独立版本、可测试、可灰度 | 多一个服务;输入契约要稳定 |
| 授权库/语言 | Cedar、OpenFGA 等 | 类型化、可分析 | 学习成本;表达能力有边界 |
这张表说明什么:不是所有系统都需要 OPA。策略少、变更慢时,硬编码加上良好的测试也能工作;但一旦出现「不同租户不同限额」「不同环境不同审批人」「监管要求按地区变化」,策略就会从几个 if 变成一张矩阵,此时外部化才划算。
12.5.2 Rego 与 Cedar:两种思路,不写语言教程
OPA(Open Policy Agent)用 Rego 描述「输入文档是否满足条件」。它的典型形态是:宿主把请求打包成 JSON,交给 OPA 求值,得到 allow/deny 或结构化决策。下面是一段示意,不是完整策略:
package agent.authz
default allow := false
deny if {
input.subject.tenant != input.resource.tenant
}
require_approval if {
input.action == "refund"
input.context.amount_cents > 500000
input.subject.tenant == input.resource.tenant
}
allow if {
input.action == "read"
input.subject.tenant == input.resource.tenant
}
三段规则可以同时为真。Rego 只负责求值,优先级必须由调用方显式定义;本章实验用 deny > require_approval > allow 的保守顺序。若把优先级交给规则书写顺序,策略行为会随文件重排而变。Rego 的强项是通用:输入是任意 JSON,策略可以遍历数组、引用数据文档、组合多个包。它适合基础设施授权、Kubernetes 准入、跨服务统一策略。代价是:输入 schema 靠约定,复杂策略的调试成本不低,性能取决于求值方式。
Cedar 走另一条路:实体(principal/resource)、动作、上下文都是类型化的,策略是 permit/forbid 规则,forbid 默认覆盖 permit。它的典型形态:
permit (
principal,
action == Action::"read",
resource
) when {
principal.tenant == resource.tenant
};
forbid (
principal,
action,
resource
) when {
principal.tenant != resource.tenant
};
Cedar 的强项是可分析:类型化让策略可以静态检查,forbid 覆盖让「默认拒绝」更容易表达。它适合应用内的细粒度授权,尤其是多租户 SaaS。代价是:实体模型要先设计好,跨系统的通用策略不如 Rego 自然。
| 维度 | Rego / OPA | Cedar |
|---|---|---|
| 输入 | 任意 JSON 文档 | 类型化实体 + 动作 + 上下文 |
| 默认 | 策略作者显式写 default | 无 permit 即拒绝;forbid 覆盖 |
| 强项 | 通用、生态广、适合基础设施 | 类型安全、可分析、适合应用授权 |
| 测试 | opa test + 表驱动用例 | 策略测试 + 授权请求批量验证 |
| 决策日志 | 宿主记录 input/result/版本 | 宿主记录 request/decision/版本 |
这张表说明什么:两者都能表达 allow 与 deny;本章的 require_approval 需要引擎把「审批」作为结构化输出(例如 obligations 或单独的 decision 实体),不能只返回一个布尔。选型时先看输入是否稳定、是否需要跨系统复用、团队是否能维护,而不是看语法是否优雅。语言不是治理的难点,输入契约与版本管理才是。
一段 if 只告诉你「现在走哪个分支」。一条策略要能回答:当时输入是什么、命中哪条规则、依据哪版策略、谁改过这版策略、如果改成新版本会发生什么。把策略当代码,不是为了写得漂亮,而是为了让它像代码一样被测试、审查、回滚和追责。
12.5.3 策略生命周期:测试、影子、灰度、回滚
一个可执行的策略生命周期有六步:
- 建模输入:定义主体、动作、资源、上下文的 schema;拒绝自由文本进入策略。
- 写规则与测试:每条规则至少一个 allow、一个 deny、一个边界用例;测试和策略同仓库。
- 影子模式(dry-run):新策略只计算、不执行,把「新旧决策差异」写进日志;差异率高就先查输入。
- 灰度:按租户/动作/比例启用;高风险动作先小范围,保留快速回滚开关。
- 强制:全量启用;决策日志带策略版本与包哈希;异常决策告警。
- 复盘与下线:定期清理不再命中的规则;删除前确认没有流量与审计依赖。
每条规则至少配三行测试,测试与策略同仓库、同一次代码评审:
| 用例 | 输入 | 期望 |
|---|---|---|
| 同租户只读 | action=read, tenant 相同 | allow |
| 跨租户读取 | action=read, tenant 不同 | deny |
| 大额退款 | action=refund, amount=1,250,000 | require_approval |
| 边界金额 | action=refund, amount=500,000 | allow(阈值是严格大于) |
| 规则异常 | 条件抛异常 | deny(fail-closed) |
这张表说明什么:测试要覆盖「允许、拒绝、审批、边界、异常」五类,而不是只测 happy path。边界用例尤其重要:> 与 >= 的一字之差,会让审批阈值在真实金额上失效。
影子模式是这里最重要的工程实践。它把「策略变更」从一次不可逆的生产切换,变成一次可比较的实验:
旧策略: allow 新策略: require_approval diff: 12 条/小时
旧策略: deny 新策略: allow diff: 3 条/小时 ← 优先审查
第二行最危险:新策略放宽了原本被拒绝的请求。每一条差异都要有人解释:是修 bug,还是放宽了边界?没有解释的放宽,不应该上线。
12.5.4 决策日志:策略的「执行回执」
策略引擎每次求值都要留下决策日志,字段与 12.2 的审计事件对齐:input_digest、policy_version、bundle_hash、decision、rule_id、obligations、latency_ms。日志要能回答三个问题:这次为什么是这个结果、当时用的是哪版策略、如果换成新策略会怎样。
{
"request_id": "req-002",
"input_digest": "sha256:5c1e...",
"policy_version": "2026.10.1",
"bundle_hash": "sha256:9a0d...",
"decision": "require_approval",
"rule_id": "require-approval-high-value-refund",
"obligations": ["four_eyes", "finance_ticket"],
"latency_ms": 2
}
四个常见坑:
- 策略求值与执行之间的 TOCTOU:策略看到的是旧属性,执行时属性已变。对策是把决策绑定到输入摘要,执行器拒绝摘要不匹配的请求。
- 义务不执行:策略返回
four_eyes,执行器只检查了allow。对策是把 obligations 变成执行器的强制参数,而不是注释。 - 版本漂移:决策日志记了版本,但策略包没有哈希;同名版本被覆盖后无法复现。对策是版本 + 内容哈希双记录。
- 策略蔓延:规则越加越多,没人知道哪条还在生效。对策是命中率统计与定期清理,删除前跑影子模式确认无差异。
12.6 合规映射:NIST AI RMF、ISO 42001 与 EU AI Act
合规不是把法条抄进代码,而是把外部要求翻译成可检查的工程产物。同一件事通常有三种说法:框架说「建立治理职能」,标准说「形成文件化信息」,法条说「实施风险管理体系」。工程师需要的是第四种说法:哪个字段、哪条日志、哪个测试、谁签字。这一节只做这种翻译,不解释法律条文,也不构成法律意见。
12.6.1 NIST AI RMF:四个函数与生成式 AI Profile
NIST AI Risk Management Framework(AI RMF 1.0,2023)是自愿性框架,不是认证;它把 AI 风险管理组织成四个函数:Govern、Map、Measure、Manage。它们不是四个阶段,而是持续循环的四类活动:
| 函数 | 核心问题 | 工程产物 | 本书落点 |
|---|---|---|---|
| Govern | 谁负责、什么政策、如何问责? | 治理四元组、责任矩阵、策略版本 | 12.1、12.5 |
| Map | 系统在什么场景、影响谁、有什么风险? | 用途清单、数据流、风险登记册 | 11.1、12.6 |
| Measure | 风险如何度量、证据是什么? | 评估集、红队结果、审计指标 | 9.4–9.6、11.7 |
| Manage | 风险如何处置、残余风险由谁接受? | 处置计划、审批记录、复核结论 | 12.4、12.7 |
这张表说明什么:Govern 不是「写一份治理文档」,而是把另外三个函数需要的决策权、证据与时效定下来。没有 Govern,Map 会变成一次性清单,Measure 会变成没有阈值的报表,Manage 会变成出了事再开会。
NIST 的 Generative AI Profile(NIST AI 600-1,2024)在 AI RMF 之上补充了生成式 AI 特有的风险,例如 confabulation、信息完整性、知识产权、数据隐私、信息安全和有害内容。对 agent 系统,最直接的可执行项是:
- confabulation:把「模型声称完成」与「系统验证完成」分开;工具结果与验收测试进入 trace。
- 数据隐私:审计事件最小化;训练/检索数据来源可追溯;删除请求有执行路径。
- 信息安全:prompt injection 的边界在能力隔离与出口约束,见第 11 章。
- 信息完整性:对外发布的内容要有来源与人工复核点,尤其是高影响领域。
12.6.2 ISO/IEC 42001:管理体系落到文件与记录
ISO/IEC 42001:2023 是 AI 管理体系(AIMS)标准,结构沿用 ISO 管理体系的高层结构:组织环境、领导作用、策划、支持、运行、绩效评价、改进。它要求组织建立 AI 政策、角色与职责、风险评估与处置、生命周期管理、供应商管理、事件管理、监视与内部审计。
工程上可以把它压成一张「文件 → 记录 → 证据」表:
| 管理体系要求 | 可执行产物 | 证据在哪里 |
|---|---|---|
| AI 政策与职责 | 决策权表、责任矩阵、审批角色 | 策略仓库、组织架构、审批日志 |
| 风险评估与处置 | 风险登记册、阈值、残余风险接受人 | 风险评审记录、策略变更单 |
| 生命周期管理 | 需求、测试、发布、退役的门禁 | CI 记录、评估报告、发布单 |
| 供应商与第三方 | 工具/模型清单、合同条款、评估结果 | 工具注册表、供应商评估 |
| 事件管理 | 事故分级、上报路径、复盘 | 事件工单、审计事件、整改项 |
| 监视与内部审计 | 指标、抽样、审计计划 | 仪表盘、审计报告、抽样记录 |
这张表说明什么:认证审核看的是「你说有,能不能拿出来」。如果审批日志只有 7 天、策略版本没有哈希、事故复盘没有整改项,管理体系文件再完整也补不上证据缺口。反过来,证据齐全但没人定期看,也不构成「监视」。
12.6.3 EU AI Act:时间表与工程可执行项
EU AI Act 的适用是分阶段的。下表按公开时间线整理,具体适用日期以欧盟官方最新发布为准:
| 节点 | 日期 | 内容 | 状态 |
|---|---|---|---|
| 生效 | 2024-08-01 | 法规生效 | 已生效 |
| 禁止性条款 | 2025-02-02 | 禁止特定 AI 实践 | 已适用 |
| 通用目的 AI(GPAI) | 2025-08-02 | GPAI 模型义务 | 已适用 |
| 高风险(原定) | 2026-08-02 | 附件 III 高风险系统义务 | 可能调整 |
| 高风险(附件 I) | 2027-08-02 | 嵌入受监管产品的系统 | 原定 |
2025 年 11 月,欧盟委员会提出 Digital Omnibus,其中包含对 AI Act 部分义务适用时间的调整建议。截至本书写作时,高风险系统的最终适用时间仍可能变化;不要把它当成已经钉死的日期。 工程上应做的是:按当前官方时间表规划,但把「合规日历」做成可配置项,而不是写死在代码里。
对 agent 系统,EU AI Act 中最直接的工程可执行项是:
| 义务方向 | 工程动作 | 证据 |
|---|---|---|
| 风险管理 | 风险登记册 + 每次高风险动作的风险分级 | 风险记录、策略决策 |
| 日志与可追溯 | 审计事件、策略版本、执行回执 | append-only 日志、哈希链 |
| 人类监督 | 审批点、四眼、超时、可中止 | 审批记录、人工中止事件 |
| 技术文档 | 架构、数据流、工具清单、评估结果 | 文档仓库、版本标签 |
| 事件报告 | 严重事件分级、时限、上报路径 | 事件工单、通知记录 |
| 上市后监测 | 指标、抽样、漂移检测、回归 | 仪表盘、评估报告 |
这张表说明什么:合规条款最终都要落到「一条记录 + 一个负责人 + 一个时限」。如果某个条款找不到对应的记录,它就没有被工程化;如果找到了记录但没人负责,它就没有被治理。
监管时间表、指南与豁免会更新。把「2026-08-02」写进策略常量,会让一次法规调整变成一次紧急发版;正确做法是把适用日期、适用地区、适用角色做成配置,并由合规负责人维护。代码只负责在配置生效时执行对应的控制项。另:本节不是法律意见,具体适用请咨询法务。
- 「过了 ISO 42001 认证就等于合规」——认证证明管理体系符合标准,不证明具体系统符合 AI Act 或行业法规;两者范围不同。
- 「有审计日志就等于可追溯」——没有策略版本、参数摘要与执行回执的日志,无法重建动作;可追溯性看的是证据链,不是日志量。
- 「高风险系统只要加人工审批就行」——人工监督只是义务之一;风险管理、技术文档、日志、事件报告、上市后监测都要有对应产物。
- 「法规日期是固定的」——适用时间可能因修订或过渡条款调整;以官方最新时间表为准,系统设计要能配置。
合规映射的终点不是一张对照表,而是证据清单:每个控制项都能指向一条可验证的记录,每条记录都有负责人和保留期。第 12.8 节的实验把其中最小的一环——策略决策与审批记录——跑成可执行的代码;其余环节(风险登记、技术文档、事件报告)需要在组织流程里落地。
12.7 失败案例:审批疲劳、日志缺失与权限漂移
三个案例分别对应治理四元组的不同缺口:审批疲劳是决策权与注意力的问题,日志缺失是证据的问题,权限漂移是时效的问题。案例中的数据经过脱敏与取整,但触发条件、观测证据与修复路径都来自可复现的工程模式。
触发条件。 某客服平台把所有退款请求都送进同一个人工队列,包括 500 元以下的小额退款。12 名值班人每月处理约 1,847 条请求,人均每个工作日约 7 条,看起来不高;但请求集中在两个高峰时段,峰值时每人队列里有 30–40 条待批。
观测证据。 审计日志显示:当月批准率 96.2%,中位停留时间 7.4 秒,92% 的批准没有填写意见。一条 1,250,000 分的退款在 02:41 进入队列,审批卡只写了「退款 1.25 万,已通过风控」,没有收款账户变更信息;值班人在 4 秒内批准,当时他的队列里有 38 条待批。三天后客户投诉收款账户不是本人,资金已转出。
根因。
- 低风险请求没有自动批准,占用了 80% 的注意力;
- 审批卡只给结论(「已通过风控」),不给差异(账户变更)与证据;
- 队列没有按风险分级,高风险请求和 50 元退款混在一起;
- 系统没有审批负载指标,管理层只看到「96% 通过、处理很快」;
- 批量批准按钮让「一次点击通过 20 条」成为常规操作。
修复。
- 50,000 分以下、同租户、无账户变更的退款自动批准,并做 5% 抽样;
- 高风险队列独立,审批卡强制包含金额、账户变更、影响面、回滚方案;
- 每人每小时待批上限 15 条,超过自动升级到第二值班组;
- 高风险禁止批量批准,批量只保留给低风险同质请求;
- 新增指标:中位停留、批准率、批量比例、事后推翻率;当「高风险中位停留 < 15 秒」时告警。
结果。 三个月后,71% 的退款自动完成;高风险请求的中位停留升到 96 秒,拒绝率 6.1%;抽样推翻率从 0.3% 升到 1.8%——不是变差了,而是以前根本没在看。
触发条件。 某数据平台的运维 agent 在 02:14 调用迁移工具删除了一张生产表。应用日志写 stdout,保留 7 天;策略判定在服务内存里完成,没有落盘;删除操作使用平台管理员账号,没有单独审批。
观测证据。 事故发生后,团队能找到的只找到 3 行有用的应用日志:
02:14:01 INFO tool=db.migrate action=delete target=orders_v2
02:14:01 INFO result=ok rows=0
02:14:02 INFO task finished
没有 request_id,没有策略版本,没有审批记录,没有执行回执,也没有「谁发起了这次调用」。SIEM 里有网络日志,但只能看到服务账号访问了数据库,无法区分是哪个 agent、哪次任务。调查花了三天,最后靠值班人回忆才确认是「清理测试表」的脚本跑错了环境。
根因。
- 日志是为调试写的,不是为审计写的:字段不全、无关联键;
- 保留期 7 天,等到要查时已经轮转;
- 危险动作没有审批,也没有 break-glass 事件号;
- 执行器没有返回可核对的回执(删了哪张表、多少行、哪个环境);
- 没有哈希链或 WORM,无法证明日志未被修改。
修复。
- 定义审计事件 schema:
request_id、actor、action、resource、environment、policy_version、decision、params_digest、evidence_ref; - 审计日志写入独立存储,启用对象锁,保留 1 年;链头每日锚定到独立账号;
- 生产删除必须走审批或 break-glass,执行器返回回执并写审计;
- 每季度做一次「只给审计记录,重建一次删除」的演练,10 分钟内答不出五个问题就补字段;
- 把
params_digest与执行回执比对纳入发布门禁。
结果。 下一次类似操作在 6 分钟内完成重建:谁发起、什么策略、谁批准、删了哪张表、影响多少行。日志量下降了 40%,因为不再把完整请求体写进审计事件,而是存摘要与引用。
触发条件。 一次线上故障中,on-call agent 需要临时读取生产数据库的慢查询日志。值班人通过工单系统给它加了 prod:admin,计划「故障结束后回收」。故障在 40 分钟后解除,工单关闭,权限没有回收。
观测证据。 94 天后做权限盘点时发现:
- IAM 里
agent-oncall仍持有prod:admin,签发时间 94 天前,无过期时间; - 使用日志显示这条权限在 94 天里只被用过 2 次,都是故障当天;
- 同期授予的 37 条临时权限中,有 14 条从未使用,未使用率 38%;
- 其中一条凭证在非工作时间从新的出口 IP 使用过一次,无法解释;
- 季度 review 的表格只列了「角色是否仍然存在」,没有列「实际是否使用」。
根因。
- 临时权限绑定工单,不绑定 TTL;工单关闭不触发回收;
- 权限粒度太粗:只需要读慢查询日志,却给了
prod:admin; - review 以「授予」为对象,不以「使用」为对象,漂移不可见;
- 没有 break-glass 流程,临时授权走的是普通工单;
- 没有「未授予使用」告警,异常调用无人发现。
修复。
- 所有临时权限改为 JIT 租约:TTL 30 分钟、scope 限定到
db:slowlog:read、到期自动失效; - 故障场景走 break-glass:必须带
incident_id、通知安全、24 小时内复核; - 每日对比授予与使用:
unused超过 30 天自动进入回收工单,ungranted立即告警; - 季度 review 改为「使用记录 + 租约记录」双表,不再只看角色;
- 新出口 IP 的凭证使用触发二次认证或拒绝。
结果。 临时权限的平均存活时间从 94 天降到 27 分钟;未使用授予从 38% 降到 6%;ungranted 事件从每月 11 条降到 1 条,且每条都有调查结论。
三个案例的共同模式可以压成一张表:
| 案例 | 缺失的治理对象 | 第一份可用证据 | 最小修复 |
|---|---|---|---|
| 审批疲劳 | 决策权(风险路由)+ 责任(负载) | 审批停留时间、队列长度、批准率 | 低风险自动 + 高风险独立队列 |
| 日志缺失 | 证据(schema/完整性) | 执行回执、策略版本、参数摘要 | 审计事件 + WORM + 锚定 |
| 权限漂移 | 时效(TTL/回收) | 授予时间、使用记录、差集 | JIT 租约 + 漂移检测 |
这张表说明什么:三个案例都不是「模型变坏了」。它们分别是路由、证据、时效的工程缺口;修复也都不需要换模型,只需要改策略、日志与权限生命周期。
12.8 实验:策略引擎 + 审批工作流
实验把本章的三条主线压成可运行的最小件:策略求值与版本、审批状态机与四眼/超时/break-glass、审计哈希链与权限漂移。全部只依赖 Python 3.11+ 标准库,时钟与输入都是确定的,输出可逐字复现。
实验目录是 experiments/ch12/,四个文件分工如下。
12.8.1 文件与运行
| 文件 | 作用 | 关键实现 |
|---|---|---|
policy_engine.py | 主体/动作/资源/上下文 → allow / require_approval / deny | 优先级、默认拒绝、策略版本、dry-run、JSONL 哈希链 |
approval_flow.py | 风险分级、四眼、超时、break-glass | 状态机、审批人角色、审计事件、统计 |
test_governance.py | 16 条回归测试 | 优先级、默认拒绝、超时、break-glass、漂移 |
README.md | 运行方式、语义、限制 | 与正文一致 |
cd agent-systems
python3 experiments/ch12/policy_engine.py demo --log /tmp/ch12-decisions.jsonl
真实输出(macOS,Python 3.13.13):
[ allow] req-001 read invoice:INV-1042 rule=allow-own-tenant-read policy=2026.10.1 dry_run=false
[require_approval] req-002 refund invoice:INV-2091 rule=require-approval-high-value-refund policy=2026.10.1 dry_run=false
obligations=four_eyes,finance_ticket
[ deny] req-003 delete db:prod-orders rule=deny-prod-destructive policy=2026.10.1 dry_run=false
[require_approval] req-004 export table:customers rule=require-approval-sensitive-export policy=2026.10.1 dry_run=true
obligations=data_owner_approval,watermark
log=/tmp/ch12-decisions.jsonl events=3
chain=chain OK events=3
四行分别演示:同租户只读被允许;大额退款需要审批;生产删除被拒绝;敏感导出在 dry-run 下只计算、不写日志。最后一行 events=3 是 dry-run 的直接证据——四条决策只落了三条。
python3 experiments/ch12/approval_flow.py demo --log /tmp/ch12-approvals.jsonl
[1] read invoice:INV-1042 risk=low status=approved mode=auto approvers=[] wait=0s
[2] update config:checkout risk=medium status=approved mode=human approvers=['sre-lead'] wait=90s
[3] refund invoice:INV-3099 risk=high status=approved mode=human approvers=['finance-2', 'finance-4'] wait=150s
[4] export table:customers risk=critical status=denied mode=human approvers=[] wait=300s
degraded_to=deny_and_page_oncall reason=timeout
[5] rollback service:checkout risk=critical status=approved mode=break_glass approvers=[] wait=0s
post_review_due=2026-10-20T10:18:20Z incident=INC-2026-1019
rejected attempts:
support-3 apr-003 reason=self_approval
stats:
submitted=5 auto=1 human=2 timeout=1 break_glass=1 rejected=1
approval_rate=0.667 mean_wait_s=120.0 anomalous_release=1
chain=chain OK events=12 ok=true
log=/tmp/ch12-approvals.jsonl
五步覆盖五种路径:低风险自动、中风险单人、高风险四眼(含自批被拒)、超时拒绝并记录降级动作、break-glass 带事件号。实验状态机只保留 pending / approved / denied 三态;生产还要补 expired / cancelled,以及对应的通知与撤销路径。approval_rate=0.667 的分母是「人工批准 + 超时」,不含自动批准与 break-glass;anomalous_release=1 只统计 break-glass,因为它是唯一绕过常规审批的放行。
12.8.2 测试与观测
python3 -m unittest discover -s experiments/ch12 -q
----------------------------------------------------------------------
Ran 16 tests in 0.006s
OK
耗时随机器变化;测试数量与断言内容是判据。测试里最值得看的是四条负向断言:
test_default_deny:没有规则命中时必须是 deny;test_policy_error_fails_closed:规则条件抛异常时不能跳过,必须 deny;test_timeout_fails_closed:到达 deadline 仍未凑够人数 → denied,且审计带wait_s=600;test_break_glass_requires_incident_and_is_audited:无事件号拒绝,有事件号放行但写post_review_within_24h。
从审计日志统计审批通过率、平均等待与异常放行,可以用实验自带命令,也可以直接用 jq:
python3 experiments/ch12/approval_flow.py stats /tmp/ch12-approvals.jsonl
submitted=5 auto=1 human=2 timeout=1 break_glass=1 rejected=1
approval_rate=0.667 mean_wait_s=120.0 anomalous_release=1
jq -s '{granted: ([.[] | select(.event_type == "approval_granted")] | length),
timeout: ([.[] | select(.event_type == "approval_timeout")] | length),
break_glass: ([.[] | select(.event_type == "break_glass_used")] | length),
mean_wait_s: ([.[] | select(.event_type == "approval_granted") | .evidence.wait_s] | (add / length))}
| . + {approval_rate: (.granted / (.granted + .timeout))}' /tmp/ch12-approvals.jsonl
{
"granted": 2,
"timeout": 1,
"break_glass": 1,
"mean_wait_s": 120,
"approval_rate": 0.6666666666666666
}
这条查询说明什么:approval_rate 只应由「有明确结论的人工审批」计算;把自动批准塞进分母,会让通过率虚高;把 break-glass 塞进分子,会掩盖「绕过常规审批」的比例。指标口径要在代码里写死,而不是在报表里解释。
12.8.3 代码骨架:三个关键判断
策略引擎的核心只有十几行,但三个顺序不能错:
def evaluate(self, request):
for rule in self.bundle.ordered(): # 高优先级 + 更保守的 tie-break
try:
hit = rule.matches(request)
except Exception: # 策略写错时 fail-closed
return Decision(..., decision="deny", reason="policy_error")
if hit:
return Decision(..., decision=rule.effect, matched_rule=rule.id)
return Decision(..., decision="deny", reason="default_deny")
审批状态机的四个判断同样是顺序敏感:
if approver == request.requester:
reject("self_approval")
elif approver in state.approvers:
reject("duplicate_approver")
elif APPROVER_ROLE not in roles:
reject("role_not_allowed")
elif risk.requires_security and "security" not in roles and not already_has_security:
reject("security_approver_required")
先查身份,再查重复,再查角色,最后查风险特例。顺序反了会出现「先记了一次批准,再发现是自批」的脏状态。审计日志的哈希链则只有一条规则:hash 覆盖除自己以外的全部字段,prev_hash 指向上一条;任何字段变动都会让后续链断掉。
12.8.4 从实验到生产:还缺什么
这个实验刻意省略了生产必需的六件事,读者不应把它直接部署:
| 缺项 | 实验现状 | 生产要求 |
|---|---|---|
| 身份 | 审批人是字符串 | 接 IdP,验证认证强度与会话 |
| 策略语言 | Python 函数 | Rego/Cedar/DSL;策略与代码同仓同流水线,可按团队分目录 |
| 存储 | 本地 JSONL | 对象存储 + 对象锁 + 外部锚定 |
| 时钟 | 注入整数秒 | 单调时钟 + NTP 监控 + 漂移告警 |
| 通知 | 无 | 审批提醒、超时升级、break-glass 告警 |
| 权限 | 仅检测差集 | 对接 IAM/租约库,自动回收与复核 |
这张表说明什么:实验证明的是语义——优先级、默认拒绝、四眼、超时、break-glass、哈希链——不是生产强度。把语义跑对,再接身份、存储与通知,比先接一堆基础设施却说不清策略语义更可靠。
母本对应
| 母本 | 章节 | 本书落点 | 程度 |
|---|---|---|---|
| NIST AI RMF 1.0(2023) | Govern / Map / Measure / Manage | 12.6.1 四函数与工程产物 | 背景 |
| NIST AI 600-1(2024) | Generative AI Profile | 12.6.1 生成式 AI 风险项 | 背景 |
| ISO/IEC 42001:2023 | AI 管理体系要求 | 12.6.2 文件、记录与证据 | 背景 |
| EU AI Act(Regulation 2024/1689) | 分阶段适用、高风险义务 | 12.6.3 时间表与工程映射 | 交叉引用 |
| OPA / Rego 文档 | 策略求值、bundle、决策日志 | 12.5.2 Rego 思路 | 背景 |
| Cedar 文档 | 类型化授权、forbid 覆盖 | 12.5.2 Cedar 思路 | 背景 |
| NIST SP 800-92 | 日志管理与保留 | 12.2 事件、保留、完整性 | 背景 |
| 《Linux 系统与内核》第 2 篇 | 2.9 进程凭证、用户、组与能力 | 12.3 权限机制的上游 | 交叉引用 |
| 本书第 4 章 | capability、seccomp、landlock | 12.3 工具级/环境级约束的机制层 | 交叉引用 |
| 本书第 10 章 | trace、日志、回放 | 12.2 证据引用的来源 | 交叉引用 |
| 本书第 11 章 | 威胁模型、注入、出口约束 | 12.1 风险输入的来源 | 交叉引用 |
读这张表要抓住两点:合规框架只提供要求的方向,不提供实现;本书对 OS 与安全章节只做交叉引用,不重复讲 capability 与 seccomp 的内核细节。表中「背景」表示正文只借用概念与术语,读者需要时再回母本。
本章陷阱
| # | 陷阱 | 症状 | 正确做法 | 去哪 |
|---|---|---|---|---|
| 1 | 把治理写成文档 | 有制度、有委员会,但策略引擎里没有对应规则 | 四元组落到求值器、凭证、日志、复核 | 12.1 |
| 2 | 把责任推给模型 | 复盘结论是「模型幻觉」,无人改系统 | 按「谁能改变决定」分责任 | 12.1.2 |
| 3 | 审计只记最终答案 | 不知道谁批准、按哪版策略 | 六问 + 八元组事件 | 12.2 |
| 4 | 把日志当审计 | 字段不全、无关联键、事故时不可用 | schema、关联 id、完整性、保留 | 12.2 |
| 5 | 哈希链当 WORM | 有链无锚定,整链可重写 | WORM + 外部锚定 + 条数锚定 | 12.2.1 |
| 6 | 审计日志存完整 PII | 日志成为隐私仓库,删除困难 | 白名单、令牌化、引用原文 | 12.2.2 |
| 7 | 只做工具级 scope | 数据、时间、环境三层未收 | 四层组合;租约参数化 | 12.3 |
| 8 | 临时权限无 TTL | 一次授权活到转岗/离职 | JIT 租约 + 到期自动回收 | 12.3.4 |
| 9 | 审批点撒得太广 | 低风险淹没高风险,96% 点同意 | 风险路由;低风险自动 + 抽样 | 12.4.5 |
| 10 | 审批卡只有结论 | 没有差异、影响面、回滚方案 | 九元组审批卡;冻结参数摘要 | 12.4.2 |
| 11 | 超时自动放行 | 没人看等于默认同意 | fail-closed;升级或降级为只读 | 12.4.4 |
| 12 | 策略硬编码、无版本 | 改策略靠发版,事后无法复现 | 策略即代码 + 测试 + 决策日志 | 12.5 |
这张表说明什么:十二个陷阱里,只有第 2 个是「归因错误」,其余都是可落地的工程缺口。它们不会因为换模型、换框架而消失;第 9 与第 11 条最危险,因为它们让「有审批」看起来像「有控制」。
本章出口检查
| 检查项 | 达标线 | 没达标去哪 |
|---|---|---|
| 能说清治理四对象 | 决策权、责任、证据、时效各给一个产物 | 12.1 |
| 能画决策链 | 请求 → 策略 → 审批 → 执行 → 审计,标出关联 id | 12.1 |
| 能写审计事件 | 六问齐全;含策略版本、参数摘要、证据引用 | 12.2 |
| 能解释哈希链边界 | 能证明篡改可见,也能说出截断/重写/锚定失效 | 12.2.1 |
| 能做保留与最小化 | 按数据类分层;原文用引用而非副本 | 12.2.2 |
| 能设计权限租约 | 八元组齐全;执行器 fail-closed 检查 | 12.3.1 |
| 能设计 break-glass | 事件号、TTL、通知、复核、不可静默 | 12.3.3 |
| 能落地四眼与超时 | 自批/重复/角色/security 四道检查 | 12.4.3、12.4.4 |
| 能写审批卡 | 差异、影响面、回滚、证据、超时齐全 | 12.4.2 |
| 能组织策略生命周期 | 测试、影子、灰度、回滚、决策日志 | 12.5 |
| 能映射合规要求 | 每个条款有产物、证据、负责人 | 12.6 |
| 能跑实验 | 16 条测试全过;demo 输出 allow/approval/deny | 12.8 |
Q1 已经启用了哈希链,为什么还需要 WORM 与外部锚定?
Q2 高风险审批超时后自动放行,会带来什么问题?如果业务无法接受拒绝,应该怎么改?
Q3 策略版本为什么要写进每一条决策日志?只写「当前策略」有什么问题?
答案
A1 哈希链只能发现「记录之间不一致」,不能阻止有写权限的人重写整条链并重算所有哈希;它也不能防止删除尾部记录后剩下的链仍然自洽。WORM 在保留期内阻止删除与覆盖,外部锚定把链头(连同条数/最后序号)放到另一个信任域,使整链重写无法同时自洽。三者防的是不同威胁:单点修改、删除、整链重写。
A2 超时自动放行等于把「没人看」变成「默认同意」,高风险动作会在注意力最稀缺的时候被放行,且审计上仍然显示「已批准」。如果业务不能接受拒绝,正确做法是缩短审批链、增加备份审批人、把动作拆成可逆的小步,或降级为只读/预览/排队;不能把超时改成同意。
A3 策略会变。只写「当前策略」,事后无法知道当时的阈值、规则与义务;同名版本被覆盖后更无法复现。每条决策日志要带 policy_version 与 bundle_hash,才能回答「当时按哪版策略、命中哪条规则、如果换成新策略会怎样」。版本是审计证据的一部分,不是运维元数据。
下一站:13 项目一:可沙箱执行的代码 agent 把本章的策略、审批与审计接进一个真实的执行器:工具注册、沙箱、限额、trace 与红队用例。