数据截至 (上游 commit e1ea56db72a6)
AP2 — 约束评估与收据(护栏与追责)
上一章把「链」验通了——签名都对、链没断。但还差最后一步:闭 mandate 里的真实成交,有没有越过开 mandate 当初设的规则? 这一章讲约束怎么逐条评估,以及结算后的收据怎么把一切绑回授权。
1. 约束:agent 自主权的护栏
它要解决的小问题: 用户签的开 mandate 说「200 刀以内、就这家鞋店」。agent 自己签的闭 mandate 说「付 $179.99 给 Cat Store」。验证方怎么机械地确认「闭落在开里」?
思路: 一种约束类型 = 一个评估器类,每个评估器 evaluate(闭 mandate) 返回违规消息列表(空列表=通过)。这是个干净的策略模式(code/sdk/python/ap2/sdk/constraints.py:69-78)。
支付侧约束一览
| 约束类型 | 检查什么 | 评估器 |
|---|---|---|
AmountRange | 币种匹配 + 金额在 [min,max] | AmountRangeEvaluator |
AllowedPayees | 收款商家在白名单内 | AllowedPayeeEvaluator |
AllowedPaymentInstruments | 用的支付工具被允许 | AllowedPaymentInstrumentEvaluator |
AllowedPisps | PISP(支付发起服务商)被允许 | AllowedPispEvaluator |
Budget | 累计消费不超预算 | BudgetEvaluator |
AgentRecurrence | 重复扣款次数不超上限 | AgentRecurrenceEvaluator |
ExecutionDate | 执行日在 [not_before, not_after] | ExecutionDateEvaluator |
PaymentReference | 闭支付的 transaction_id 绑对了开 checkout | PaymentReferenceEvaluator |
所有评估器由一个工厂分发(code/sdk/python/ap2/sdk/constraints.py:348-378 create_payment_evaluator),check_payment_constraints 把它们串起来跑(constraints.py:489-541)。
两个值得看的细节
1. 预设字段必须原样保留。 如果开 mandate 已经写死了某字段(比如 payee),闭 mandate 必须一字不差地带同一个值,否则违规(code/sdk/python/ap2/sdk/constraints.py:433-486 check_preset_payment_claims)。这堵住了「开 mandate 留空、agent 闭 mandate 随便填」的漏洞。
2. 跨约束的依赖校验。 如果有 AgentRecurrence(重复扣款),则必须同时有 AmountRange 和 Budget,否则报违规(constraints.py:514-533)。直觉:你授权 agent「每周自动扣一次」,就必须同时给「单笔上限」和「累计上限」,否则一个失控的重复扣款能掏空账户。
3. 未知约束=失败。 规范明令:验证方碰到不认识的约束类型必须当作评估失败(agent_authorization.md:463-465),工厂里也确实对未知类型直接 raise(constraints.py:378)。这是「默认拒绝」的安全姿态。
2. 巧妙之处:用最大流做商品配货匹配
Checkout 侧最有意思的约束是 LineItems:开 mandate 说「我要 2 件鞋(A 或 B 都行)+ 1 件袜子(只能 C)」,闭 checkout 里有一堆真实 SKU。怎么判断这堆 SKU 能不能恰好填满所有需求槽?
这是个经典的二部图匹配 / 指派问题——而 AP2 真的用最大流来解(code/sdk/python/ap2/sdk/max_flow_helper.py:14 evaluate_line_items_max_flow)。
SKU 侧 需求槽侧
┌──────────────┐ ┌──────────────────┐
│ source │cap=数量 cap=需求数量│ sink │
└───┬────┘ │ 允许匹配则连边(cap=∞) │ ▲ └──────┘
▼ ▼ ───────────────────────▶ │
[SKU_A] ───────────────────▶ [需求1: 鞋×2]──┤
[SKU_C] ───────────────────▶ [需求2: 袜×1]──┘
怎么读这张图: source 给每个 SKU 灌「购物车里该 SKU 的数量」的流;SKU 只能流向「接受它的需求槽」;每个需求槽到 sink 的容量是「该需求要的件数」。如果最大流 = 购物车总件数,就说明每件货都被指派进了某个合法槽;否则有货指派不掉 = 违规。
实现里有两个务实优化(max_flow_helper.py:64-121):
- 先做贪心消元:只匹配一个需求的 SKU(degree-1)直接指派掉,只把「能匹配多个需求」的复杂 SKU 丢进真正的最大流(
max_flow_helper.py:64-91)。 - 稀疏图 + Dinic:邻接用
list[dict]而非邻接矩阵,默认跑 Dinic 算法(也支持 Edmonds-Karp)(max_flow_helper.py:124-191,243)。
为什么不用简单的「逐项打勾」?因为当「一个 SKU 能满足多个需求、一个需求接受多个 SKU」时,贪心会误判——必须全局求最优指派。用最大流是正确性的选择,不是炫技。
3. 把约束接回链:PaymentMandateChain
上一章 verify_chain 吐出的是「每跳的有效 payload 列表」。PaymentMandateChain.parse 把它解析成强类型的「(开 mandate, 闭 mandate)」对(code/sdk/python/ap2/sdk/payment_mandate_chain.py:25-36),然后 .verify() 调约束引擎并额外核对 transaction_id(payment_mandate_chain.py:38-93)。
# 示意,非源码:验证方拿到链之后的完整两步
payloads = client.verify(chain, key_or_provider=lookup_root, expected_aud="merchant", expected_nonce="tx_abc")
parsed = PaymentMandateChain.parse(payloads) # 拆成 (open, closed)
violations = parsed.verify(expected_transaction_id="tx_abc") # 跑约束 + 核 tx_id
assert not violations # 空 = 全部通过
两层验证别混淆: 第一层(verify_chain)验密码学完整性(签名、绑定、cnf 链);第二层(PaymentMandateChain.verify)验业务约束(闭是否落在开的规则内)。两层都过才算授权成 立。
4. 收据:把结算绑回授权
它要解决的小问题: 交易成了,事后纠纷怎么证明「这笔结算对应的正是那份授权」?
答案——一个稳定的引用键:
# code/sdk/python/ap2/sdk/README.md:104-109
reference = compute_sha256_b64url(MandateClient().get_closed_mandate_jwt(chain))
get_closed_mandate_jwt 取链的叶子 JWT(最后一个 ~~ 段、~ 之前那截)(code/sdk/python/ap2/sdk/mandate.py:402-417)。它的 SHA-256 就是收据的 reference。
巧妙处:这个 reference 对「链有多深」「揭了哪些披露」都不敏感(
mandate.py:411-414注释)。无论中间转授权了几跳、agent 选择露出多少字段,叶子闭 mandate JWT 始终是同一串,所以收据永远绑得回那份授权。这正是「可追责」落地的支点。
ReceiptClient 据此创建/验证收据(code/sdk/python/ap2/sdk/receipt_wrapper.py:18):
create_payment_receipt/create_checkout_receipt:造收据模型,issuer 从闭 mandate 的 PISP 继承(receipt_wrapper.py:44-98)。verify_receipt:两步——(1) 验 ES256 签名;(2) 通过回调确认reference指向一个已知的闭 mandate(receipt_wrapper.py:100-161)。
规范侧对应的是 Mandate Receipt 的 result: success|error + reference(agent_authorization.md:503-516)。收到 error 收据时,agent 才被允许出示下一份开 mandate(防双花,见第 01 章)。
5. 出错时的回退信号
约束验不过不一定是终止错误。规范定义了几个 action authorization 错误码(agent_authorization.md:521-535),其中 unresolved_constraint 很关键:
当验证方碰到不认识的约束、或无法确认闭 mandate 满足约束时返回
unresolved_constraint——这可以作为回退信号:把用户拉回环里,改走「人在场直接批闭 mandate」或非 agentic 流程(flows.md:11-13、agent_authorization.md:526-530)。
这让「人不在场」流程可以优雅降级回「人在场」,而不是硬失败。
6. 代码地图
| 主题 | 文件 | 符号名 |
|---|---|---|
| 约束引擎入口 | code/sdk/python/ap2/sdk/constraints.py | check_payment_constraints check_checkout_constraints |
| 预设字段保全 | code/sdk/python/ap2/sdk/constraints.py | check_preset_payment_claims |
| 评估器工厂 | code/sdk/python/ap2/sdk/constraints.py | create_payment_evaluator create_checkout_evaluator |
| 配货最大流 | code/sdk/python/ap2/sdk/max_flow_helper.py | evaluate_line_items_max_flow _dinic_sparse |
| 支付链解析+约束 | code/sdk/python/ap2/sdk/payment_mandate_chain.py | PaymentMandateChain |
| Checkout 链 | code/sdk/python/ap2/sdk/checkout_mandate_chain.py | CheckoutMandateChain |
| 收据 | code/sdk/python/ap2/sdk/receipt_wrapper.py | ReceiptClient |
| 稳定引用键 | code/sdk/python/ap2/sdk/mandate.py | get_closed_mandate_jwt |
| 错误码 | docs/ap2/agent_authorization.md | (Errors: unresolved_constraint …) |