dtwo Policy Store

Stripe Refund Group Gate and Amount Cap

Denies Stripe refund tool calls — money out, irreversible — unless the caller's IdP groups include finance or billing-admin.

Direction
ingress
Rego package
stripe.ingress.gate_money_movement_refund_cap
App
stripe
Bundles
pci-dsssox
Published
Minimum gateway
1.0.0b24
Schema version
1.0.0
Checksum
sha256:13424f63b4b306879d531cf7825b3c8b4930965d9d24a4b3889a2310f4277540

stripegate-money-movementingresspci-dsssox

What this policy does

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 amountrefund_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

{
  "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

{
  "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.

Policy source (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)
}

Canonical source: policy.md on GitHub · raw · raw on this site (.md)

Used in these guides