agent 代你刷卡,憑什麼證明是你同意的:讀 AP2 的 Mandate 授權鏈
補寫說明:這篇是 2026 年 9 月 2 日補寫的,日期仍掛回原本那一天。文中對 AP2 的描述,一律以動筆當天的官方規格內容為準。
Claude Code 跳出權限視窗,問你要不要讓它跑某個指令,你按了允許。那個「允許」存在哪裡?
存在那一秒。視窗關掉,同意就沒了,留下來的是一行 log:某個時間、某個工具、某個參數。真的有人事後來問「這誰批准的」,你能拿出來的只有那行 log,加上你自己的記憶。
這套做法撐得住,是因為代價一直很小。跑錯指令頂多重跑,改壞檔案還有 git 兜著。等到有人要讓 agent 拿你的卡去結帳,它就不夠了。
先講清楚我讀到哪:這篇從頭到尾是讀規格。docs/ap2/specification.md 我逐段對過原文,但沒裝過 SDK、沒跑過任何一個 scenario、也沒有生出過一份真的 Mandate JWT。所以下面每一句都是「規格是這樣寫的」,不是「我試出來是這樣」。repo 是 google-agentic-commerce/AP2,Apache-2.0,最後一次 push 在 2026 年 6 月 17 日,規格檔第一行自稱 Agentic Payment Protocol (v0.2)。它不是本週熱門,是一份已經定到 v0.2 的東西。
同意從一個動作,變成一份文件
彈窗那套的同意是動作:你按下去,程式繼續往下跑,狀態機前進一格。同意本身沒有實體,沒有內容,也不能被別人檢查。
AP2 走的是另一條路。使用者的同意被寫成 Mandate,一份帶簽章的文件,而且一次拆成兩份。Checkout Mandate 管 what is being purchased,Payment Mandate 管那筆 Checkout 的 payment,兩份各自還有對應的 Receipt。買什麼跟怎麼付這兩件事在規格裡是分開簽的。
這個切法一開始看起來像過度設計。你買一杯咖啡,為什麼要兩份文件?直到你想清楚「爭議」發生時要拿什麼出來:買錯東西跟被多刷一筆,是兩種完全不同的糾紛,一份混在一起的同意書沒辦法回答第二種。
一個 agent 全包,變成五個角色各簽各的
規格列了五個角色:Shopping Agent、Credential Provider、Merchant、Merchant Payment Processor、Trusted Surface。買東西的、拿憑證的、賣東西的、處理款項的、給人看畫面確認的,各自一格。
規格自己補了一句:a single entity to play multiple (or even all) of the roles。同一個實體可以扮演多個、甚至全部角色。
那切這麼細幹嘛?因為角色是責任的單位,不是部署的單位。你可以全部塞在同一個服務裡跑,但出事的時候,「是誰在這一步簽的名」這個問題有明確答案。
人不在場的時候,同意要事先長成什麼形狀
Human Present 好處理:人看著、人按確認,跟彈窗差不多。Human Not Present 才是真問題。規格是這樣寫的:使用者看見並核准的是一組約束(a set of constraints over what closed Checkout and Payment would meet their intent),Shopping Agent 之後拿著這些 open mandate,自己組出並核准實際成交的那份 closed Checkout 和 Payment Mandate。
open mandate 帶約束,closed mandate 是真的成交那一份。你同意的不是某一次購買,是一個購買的形狀。
停一下想想你自己每天在幹嘛。--dangerously-skip-permissions、hooks 自動放行、排程半夜無人值守跑一串 subagent,這些全部都是同一件事:你不在場,但你事先同意了一組約束。差別只在於,你那組約束寫在設定檔裡,沒有簽章、沒有有效期、沒有 receipt,而且沒有任何機制檢查 agent 真的有待在約束裡面。
規格把這件事寫成文件格式,我們把它寫成 settings.json。
綁定不靠信任,靠 hash——順帶一條規格級的死路
Payment Mandate 怎麼確定它綁的是「這一份」Checkout,而不是另一份?用 Checkout JWT 的密碼學雜湊。
然後規格接了一句很少見的話:為了防止彩虹表攻擊,Checkout JWT 必須用具備隨機性的數位簽章方案(例如 ECDSA)簽,不可以用確定性的簽章方案(例如 Ed25519)。
Ed25519 在其他場合幾乎是預設推薦選項,快、實作乾淨、不容易踩到隨機數的雷。但它是確定性的:同樣的內容、同樣的金鑰,簽出來永遠一模一樣。當你要簽的東西是一份結構高度固定、欄位可枚舉的購物明細,這個性質就變成攻擊面,別人可以預先算好一整張表來反推。一條在別處是最佳實務的選擇,在這裡是明文禁止的死路。
寫規格的人願意把這種細節寫進 MUST NOT,通常代表他們真的踩過或推演過。這比整份文件的架構圖更能說明它的成色。
版本號要逐字比對,不接受「差不多」
Mandate 的型別與版本用 vct claim 表示,例如 mandate.payment.1、mandate.checkout.open.1。規格要求實作端必須完全比對整串 vct 字串,含版本後綴。
這條讀起來很無聊,但它擋掉的是一整類最難查的 bug。你只要允許「前綴對就好」這種寬鬆比對,某一天新舊版本混跑,收單端會很開心地用舊版的欄位理解去解析新版的文件,然後在一個跟版本完全無關的地方炸掉。
規格另外標明它跟 A2A、x402 是接得起來的:sample 目錄下確實有 a2a/human-present/cards/ 跟 human-not-present/x402/ 兩條路徑。上個月寫過 A2A 那篇談的是「agent 中途想反問你,協定裡沒有那個位置」,這份規格接的正好是它的下一步:當 agent 根本不能反問你的時候,同意要長什麼形狀。
我的看法,以及什麼會讓我改口
彈窗那套權限模型是被支付逼到極限的,不是被 AI 逼的。它同時缺兩件東西:事前的約束沒有形狀,事後的憑據沒有實體。AP2 兩邊都補了,open mandate 補前者,Receipt 補後者。我認為 Claude Code 這類工具的權限模型遲早要往這個方向長,不是因為大家要用它買東西,是因為「無人值守跑一整晚」這件事已經是日常了,而我們對那一整晚發生過什麼的說明能力,跟三年前一模一樣。
會讓我改口的條件也很具體:如果 open mandate 的約束語言最後複雜到沒有人寫得對,那它就只是把責任從一個沒人細看的彈窗,搬到一份沒人細讀的合約,換了位置而已。這一點我完全沒有證據,因為我沒讀過 checkout_mandate.md、payment_mandate.md、agent_authorization.md、flows.md、security_and_privacy_considerations.md 這五份細節文件,只從 specification.md 的連結知道它們存在。欄位長什麼樣、約束能寫到多細,我不知道。
我也完全沒查商家與銀行的實際採用狀況,所以這篇不談落地率。
真正被換掉的不是流程,是「同意」這個字的意思。以前它是一個時間點,一個你按下去就過去了的瞬間;現在它是一份有內容、有範圍、有期限、事後可以拿出來對帳的東西。










