返回 AIPA 笔记
AIPA Day 104

策略引擎 II — 事中拦截 (每次工具调用过判定 · allow/deny/escalate→HITL)

策略引擎 II — 事中拦截 (每次工具调用过判定 · allow/deny/escalate→HITL)

2026-09-26
policy-enforcementinterceptionhitl-escalationcoverage-test

日期: 2026-09-26 阶段: Phase 4 - 自建 Agent 平台×求职冲刺 标签: #policy-enforcement #interception #hitl-escalation #coverage-test

核心问题

Day 103 把授权从命令式 if 升级成了声明式策略数据 + evaluate() 评估器。但 evaluate() 只是「判定函数」——它不会自己挡住任何调用。今天回答策略引擎的另一半:怎么让每一次工具调用,在副作用发生之前,都不可绕过地过一遍策略判定?

三个子问题:

  1. 执行器怎么挂、判定结果怎么落地? AgentCore 的工业答案是:每个工具调用在 Gateway 边界被拦截、过 Cedar 判定、得到 allow/deny——「Every agent action through AgentCore Gateway is intercepted and evaluated at the boundary outside of agent's code」(AWS, 2026-03)。但 allow/deny 二元够吗?AML 的真实需求是三态——第三态 escalate→HITL(高风险但不该直接拒、该转人工),怎么塞进执行器状态机?
  2. 判定怎么联动审计? AgentCore:「The result is a deterministic allow or deny decision, automatically recorded in the audit log... every allow or deny decision is recorded with full context」(2026-03)。判定与审计不是两件事,是一件事——每个判定原子地写一条审计
  3. 怎么证明拦截没有漏洞? 「网关不可绕过」是声明,不是事实——必须有拦截覆盖率测试证明:不存在任何一条工具调用路径能跳过 evaluate()

本篇是策略引擎 v2——事中执行器 + 三态状态机 + 覆盖率测试。Agent 注册表绑定策略集留 Day 105。

关键内容

A. 事中拦截执行器:从「判定函数」到「不可绕过的闸」

Day 103 的 evaluate() 是纯函数;执行器(enforcer)是包在工具调用外层的强制层,把判定结果翻译成对调用的实际处置。挂载点与 Day 53 一致——OAuth(Day 51)之后、工具 handler 执行之前,唯一闸门

tools/call → handleRpc
   │ [Day 51 OAuth verifyToken] token ok
   ▼
★ enforcePolicy(call, ctx) ★            ← PEP,唯一拦截点,不可绕过
   │  request = { principal: ctx.identity,
   │              action:   call.tool,
   │              resource: ctx.gatewayId,
   │              context:  { input: call.args, ...ctx.caseCtx } }
   │  decision = evaluate(request, AML_POLICIES)   ← Day 103 纯函数判定
   ▼
  三态分流(见 B)→ 无条件 writeAudit(见 C)

关键设计:request 的 context 构造evaluate() 的判定质量取决于喂进去的 context 有多全。这里有个 AgentCore 揭示的精妙顺序——interceptor 先于 policy:「The Gateway evaluates the request interceptor before the Cedar policy... use the interceptor to enrich the request context before using policy to evaluate that enriched context」(AWS, Lambda interceptors, 2026-06-01)。即先有一个上下文富化步(从 JWT 取 role/scope、从 case 取金额/跨境标记、从 Day 80 取置信档),把这些注入 context交给确定性策略评估。本项目执行器照搬这个两段式:

enforcePolicy(call, ctx):
  # ── 第一段:上下文富化(可读外部状态,非纯函数)──
  enriched = {
    input:      call.args,
    role:       ctx.identity.role,           # 从 OAuth JWT
    scope:      ctx.identity.scope,
    amountCents: ctx.caseCtx.amountCents,    # 从 case
    crossBorder: ctx.caseCtx.crossBorder,
    confidence:  ctx.caseCtx.confidenceBand, # Day 80
  }
  # ── 第二段:确定性策略评估(纯函数,Day 103)──
  decision = evaluate({principal, action: call.tool, resource, context: enriched}, AML_POLICIES)
  # ── 第三段:处置 + 审计(B/C)──
  return dispatch(decision, call, ctx)

两段分离的意义:富化可以读运行时状态(有副作用、不可测 deep-equal),但评估必须是纯函数(确定性、可 deep-equal 测)。把不纯的部分(读 JWT/case)隔离在富化段,评估段保持 Day 103 的确定性契约——这样策略评估永远可被单测穷举,富化段单独 mock 即可。

B. 三态状态机:allow / deny / escalate→HITL

AgentCore Policy 原生是二元 allow/deny(Cedar 只有 permit/forbid)。但 AML 需要第三态——escalate:策略判定「这个动作风险太高,不能自动放行,但也不该直接拒,转人工同步批准」。这正是 Day 81 SYNC_APPROVE 的策略化表达。怎么在「Cedar 只有 permit/forbid」的约束下做出三态?

两种工程实现,本项目选后者

方案做法取舍
富化 Cedar(AgentCore 路线)Cedar 判 allow/deny;deny 后由 Lambda interceptor 返回结构化 MCP error 或转人工流(AWS 2026-06)三态逻辑散在 interceptor 代码里,策略本身仍二元
策略带 effect 扩展(本项目 v1)DSL 的 effect 扩成 permit/forbid/escalate;评估器三态返回三态显式在策略数据里,合规官直接声明「这条转人审」

本项目执行器状态机(dispatch):

                   evaluate() → decision
                          │
        ┌─────────────────┼──────────────────┐
     ALLOW              DENY              ESCALATE
        │                 │                   │
   执行工具 handler   不执行            置「待人审」暂停态
   (hybridSearch…)   返回结构化         (非错误,是 pending)
        │            MCP error          挂进 HITL 队列
        │            (403-like)              │
        │                 │            ┌─────┴─────┐
        │                 │         人工 approve  人工 reject
        │                 │            │           │
        │                 │       执行 handler   退回 + 记 override
        │                 │            │           │
        └────────┬────────┴────────────┴───────────┘
                 ▼
        ★ writeAudit(call, decision, outcome) ★   ← 无条件(C 节)

三态的语义对齐 Day 81 三档处置(AUTO/SYNC_APPROVE 是 ALLOW/ESCALATE;DENY 是 Day 81 没有的「硬拒」,对应监管红线 forbid):

  • ALLOW:策略命中 permit 且无 forbid/escalate → 工具 handler 执行。对应 Day 81 的 AUTO(低风险)。
  • DENY:策略命中 forbid(监管红线,如 junior 提交 SAR)→ 不执行,返回结构化 MCP error(仿 AgentCore「return a structured MCP error for unauthorized calls」,2026-06),agent 收到的是「明确拒绝」而非崩溃。
  • ESCALATE:策略命中 escalate(高风险但非红线,如提交 SAR、跨境大额案)→ 置 pending 暂停态,挂 HITL 队列,阻断 agent 推进直到人工裁决。批准则执行、退回则记 override。对应 Day 81 的 SYNC_APPROVE。

反直觉洞察①(escalate 不是 deny 的弱化版,是与 deny 正交的第三态——把它做成「deny + 重试」会酿成合规事故):工程师容易想「escalate 就是先 deny、让人去开个口子再重试」,于是实现成「拒绝 → agent 收到错误 → agent 重新规划 → 换个说法再调一次」。这在 AML 里是灾难:(1) agent 被 prompt 注入诱导时,「deny+重试」给了它反复试探、措辞绕过的机会(Day 52 证明强模型更会「讲道理」绕约束);(2) escalate 的本质是把控制权移交人类并冻结 agent,不是「让 agent 再想想」。正确实现:escalate 进入一个agent 完全失去主动权的 pending 态——它既不能重试、也不能撤回、只能等人。AgentCore Lambda interceptor 的「act-on-behalf token exchange」也是这个哲学:把高敏动作的权限移交给一个外部确定性流程,而非留给 agent 自己博弈。deny 和 escalate 必须是两条不同的边,指向两个不同的终态。

C. 判定联动审计:每个判定原子写一条,三态全留痕

AgentCore 的口径是判定即审计——「The result is a deterministic allow or deny decision, automatically recorded in the audit log」「every allow or deny decision is recorded with full context」(2026-03)。本项目把它做成:dispatch 的每条出口,无论 ALLOW/DENY/ESCALATE,都在处置的同一步原子写一条审计,复用 Day 75 的不可篡改哈希链审计轨迹(auditTrail.ts)。

审计条目结构(呼应 Day 81 AuthDecisionRecord,但补「策略命中」字段):

PolicyDecisionRecord {
  seq,                          # 逻辑序号(Day 75 哈希链,不读时钟)
  action, principal, resource,  # 四元组定位
  enrichedContext,              # A 节富化后的 context 快照(判定输入可复现)
  decision: ALLOW|DENY|ESCALATE,
  matchedPolicyId,              # 命中哪条策略(可追到 annotation 监管依据)
  annotation,                   # 该策略的监管依据(如「BSA CTR」),可读
  # ESCALATE 专属(人裁决后回填):
  outcome?: approved|rejected, approver?, approverEdits?,
}

为什么 ALLOW 也要带 matchedPolicyId——监管事后审查问的不是「你拦了什么」,是「你凭哪条规则放行了这个?」(Day 81 反直觉的延续)。一份后来出问题的 SAR 若当初走了 ALLOW,必须能追到「是 POL-SAR-DRAFT-LOW 这条 permit 放的,其 annotation 写明依据」——没有 matchedPolicyId,自动放行就无法自证是合规判断而非系统漏判。Cedar 设计天然支持这点:判定能精确归因到命中的那条 policy。

反直觉洞察②(拦截的可信度上限 = 审计的不可篡改性,不是判定的正确性):直觉认为「策略引擎的价值在判得准」。但在合规语境里,一个判错但留痕完整、可追溯、可复现的系统,远胜一个判得更准但留痕缺失的系统。原因:监管审查(SR 11-7 三道防线、DORA 可重建性)审的是「过程可问责」,不是「结果零错」——AML 本就不存在零误报。所以执行器的工程重心,不是把 evaluate() 调到 100% 准(不可能),而是保证「每个判定都原子地、不可篡改地、可复现地落审计」。这就是为什么 C 节把审计写进 dispatch 的每条出口、用 Day 75 哈希链(改一条后续全部校验失败)、并快照 enrichedContext(保证判定可离线复现)。判定可以将来被更好的策略改进;但当时凭什么判这件事,必须当时就钉死、永不可改。

D. 拦截覆盖率测试:证明「不可绕过」是事实而非声明

「网关是唯一闸门、不可绕过」是 Day 53 起反复说的——但这是断言。P4 平台尺度必须把它变成可执行的回归测试。覆盖率测试要证两件事:

  1. 无旁路:不存在任何工具调用入口跳过 enforcePolicy。测法——遍历 toolRegistry 注册的全部工具,对每个工具构造调用,断言其执行路径必然经过 enforcePolicy(用 spy/计数器:每次工具 handler 被调用前,enforcePolicy 的调用计数必 +1;二者严格 1:1)。
  2. 判定全覆盖 + 红线必拒:对每条策略,构造命中它的 request 和差一点不命中的 request(边界测试),断言判定符合预期;特别是红线 forbid 必须在所有相关路径上拒绝(如 junior 角色对 submit_sar 的所有调用,无论参数怎么变,恒 DENY)。
test('拦截不可绕过:每个工具调用必过 enforcePolicy'):
  for tool in toolRegistry.list():
    spy = wrap(enforcePolicy)
    handlerSpy = wrap(tool.handler)
    callTool(tool.name, validArgs)
    assert spy.calledBefore(handlerSpy)         # 顺序:闸在 handler 前
    assert spy.callCount == handlerSpy.callCount  # 1:1,无旁路

test('红线 forbid 在全参数空间恒拒'):
  for args in boundaryArgsSpace('submit_sar'):
    d = evaluate({principal: junior, action: 'submit_sar', context: enrich(args)}, AML_POLICIES)
    assert d.decision == DENY                    # 无论 args 怎么变

这把 Day 52 的 ASR 基线思路(防御有效要可量化)延伸到了授权层:拦截覆盖率本身是一个可断言、可回归的数字——新增一个工具却忘了让它过闸,覆盖率测试当场红。

设计要点/决策表

要点决策理由
拦截点OAuth 后、handler 前,唯一闸,不可绕过沿用 Day 53;覆盖率测试钉死无旁路(D)
两段式上下文富化(不纯)→ 策略评估(纯函数)隔离副作用,评估段保持确定性可测(A)
判定态三态 ALLOW/DENY/ESCALATE,非二元AML 需「转人审」第三态(Day 81 SYNC_APPROVE 策略化)
escalate 实现冻结 agent 的 pending 态,非 deny+重试防试探绕过,控制权移交人类(反洞察①)
deny 形态返回结构化 MCP error,不崩溃仿 AgentCore,agent 收到明确拒绝可优雅处理
审计三态全留痕、原子写、含 matchedPolicyId+context 快照判定即审计;可归因、可复现(C,反洞察②)
审计底座复用 Day 75 哈希链不可篡改轨迹改一条后续全失败,监管可问责
不可绕过验证拦截覆盖率测试(1:1 spy + 红线全空间拒)把「不可绕过」从断言变成回归测试(D)

对本项目的落地

  • 新建 src/agent/policy/enforcer.ts:导出 enforcePolicy(call, ctx): Promise<EnforceResult>(A 节两段式:enrichContext() 富化 + 调 Day 103 evaluate())、dispatch(decision, call, ctx)(B 节三态状态机,ESCALATE 返回 { status: 'pending', queueId } 而非执行)、PolicyDecisionRecord 类型(C 节)。enrichContext 是唯一读运行时状态的不纯函数,单独 mock 测;evaluate 段沿用 Day 103 纯函数契约。
  • 挂进 MCP 调用链,替换 Day 53 命令式 PDP:在 src/agent/mcp/server.ts(或 toolRegistrycall())的 OAuth 之后、handler 之前插 enforcePolicy——网关 PEP 骨架(Day 53)保留,把内部命令式 if 判定换成「evaluate + 三态 dispatch」。HIGH_RISK_TOOLSsubmit_sar/draft_sar_batch)对应的 escalate 策略命中即走 pending。
  • 审计接 Day 75 轨迹dispatch 三条出口无条件 appendAudit(toPolicyRecord(decision, call, outcome)) 写入 src/aml/auditTrail.ts 的哈希链;enrichedContext 快照入条目,保证判定离线可复现;ESCALATE 的人裁决(approve/reject/edits)后回填同一条(追加新事件,非改旧条——哈希链 append-only)。
  • HITL 暂停态接 UI:ESCALATE 的 pending 在 src/components/aml/AmlSarPanel.tsx 的 HITL 区呈「待批准」卡片(动作 + matchedPolicy annotation + 富化 context),批准/退回写回 outcome;串 Day 80——LOW 置信段对应动作天然命中 escalate 策略,与已置顶的「待重点复核」同队列。
  • 拦截覆盖率测试:新建 src/agent/policy/__tests__/coverage.test.ts(D 节)——遍历 toolRegistry 全工具断言 enforcePolicy 1:1 前置于 handler(无旁路),对红线 forbid 在边界参数空间断言恒 DENY。这是把「网关不可绕过」做成 CI 可回归的数字(呼应 Day 19 阻断式 CI gate)。
  • 诚实标注enforcer.ts 头注写明——三态 escalate 是本项目对 Cedar 二元 permit/forbid 的扩展(AgentCore 原生用 Lambda interceptor 实现类似效果);pending 暂停态在静态仓库表现为「状态标记 + 阻断 agent 推进」,真实工单系统/异步审批流对接为接后端后续;enrichContext 读 case/JWT 为 mock,真实 OAuth/case 服务接入留 P4 后续。

参考资料

  1. AWS — Policy in Amazon Bedrock AgentCore: Control Agent-to-Tool Interactions:「Every agent action through AgentCore Gateway is intercepted and evaluated at the boundary outside of agent's code」「deterministic allow or deny... automatically recorded in the audit log」「every allow or deny decision recorded with full context once CloudWatch log delivery enabled」 (2026-03)
  2. AWS ML Blog — Secure AI agents with Policy and Lambda interceptors in Amazon Bedrock AgentCore gateway:「Gateway evaluates the request interceptor before the Cedar policy」(富化先于评估);interceptor 可 token exchange/payload 改写/响应过滤;「return a structured MCP error for unauthorized calls」;Policy 自动 CloudWatch 日志 vs interceptor 手动埋点 (2026-06-01)
  3. AWS — AgentCore Policy 延迟数据:Cedar 评估约 50-150ms 开销,对延迟敏感应用需权衡 (2026)
  4. 本仓库 src/agent/policy/policyEngine.ts(Day 103 evaluate 纯函数判定)、src/agent/mcp/riskGateway.ts(Day 53 PEP 骨架/挂载点)、src/aml/authorization.ts(Day 81 三档处置 AUTO/SYNC_APPROVE)、src/aml/auditTrail.ts(Day 75 哈希链不可篡改轨迹)、src/aml/confidence.ts(Day 80 置信档→escalate)、src/components/aml/AmlSarPanel.tsx(HITL 区)、src/agent/mcp/toolRegistry.ts(覆盖率测试遍历对象)(2026-06)

SOTA 检查 (2026-06-11)

  • 「网关边界拦截每次工具调用、判定即审计」是 2026-06 的 agent 治理 SOTA:AgentCore Policy(GA 2026-03-03)把它做成产品——边界拦截、Cedar 确定性判定、allow/deny 自动入 CloudWatch 审计。本项目执行器方向与之对齐,且补了 AML 需要的 escalate 第三态。
  • 三态(含 escalate→HITL)是合规域必需、纯二元不够:Cedar/AgentCore 原生二元,靠 Lambda interceptor 补人工流;本项目把 escalate 显式进策略 effect,让合规官直接声明「这条转人审」——更适合 AML(监管规则本就分「禁止/放行/需复核」三类)。2026-06 未见把三态判定标准化的公开规范,这是本项目可展示的设计点(须诚实标注为自有扩展)。
  • 拦截延迟是真实工程约束:AgentCore Cedar 评估 ~50-150ms(本日来源),「对延迟敏感应用是问题」。本项目 evaluate 是进程内纯函数(μs 级),无此问题;但接真实 Cedar/远程 PDP 时须评估缓存——注意授权判定缓存有风险(context 变即失效),Day 45 语义缓存思路不能直接套用,缓存键须含完整 enrichedContext。
  • 「判定可复现」正成监管硬要求:2026 监管从「控制存在」转向「控制有效性」(Day 81 SOTA 已记 Wolters Kluwer 口径)——这要求审计不仅记「判了什么」,还要能复现「凭什么判」。本项目快照 enrichedContext 入审计正是为此;这比早期「只记 yes/no」的审计更前瞻。
  • 过时认知警示:「deny 后让 agent 重试换路径」是危险的过时实现——给了被注入 agent 试探绕过的机会(反洞察①);escalate 必须是冻结 agent、移交人类的终态。另:「只记拦截、不记放行」过时——监管问「为何没拦」,ALLOW 也须带 matchedPolicyId 留痕(反洞察②)。
  • 待跟踪:Day 105 把策略集绑定到 Agent 注册表后,复核「不同 Agent 不同策略集」时覆盖率测试是否需按 Agent 版本分别跑;以及 AgentCore Evaluations(GA,13 评估器)是否提供「策略拦截有效性」的现成评估器可对标本项目覆盖率测试。