跳到主要内容

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
AllowedPispsPISP(支付发起服务商)被允许AllowedPispEvaluator
Budget累计消费不超预算BudgetEvaluator
AgentRecurrence重复扣款次数不超上限AgentRecurrenceEvaluator
ExecutionDate执行日在 [not_before, not_after]ExecutionDateEvaluator
PaymentReference闭支付的 transaction_id 绑对了开 checkoutPaymentReferenceEvaluator

所有评估器由一个工厂分发(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(重复扣款),则必须同时有 AmountRangeBudget,否则报违规(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-13agent_authorization.md:526-530)。

这让「人不在场」流程可以优雅降级回「人在场」,而不是硬失败。

6. 代码地图

主题文件符号名
约束引擎入口code/sdk/python/ap2/sdk/constraints.pycheck_payment_constraints check_checkout_constraints
预设字段保全code/sdk/python/ap2/sdk/constraints.pycheck_preset_payment_claims
评估器工厂code/sdk/python/ap2/sdk/constraints.pycreate_payment_evaluator create_checkout_evaluator
配货最大流code/sdk/python/ap2/sdk/max_flow_helper.pyevaluate_line_items_max_flow _dinic_sparse
支付链解析+约束code/sdk/python/ap2/sdk/payment_mandate_chain.pyPaymentMandateChain
Checkout 链code/sdk/python/ap2/sdk/checkout_mandate_chain.pyCheckoutMandateChain
收据code/sdk/python/ap2/sdk/receipt_wrapper.pyReceiptClient
稳定引用键code/sdk/python/ap2/sdk/mandate.pyget_closed_mandate_jwt
错误码docs/ap2/agent_authorization.md(Errors: unresolved_constraint …)