--- name: Stripe Refund Group Gate and Amount Cap tags: - stripe - gate-money-movement - ingress - pci-dss - sox publishedAt: 2026-07-12 description: | # stripe / gate-money-movement-refund-cap **Direction:** ingress (`tool_pre_invoke`) **Default:** deny refund tools unless group-authorized and under the cap; allow everything else **Package:** `stripe.ingress.gate_money_movement_refund_cap` ## What it does Denies Stripe refund tool calls — money out, irreversible — unless the caller's IdP groups include `finance` or `billing-admin`. Even for those groups, it denies any refund whose `amount` exceeds a configured ceiling (default `50000` = $500.00, in cents). Because the Stripe API treats an **omitted `amount` as a full refund** of the payment intent, a missing `amount` is treated as unbounded and denied above the ceiling — only refunds with an explicit positive amount at or under the ceiling go through. The check runs at ingress, before the call reaches Stripe, so a blocked refund never moves money. All non-refund tool calls pass through unchanged. ## Compliance alignment - **PCI DSS 7.2.1 / 7.2.2** — supports the least-privilege access model by restricting a money-moving operation on the payment platform to defined finance roles. - **SOX ITGC (access to programs and data)** — supports least-privilege access to a financial system on the agent channel; **Rule 13a-15(f)(3)** — supports safeguarding of assets by capping the unattended outflow an agent can trigger; **Rule 13a-15(f)(2)(ii)** — supports transaction authorization via the amount threshold, above which a human must act in the Stripe dashboard. - **SOC 2 CC6.3** — supports role-based access and segregation of duties: refund initiation through the agent is limited to finance groups, and larger refunds are separated out to human approval. ## Tool name matching The policy matches refund tools by suffix on `lower(input.resource.name)`: - `*create_refund` — the official Stripe MCP server's dedicated refund tool. The same name is used by the current meta-tool server (mcp.stripe.com / `@stripe/mcp` ≥ 0.9) and the legacy per-resource v0.8.x tool set. - `*refund_create` — the community `atharvagupta2003/mcp-stripe` server uses inverted `noun_verb` names, which breaks suffix symmetry with the official naming; matched explicitly. The DTwo gateway prefixes tool names with the configured MCP server name (e.g. `stripe-mcp-create_refund`), and that prefix is not standardized — suffix matching keeps the policy portable. Verify the exact name your gateway sends with the dump-input debug technique before relying on this in production. ## Argument shape Verified from the official server source: `create_refund` takes `{ payment_intent: string, amount?: int }` with `amount` in **cents** and an omitted `amount` meaning a full refund. The policy reads `object.get(input.payload.args, "amount", 0)`, so: - missing `amount` → default `0` → not a positive explicit amount → denied (unbounded full refund); - explicit `amount` of `0` or a non-numeric value → denied (fail closed); - explicit positive `amount` ≤ `refund_ceiling` → allowed for permitted groups. Amounts are in the currency's smallest unit — see Known limitations for non-cent currencies. ## Identity gate The caller must present an IdP `groups` claim (array of strings, compared case-insensitively) containing `finance` or `billing-admin`. Claims are read with `object.get` chains, so a caller with no claims, no `groups` claim, or an unpopulated `input.subject` **fails closed**: no group → denied. ## Examples ### Allowed — finance member, refund under the cap ```jsonc { "input": { "action": "tool_pre_invoke", "resource": { "name": "stripe-mcp-create_refund", "type": "tool" }, "subject": { "sub": "auth0|jane", "claims": { "groups": ["finance"] } }, "payload": { "name": "stripe-mcp-create_refund", "args": { "payment_intent": "pi_3Abc", "amount": 2500 } } } } ``` `allow = true`, no reason. ### Denied — full refund (amount omitted), even for finance ```jsonc { "input": { "action": "tool_pre_invoke", "resource": { "name": "stripe-mcp-create_refund", "type": "tool" }, "subject": { "sub": "auth0|jane", "claims": { "groups": ["finance"] } }, "payload": { "name": "stripe-mcp-create_refund", "args": { "payment_intent": "pi_3Abc" } } } } ``` `allow = false`, `reason = "This refund has no explicit amount or exceeds the 50000-cent ($500.00) ceiling ..."`. ## Composition This policy is single-purpose. Useful companions: - **A read-only Stripe gate** denying `*stripe_api_write` and the legacy write/destructive suffixes outside finance groups — `stripe_api_write` can issue refunds via `POST /v1/refunds` and this policy does not see inside it. - **A dispute-submit gate** on `*update_dispute` (deny or strip `submit: true`) — the other irreversible Stripe surface. - Stripe Restricted API Key (RAK) scoping — layer key permissions with gateway policy rather than relying on either alone. ## Known limitations - **Group names are placeholders — replace `finance` and `billing-admin` with your IdP's group names at import time.** The `groups` claim must be emitted by your IdP; many (including Auth0) require explicit configuration before group information reaches the token. - **`stripe_api_write` bypass.** The official meta-tool server can execute any Stripe `POST` method, including refund creation, through `*stripe_api_write`. This policy matches only dedicated refund tools; pair it with an API-write gate or allowlist policy. - **Currency-blind cap.** `amount` is in the currency's smallest unit. The default ceiling assumes a cent-denominated currency: 50000 JPY is ¥50,000 (zero-decimal), not $500. Tune `refund_ceiling` if you refund in zero-decimal currencies. - **Composio tool names unverified.** Composio's ~415-action Stripe toolkit uses its own `STRIPE_*` slug convention; whether its refund action ends in `create_refund` is unverified. Capture the live tool name from your gateway and extend `is_refund_tool` if needed. - **Treasury preview tools unverified.** Stripe's agentic-finance preview adds money-movement tools whose names are not published; they are not matched here — do not assume they are covered. - **MCP path only.** Refunds issued via the Stripe dashboard, direct API keys, or webhooks are outside the gateway's reach. > **Compliance note.** This policy supports alignment with the cited framework controls **on the MCP path only**. No policy or bundle makes an organization compliant with any framework; web-UI, native-API, and in-app access are outside the gateway's reach by design. Validate against your own compliance program before relying on it. direction: ingress apps: - stripe industries: [] bundles: - pci-dss - sox experimental: true schemaVersion: 1.0.0 minimumGatewayVersion: 1.0.0b24 --- ```rego package stripe.ingress.gate_money_movement_refund_cap # Deny-by-default: only the explicit allow rules below permit the request. default allow := false # Maximum agent-issued refund, in the currency's smallest unit # (50000 = $500.00 for cent-denominated currencies). Tune per tenant; # note zero-decimal currencies (e.g. JPY) count whole units. refund_ceiling := 50000 # IdP groups allowed to issue refunds through the agent. PLACEHOLDERS — # replace with your IdP's group names at import time. Compared # case-insensitively against the caller's `groups` claim. allowed_groups := {"finance", "billing-admin"} # Refund tools, matched by suffix so the gateway's server-name prefix # (e.g. `stripe-mcp-`) doesn't matter. `create_refund` covers the official # current and legacy servers; `refund_create` covers the community server's # inverted noun_verb naming. is_refund_tool if { endswith(lower(input.resource.name), "create_refund") } is_refund_tool if { endswith(lower(input.resource.name), "refund_create") } # Pass through any tool that isn't a refund call. allow if { not is_refund_tool } # Refunds go through only for permitted groups AND within the amount ceiling. allow if { is_refund_tool caller_in_allowed_group within_ceiling } # Fail closed on identity: missing subject, claims, or groups claim means # no membership and therefore no refund. caller_in_allowed_group if { claims := object.get(object.get(input, "subject", {}), "claims", {}) groups := object.get(claims, "groups", []) some group in groups allowed_groups[lower(group)] } # A refund is within the ceiling only when an explicit positive numeric # `amount` (smallest currency unit) is present and does not exceed # refund_ceiling. Stripe treats an omitted `amount` as a FULL refund of the # payment intent, so a missing amount (object.get default 0 here) is # unbounded and never within the ceiling. Non-numeric amounts fail closed. within_ceiling if { amount := object.get(input.payload.args, "amount", 0) is_number(amount) amount > 0 amount <= refund_ceiling } reasons contains "Agent-issued Stripe refunds are limited to members of the finance or billing-admin group. Ask someone in those groups to issue this refund from the Stripe dashboard. Contact your InfoSec team if you believe your access is misconfigured." if { is_refund_tool not caller_in_allowed_group } reasons contains "This refund has no explicit amount or exceeds the 50000-cent ($500.00) ceiling for agent-issued refunds; Stripe treats a missing amount as a full refund. Route this refund to a human in the Stripe dashboard. Contact your InfoSec team if the cap is blocking a legitimate refund." if { is_refund_tool not within_ceiling } reason := joined if { count(reasons) > 0 reason_list := sort([r | some r in reasons]) joined := concat("; ", reason_list) } ```