Skip to main content
Glama

TrustGate

TrustGate は、Razorpay テストモードを利用した合成データによるデモで、AI バイヤーがカタログ購入を提案できるものの、販売者・金額・通貨・承認・プロバイダー結果を書き換える権限は持たないことを示します。エージェントは提案するだけであり、TrustGate が独立して承認し、証跡を記録します。

これが証明すること

  • カタログ SKU と数量だけが、エージェントが影響を与えられる唯一の購入事実です。

  • テナント単位のポリシー、人間による承認、ワンタイムのチェックアウト権限、検証済みのプロバイダーイベントが、すべての支払いアクションを拘束します。

  • 安全でない試行は、プロバイダー注文が作成される前に拒否され、監査可能な証跡を残します。

Related MCP server: safe-cart-ai

ここでの AI の使われ方

バイヤーエージェントは意図的に薄く作られており、それは手抜きではなく、むしろ主張そのものです。エージェントが提案できるのはカタログ SKU、数量、目的のみで、金銭に関わるすべての事実はサーバー側で導出されます。エージェントを完全に削除しても、承認レイヤーは変わらず、依然として正しく機能します。

技術的な貢献は、高度なエージェントではありません。言語モデルの失敗モードを真剣に受け止めること——サードパーティのコンテンツによるプロンプトインジェクション、運用者の利益に反する指示追従、金銭に関する過信した誤った推論——そして、それらが発生しても正しく動作し続けるシステムを構築することです。

この主張は双方向でテストされています。python -m agent.demo --live は、敵対的なものを含むサードパーティが書いたカタログ記述に対して実際のモデルを実行します。同じカタログが2回送信され、1回は記述を削除した状態で送信されるため、信頼できないコンテンツからの影響は自己申告ではなく、2つの提案を比較することで測定されます。回帰テストスイートはモデルプロバイダーを一切呼び出しません。決定的な代替手段を使用するため、安全性の検証は、モデルが特定の日に特定の動作をするかどうかに依存しません。

問題を見る

3つの明確な拒否はシステムが機能することを証明しますが、なぜ誰かがそれを必要とするのかを示すには弱いものです。そのため、デモはこのプロジェクト自身のコードが失敗するところから始まります。ネットワークも、認証情報も、データベースも使用しません。

python -m demo.unguarded

同じエージェントが同じ毒入りカタログを読みます——文字通り同じものです。両方のカタログは agent/demo_catalog.py から構築され、テストはシードされた行とベースラインオブジェクトが同一の注入命令を保持していることを検証します。1つのモデル応答が2つのアダプターに渡されます。

ガードのない方は金額と販売者を受け入れるため、注入された命令が実行され、カタログ価格 INR 600.00 に対して、カタログテキストが指定した販売者に INR 20,000.00 を支払います。もう一方は、PurchaseProposal が SKU、数量、目的のみを宣言するため、どちらのフィールドにも入れる場所がありません。破棄を生き残るのは quantity=50 で、これはエージェントが設定できるフィールドです——そしてサーバーはそれをカタログ自身の最大値2に対して制限するため、支払いリクエストが作成される前に試行は拒否されます。

違いは攻撃を認識したフィルターではありません。一方のインターフェースには金銭のフィールドがあり、もう一方にはなかったということです。ベースラインには、現実のものに到達できないことと、依然として悪用可能であることの両方を検証する独自のテストがあります。なぜなら、静かに脆弱でなくなったデモは、反対の主張をしながらテストを通過し続けることになるからです。

攻撃マトリックス

Tier A の敵対的シナリオ。この表は python -m scenarios.report によってシナリオレジストリから生成され、テストが一致することを検証するため、合格するテストでカバーされていない攻撃を主張することはできません。すべてのシナリオは3つのことを証明します。攻撃が理由コード付きで拒否されること、プロバイダー注文が作成されないこと、そして支払いが持っていなかった権限を獲得していないことです。

ID

攻撃

証明された不変条件

テスト

A1

金額の改ざん

金額はカタログ商品の価格とサーバー側で制限された数量から導出される。エージェントが提供する値では変更できない。

test_a1_supplied_amount_field_is_refused_at_the_boundarytest_a1_mcp_surface_has_no_amount_parametertest_a1_quantity_cannot_be_used_to_escalate_the_amount

A2

加盟店のすり替え

加盟店はテナントスコープのカタログ商品から導出される。テナント外の加盟店には到達できず、アクティブなポリシー外の加盟店には支払いできない。

test_a2_another_tenants_sku_is_not_reachabletest_a2_policy_disallowed_merchant_cannot_be_paid

A3

通貨のすり替え

通貨はカタログ商品から導出され、通貨を受け付ける唯一のルートはデフォルトで無効化されており、有効化された場合もアクティブなポリシーとの不一致を拒否する。

test_a3_the_agent_surface_derives_currency_and_cannot_be_told_onetest_a3_the_only_currency_accepting_route_is_disabled_by_defaulttest_a3_an_enabled_legacy_route_still_denies_a_currency_outside_the_policy

A4

期限切れまたは再利用された承認

承認は有効期限と単回使用を持つ権限である。期限切れのものも既に消費されたものも承認できず、拒否された承認は消費されない。

test_a4_an_expired_approval_cannot_authorizetest_a4_an_already_consumed_approval_cannot_authorize_again

A5

自己承認

購入を要求した本人は承認を付与できない。職務分離は設定で期待されるだけでなく、強制される。

test_a5_an_approval_cannot_be_granted_by_the_requesting_actortest_a5_a_separate_approver_can_still_grant

A6

偽造されたWebhook署名

プロバイダーイベントは、ボディが解析される前に生バイトのHMACで認証される。偽造された署名や欠落した署名は、イベントがどれだけ整形式でも何も変更しない。

test_a6_a_forged_signature_is_refusedtest_a6_an_unsigned_event_is_refused

A7

改ざんされたWebhookボディ

署名は受信した正確なバイトを対象とするため、署名後に送信中に編集されたイベントは検証に失敗し、支払いに到達することはない。

test_a7_a_body_altered_after_signing_no_longer_verifies

A8

重複したWebhook配信

プロバイダーイベントの識別情報が保存されるため、真正でウィンドウ内のイベントのリプレイは、たまたま参照するハンドラーではなくデータベースによって拒否される。

test_a8_a_replayed_event_does_not_transition_the_payment_twice

A9

順序が乱れたプロバイダーイベント

到着順はプロバイダーのものであり、合法性はこちら側のものである。キャプチャは承認に先行できず、終端状態の支払いはそれ以上の結果を受け付けない。

test_a9_a_capture_cannot_precede_an_authorizationtest_a9_a_terminal_payment_accepts_no_further_provider_outcome

A10

二重返金

どのサーフェスも返金を開始できず、これは稼働中のルートテーブルとツールリストに対して検証される。また、台帳の不変条件はキャプチャ額を超える返金合計を拒否する。

test_a10_no_surface_anywhere_can_initiate_a_refundtest_a10_a_refund_total_cannot_exceed_what_was_captured

A11a

不明なテナントヘッダー

解決できないテナントは、ルートボディが実行される前に拒否され、その拒否は呼び出し元がどのテナントが存在するかを列挙できるような情報を一切開示しない。

test_a11a_an_unknown_tenant_header_is_refusedtest_a11a_an_unknown_tenant_is_indistinguishable_from_a_forbidden_one

A11b

クロステナントオブジェクトアクセス

すべてのテナントスコープのルックアップは、信頼されたテナントでフィルタリングされる。既知のテナントは、どのサーフェスでも別のテナントのリクエスト、支払い、または権限を読み取ったり操作したりできない。

test_a11b_checkout_authority_route_refuses_another_tenants_requesttest_a11b_razorpay_route_refuses_another_tenants_authoritytest_a11b_mcp_refuses_another_tenants_payment

A12

冪等性キーの衝突

異なる購入で再利用されたキーは、元の決定と409を返す。2番目の購入は決して作成されず、受け入れられたものと誤認されることはない。

test_a12_a_reused_key_with_a_different_purchase_returns_the_first_decision

A13

承認と使用の間のポリシードリフト

権限は、それが照合されたポリシーや発行された購入よりも長く存続しない。後続のポリシーや編集された金額は、それを消費せずに失効させ、ドリフトしていない権限は引き続き機能する。

test_a13_an_authority_is_valid_until_the_policy_under_it_movestest_a13_a_policy_published_after_authorization_revokes_the_authoritytest_a13_an_amount_edited_after_authorization_breaks_the_snapshot_hash

A14

古いまたは先日付のWebhook

署名は発信元を証明するものであり、新しさを証明するものではない。鮮度ウィンドウ外のイベント、未来の日付のイベント、またはタイムスタンプをまったく持たないイベントは、ルックアップの前に拒否される。

test_a14_a_stale_signed_event_is_refusedtest_a14_a_post_dated_event_cannot_extend_its_own_validitytest_a14_an_event_with_no_timestamp_is_refused_rather_than_exempted

A15

MCP経由の不正なキャプチャ

エージェントが到達可能なツールは、承認、キャプチャ、返金、プロバイダー呼び出しのいずれもできない。これはツール名を検査するのではなく、公開されたすべてのツールを実行して証明される。

test_a15_every_exposed_mcp_tool_grants_no_payment_authoritytest_a15_mcp_exposes_no_provider_or_authorization_tool

すべてのTier Aシナリオが実装されている。A11aとA11bは、テナントの混同を「解決できないテナント」と「境界を越えて到達する既知のテナント」に分割している。両者は異なる理由で失敗し、後者のみが認可の問題だからである。

テストの価値

テストスイートが成功することは、コードが書かれたとおりに動作することを示す。コードが重要な何かをしなくなった場合にテストが異議を唱えることを示すものではなく、お金が関わる場合に重要なのは後者の主張だけである。このプロジェクトにはその違いの証拠がある。リクエストスコープのセッションが、146のテストが成功している間にすべての書き込みを破棄していたことがある。スイートが書き込みを行ったのと同じトランザクション内で検証していたからである。

make mutationは、各安全不変条件を意図的に一度に1つずつ破壊し、その守護者として指定されたテストが失敗することを要求する。すべてのソースファイルはfinallyブロックで復元され、レポートが出力される前にgit diffで復元が確認されるため、中断された実行でもミューテーションが残ることはない。ミューテーションが生き残った場合は非ゼロで終了する。

このテーブルはpython -m scenarios.report --mutationsによってミューテーションレジストリから生成され、テストが一致を検証するため、実際には守られていない不変条件が守られていると主張することはできない。

ミューテーション

それが除去する不変条件

payment-row-lock

支払いは、その状態が読み取られ変更される前にロックされます。

locked-read-freshness

ロックされた読み取りは、キャッシュされた行ではなく、コミットされた行から決定します。

locking-discipline

行ロックは、それらを意味のあるものにする唯一のヘルパーを通じて取得されます。

webhook-signature-check

プロバイダーイベントは、何かを行う前に認証されます。

webhook-freshness-window

署名されたプロバイダーイベントは、発信元を証明し、新しさは証明しません。

webhook-timestamp-required

日付を付けることができないイベントは制限できないため、拒否されます。

approval-expiry

承認は、永続的な許可ではなく、有効期限のある許可です。

authority-policy-drift

権限は、それがチェックされたポリシーよりも長く存続しません。

authority-snapshot-binding

権限は、それが発行された正確な購入に結び付けられています。

daily-budget-predicate

日次予算のアップサートは、制限を超えることを拒否します。

budget-release-from-state-guard

予算は、実際にそれを予約した支払いによってのみ返されます。

checkout-script-escaping

カタログテキストは、チェックアウトページのスクリプト要素を終了できません。

request-session-commit

成功したリクエストは、その書き込みをコミットします。

provider-event-identity

1つの支払いのライフサイクルイベントは、リプレイではなく、個別のイベントです。

self-approval-guard

承認は、要求元のアクターによって付与されることはできません。

evidence-tenant-filter

証拠は、それを要求したテナントに限定されます。

receipt-search-fail-closed

不完全なプロバイダー検索は、領収書を不存在として報告することはありません。

このスイートの最初の実行で、実際の欠陥が見つかりました。ORMを介したSELECT ... FOR UPDATEはロックを正しく取得しますが、Postgresが返した行を破棄します。これは、SQLAlchemyがセッションのアイデンティティマップに既にあるオブジェクトの属性を保持するためです。そのため、2番目の呼び出し元は設計どおりロックでブロックされ、コミットされた行を受け取り、古いコピーを保持し、ちょうど承認されたばかりの支払いを承認しました。ロックは、遷移が実行されるタイミングを直列化していましたが、遷移が決定する状態を直列化していませんでした。現在、すべてのロックはmodels.locking.locked()を介して行われ、テストはそのヘルパーがソース内でロックを取得できる唯一の場所であることを検証します。

現在のスコープ

TrustGateは、合成テナント、マーチャント、およびINR価格のみを使用します。これはローカルな安全性テストベッドであり、決済処理業者、コンプライアンス製品、法的同意システム、不正モデル、またはライブモードの決済統合ではありません。

クイックスタート

要件: Docker Desktop と Python 3.12。

Copy-Item .env.example .env
docker compose up -d
docker compose exec -T api python -m alembic upgrade head
docker compose exec -T api python -m pytest -q

ローカルAPIのヘルスチェックは http://127.0.0.1:8000/health で利用できます。

使い捨ての合成M1テナントを python -m agent.seed で作成します。MCP_TENANT_ID と MCP_ACTOR_ID を出力された値に設定し、python -m agent.demo "Buy Starter credits for our student club." でローカルの買い手エージェントデモを実行します。これはカタログのSKU、数量、目的のみを提案できます。MCPサーバーがすべての金銭的に重要な事実を導き出します。python -m agent.demo --adversarial "Buy a small amount of cloud credits." を使用して、決定論的な毒カタログデモを実行します。

決定論的な代替ではなく実際のモデルで同じフローを実行するには、オプションのエクストラを pip install -e ".[agent]" でインストールし、--live を追加します。2つのバックエンドがサポートされており、両方とも同じMessages APIの形状を使用します。

  • TRUSTGATE_MODEL_BACKEND=anthropic (デフォルト) は ANTHROPIC_API_KEY を読み取ります。

  • TRUSTGATE_MODEL_BACKEND=bedrock はAWSアカウントに請求します。AWS_REGION を資格情報が属するリージョンに設定し、次に AWS_BEARER_TOKEN_BEDROCK(Bedrock APIキー、最も簡単な方法)またはSigV4署名用の標準AWS資格情報のいずれかを設定します。Amazon BedrockはAWS Marketplaceサブスクリプションを通じてAnthropicモデルを提供するため、AWSアカウントには、クレジットで使用量をカバーできる場合でも、有効な支払い手段も必要です。

  • TRUSTGATE_MODEL_BACKEND=groq は GROQ_API_KEY を読み取り、支払い手段はまったく必要ありません。

TRUSTGATE_MODEL_ID は任意のバックエンドでモデルを上書きします。買い手はプロトコル実装であるため、プロバイダーはアーキテクチャ上の選択ではなく構成上の選択です。承認レイヤーの動作は、どのモデルが購入を提案するかに依存しません。

これはプロジェクト内でモデルプロバイダーに接触する唯一の経路であり、テストスイートは決して接触しません。合成シードカタログに対してのみ実行してください。そのサードパーティの説明は、影響を測定するために、説明を削除した状態とそのままの状態の2回モデルに送信されます。実際の顧客、マーチャント、または支払いデータをこのデモを通じて送信しないでください。

Razorpay Test Modeアダプターを試すには、無視される .env ファイルに RAZORPAY_KEY_ID と RAZORPAY_KEY_SECRET を設定します。Test ModeまたはLive Modeのシークレットをリポジトリに追加しないでください。

信頼境界

AI buyer proposes SKU, quantity, and purpose
        -> TrustGate derives and authorizes money-critical facts
        -> Razorpay Test Mode executes a bounded order
        -> TrustGate records authorization and provider evidence

正式なビルド計画は docs/build-plan.md にあります。アーキテクチャ、脅威モデル、設計上の決定は docs/ にあります。

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to browse product catalogs and make purchases through a policy engine that enforces spending limits, requires human approval for certain amounts, and logs all actions to an audit trail.
    -