AIエージェントが商品を探し、購入する時代が近づいています。
でも、エージェントから「この商品を購入してよい」という署名付きデータが届いたとして、本当にそのまま購入処理を進めてよいのでしょうか?
例えば、次のような問題があります。
- 署名は正しいが、購入金額が許可範囲を超えている
- 正しい委任情報だが、購入先のMerchantが異なる
- 委任は有効だったが、すでに期限が切れている
- 一度使われた購入委任が、もう一度送られてくる
こうした問題を考えると、署名が正しいことと、購入を許可してよいことは別だと分かります。
今回は、この問題をRubyから扱うために開発したOSS「AP2 Mandate Verifier」を紹介します。
AP2とは?
AP2(Agentic Payment Protocol)は、AIエージェントによる購入や支払いの認可を安全に扱うためのプロトコルです。
公式仕様 v0.2では、主に次の2種類のMandateが定義されています。
- Checkout Mandate:エージェントが特定の購入内容を実行する権限を示す
- Payment Mandate:その購入に対する支払い権限を示す
例えば、ユーザーがAIエージェントに「この商品を予算内で買って」と依頼したとします。
Merchant側では、エージェントが提示した委任情報について、署名だけでなく、購入商品・金額・委任範囲などを確認する必要があります。
ここで重要なのがMandateの検証です。
Ruby用の検証Gemを作りました
ap2-mandate-verifierは、AP2のCheckout Mandateを検証するためのRuby Gemです。
Railsには依存していません。
2026年10月に、固定profile対応の最初の安定版 0.1.0 をRubyGemsへ公開しました。
gem "ap2_mandate_verifier", "= 0.1.0"
このGemは、対応する固定profileの範囲で、次のような検証を行います。
- 信頼情報と検証対象の整合性
- Issuerの署名
- 選択的開示(Selective Disclosure)の完全性
- Agentへの委任と署名
- Merchantの署名
- 購入内容とHost側注文情報の結び付き
- 購入制約と有効期限
検証が成立すると、確認できた事実をEvidenceとして返します。
ただし、現行版はAP2全体に対応する汎用SDKではありません。
対応範囲は ap2.checkout.single_merchant_sku v1という固定profileと、その7つの支持形状に限定しています。実Issuerとの相互運用や本番決済まで保証しているわけではありません。
正確な対応条件はSUPPORT_CONTRACT.mdにまとめています。
実際にRubyから検証する
公開済みの 0.1.0 を使った最小例です。
今回の例は、公開リポジトリに保存されている合成テストデータを使用します。実際の購入や支払いは発生しません。
1. Gemをインストール
現行版の検証済みベースラインはMRI Ruby 4.0.9 と固定依存です。対応OS・native providerにも条件があります。
適合する環境で以下を実行します。
gem install ap2_mandate_verifier --version 0.1.0
2. 合成テストデータを取得
次のコマンドで、固定コミットに保存された入力データを取得します。
curl --fail --location \
--proto '=https' --proto-redir '=https' \
-o AP2_RESEARCH_INPUTS.json \
https://raw.githubusercontent.com/cuichangquan/ap2-mandate-verifier/f6d4f83d396b75505256dba5769a54a8f065eeea/research/stage15/results/R3/INPUTS.json
入力データのSHA256は、次のコードで検証します。
3. Rubyコードを書く
verify_example.rb を作成します。
require "ap2_mandate_verifier"
require "json"
require "digest"
raw = File.binread("AP2_RESEARCH_INPUTS.json")
expected_sha256 =
"41ad845a7ea82a9c7316c9ef11fb8ee5281453afafe2b4932c7217c1936562c1"
abort "Fixture mismatch" unless
Digest::SHA256.hexdigest(raw) == expected_sha256
full = JSON.parse(raw).fetch("FULL")
result = AP2MandateVerifier.verify_research(
trust_json: full.fetch("trust_json"),
selection_json: full.fetch("selection_json"),
host_json: full.fetch("host_json"),
root_presentation: full.fetch("root_presentation"),
terminal_jws: full.fetch("terminal_jws"),
host_checkout_jws: full.fetch("host_checkout_jws"),
verified_at: full.fetch("verified_at")
)
puts "status=#{result.status}"
puts "code=#{result.code}"
abort "Verification did not succeed" unless result.success?
puts "evidence_present=#{!result.evidence.nil?}"
実行します。
ruby verify_example.rb
保存済みの正常ケースでは、次の結果が期待されます。
status=verified_research
code=scoped_research_verified
evidence_present=true
verified_at には、合成データで固定されたUnix時刻 1800000000 を使用しています。現在時刻や実際の注文として扱うものではありません。
なお、verified_research という名前は研究段階から維持している既存API契約です。0.1.0 が安定版であっても、対応範囲が広がったことや本番運用可能であることを意味しません。
改ざんや期限切れではどうなる?
正常系だけでは検証器の動作を理解しにくいため、実際のGemを呼び出すRailsデモも公開しています。
デモには3つのシナリオがあります。
| シナリオ | Gemの結果 | Evidence |
|---|---|---|
| 正常な購入内容 | scoped_research_verified |
あり |
| 購入内容を改ざん | host_binding_mismatch |
なし |
| 有効期限切れ | invalid_purchase_time |
なし |
改ざんケースでは、保存済みの署名とHost側の購入情報を維持したまま、Checkout JWT内の商品の単価を変更します。
その結果、購入内容の結び付きが一致せず、検証が拒否されます。
期限切れケースでは、合成の評価時刻をMandateの有効期限後に変更します。
この場合も成功にはなりません。
デモは単なる成功・失敗のモックではなく、公開済みのGem 0.1.0を呼んで結果を表示しています。
ローカルDockerでの起動手順はデモリポジトリのREADMEを参照してください。対応architectureやfresh buildの検証状況など、記載された制約を確認して利用してください。
画面を使った詳しい説明は、Zennの記事にもまとめています。
一番重要なこと:検証成功≠購入許可
このGemを設計するうえで、特に意識したのが責任の分離です。
Verifierが検証に成功しても、それだけで購入を実行してはいけません。
なぜでしょうか?
例えば、署名や購入制約が正しい場合でも、次のような問題は残ります。
- Issuerの信頼登録が後から取り消されている
- 注文内容が検証後に変更されている
- 同じnonceがすでに消費されている
- 複数のリクエストが同じ購入を同時に実行しようとしている
これらは、アプリケーション側で扱う必要があります。
責任を整理すると、次のようになります。
| Verifier(Gem) | Host(Railsアプリなど) |
|---|---|
| 署名と委任の検証 | 信頼情報の真正な取得と最新性管理 |
| 対応する購入制約の検証 | 注文の最新状態の確認 |
| 署名対象と購入情報の結び付き | nonceの原子的な一度限りの消費 |
| Evidenceの生成 | 注文の排他的な更新・最終購入判断 |
Gemは、受け取った信頼情報や注文情報との整合性を検証します。
一方、それらの情報が本当に信頼できる最新の状態なのか、すでに使われた委任ではないか、といった運用上の判断はHostの責任です。
この分離によって、Railsなどのアプリケーションは検証器の結果を利用しながらも、最終的な業務判断を独立して行えます。
逆に言えば、次のような実装は危険です。
# NG: 検証成功だけを購入許可として扱う
if result.success?
# ここで無条件に購入処理を実行するのは危険
end
success? は対応範囲の検証が成功したことを示すもので、購入権限の最終承認ではありません。
今後の方向性
現在のGemは、対応範囲を限定したCheckout Mandateの検証専用です。
将来的には、Ruby開発者がAP2の複雑な署名・委任処理を自作しなくても済むよう、対応profileを慎重に広げ、発行や提示生成などを扱うSDKへ発展させたいと考えています。
ただし、最初からすべてのAP2機能を実装することよりも、まずは対応範囲が明確で、安全性の前提と限界を確認できるRuby Gemを目指しています。
Ruby/RailsでAP2を扱いたい方や、Mandateの検証・API設計に興味がある方から、フィードバックをいただけると嬉しいです。
不具合や改善案は、GitHub Issuesで受け付けています。セキュリティ上の問題は公開Issueではなく、リポジトリのPrivate vulnerability reportingをご利用ください。秘密鍵・実Mandate・個人情報は投稿しないでください。
リンク
- AP2 Mandate Verifier(GitHub)
- RubyGems — ap2_mandate_verifier 0.1.0
- Railsデモ(GitHub)
- Zenn — Railsデモの解説
- AP2公式仕様 v0.2
※ 本記事は2026年10月28日時点の ap2_mandate_verifier 0.1.0 の対応範囲を説明しています。デモは合成データによる限定的な検証であり、実Issuerとの相互運用・本番決済・一般的なAP2適合性を保証するものではありません。
