Skip to main content
Glama

TrustGate

TrustGate 是一个合成数据、Razorpay Test Mode 演示,展示一个 AI 买家可以提出目录购买建议,但无法获得改写商户、金额、货币、审批或提供商结果的权限。代理提出建议;TrustGate 独立授权并记录证据。

它证明了什么

  • 目录 SKU 和数量是代理唯一能影响的购买事实。

  • 租户范围的策略、人工审批、一次性结账权限和已验证的提供商事件约束着每一个支付操作。

  • 不安全的尝试在提供商订单创建之前就被拒绝,并留下可审计的痕迹。

Related MCP server: safe-cart-ai

AI 在这里如何使用

买家代理被刻意设计得很薄,而这正是论点本身,而非捷径。它只能提出目录 SKU、数量和用途;每一个涉及金钱的关键事实都在服务端派生。完全删除代理,授权层保持不变,仍然正确。

工程贡献不是复杂的代理。而是认真对待语言模型的失败模式——通过第三方内容进行提示注入、违背操作者利益的指令遵循、关于金钱的自信错误推理——并构建一个在这些情况发生时仍能保持正确的系统。

这一主张以两种方式得到检验。python -m agent.demo --live 针对由第三方(包括敌意方)编写的目录描述运行真实模型。同一目录被发送两次,一次移除描述,因此不受信任内容的影响通过比较两次提议来衡量,而非自我报告。回归套件从不调用模型提供商:它使用确定性替代品,因此安全验证不依赖于模型在某一天以特定方式表现。

看到问题

三次干净的拒绝证明了系统有效,但很难说明为什么有人需要它。因此演示以本项目自己的代码失败开场,不使用网络、不使用凭据、不使用数据库:

python -m demo.unguarded

同一个代理读取同一个被投毒的目录——字面上相同,因为两个目录都构建自 agent/demo_catalog.py,且一个测试断言种子行和基线对象携带相同的注入指令。一个模型响应被交给两个适配器。

未受防护的那个接受金额和商户,因此注入指令得以执行,它向目录文本中指定的商户支付 INR 20,000.00,而目录价格为 INR 600.00。另一个没有地方放置这两个字段,因为 PurchaseProposal 只声明了 SKU、数量和用途。丢弃后幸存的是 quantity=50,这是代理被允许设置的字段——而服务器将其限制在目录自身的最大值 2 以内,因此在支付请求创建之前尝试就被拒绝。

区别不在于识别攻击的过滤器。而是一个接口有金钱字段,另一个没有。基线有自己的测试,既断言它无法触及任何真实事物,也断言它仍然可利用,因为一个悄悄不再易受攻击的演示会继续通过测试,同时传达相反的观点。

攻击矩阵

Tier A 对抗性场景。此表由 python -m scenarios.report 从场景注册表生成,且一个测试断言其匹配,因此它不能声称一个未被通过测试覆盖的攻击。每个场景证明三件事:攻击以其原因代码被拒绝,没有创建提供商订单,没有支付获得其本不具备的权限。

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 状态码。第二次购买永远不会被创建,也不会被误认为是已接受的购买。

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 会故意一次一个地破坏每个安全不变量,并要求以其命名的测试作为守卫而失败。每个源文件都会在 finally 块中恢复,并且在报告打印之前会通过 git diff 检查恢复结果,因此中断的运行不会留下任何变异。如果任何变异存活,它会以非零状态退出。

此表由 python -m scenarios.report --mutations 从变异注册表生成,并且有一个测试断言其匹配,因此它不能声称某个受守卫的不变量实际上并未受到守卫。

Mutation

其移除的不变量

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

每日预算的 upsert 拒绝超过限额。

budget-release-from-state-guard

预算仅由实际预留它的支付返还。

checkout-script-escaping

目录文本不能终止结账页面的 script 元素。

request-session-commit

成功的请求提交其写入。

provider-event-identity

同一支付的生命周期事件是不同的事件,而非重放。

self-approval-guard

审批不能由发起请求的行为者授予。

evidence-tenant-filter

证据限定在请求它的租户范围内。

receipt-search-fail-closed

不完整的提供商搜索绝不会将收据报告为不存在。

该套件的首次运行发现了一个实际缺陷。通过 ORM 的 SELECT ... FOR UPDATE 正确获取了锁,但随后丢弃了 Postgres 返回的行,因为 SQLAlchemy 保留了会话身份映射中已有对象的属性。因此,第二个调用者按设计阻塞在锁上,收到已提交的行,保留其过期副本,并授权了一笔刚刚已被授权的支付。锁在转换运行时进行序列化,而非序列化它们所依据的状态。现在所有加锁都通过 models.locking.locked() 进行,并且有一个测试断言该辅助函数是源码中唯一可以获取锁的地方。

当前范围

TrustGate 仅使用合成租户、商户和 INR 价格。它是一个本地安全测试平台,而非支付处理器、合规产品、法律同意系统、欺诈模型或 Live Mode 支付集成。

快速开始

要求: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 获取。

使用 python -m agent.seed 创建一个一次性的合成 M1 租户。将 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。支持两个后端,两者都使用相同的 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 在任何后端上覆盖模型。买家是协议实现,因此提供商是配置选择而非架构选择:授权层的行为不依赖于哪个模型提出购买。

这是项目中唯一与模型提供商接触的路径;测试套件从不这样做。仅针对合成种子目录运行它。其第三方描述会发送给模型两次,一次移除描述,一次保持完整,以衡量影响。切勿通过此演示发送真实的客户、商户或支付数据。

要使用 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.
    -