Human-Only Stripe Dispute Submission
Strips the irreversible submit flag from Stripe update dispute tool calls.
- Direction
- ingress
- Rego package
stripe.ingress.require_human_approval_dispute_submit- App
- stripe
- Bundle
- sox
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:f97adf30fcbb2267bb1875aff767e1b4d22820d4e210df901e64dec4c76ad081
striperequire-human-approvaldisputesseparation-of-dutiestransformingresssox
What this policy does
Direction: ingress (tool_pre_invoke)
Default: allow (transform-only — never denies)
Package: stripe.ingress.require_human_approval_dispute_submit
What it does
Strips the irreversible submit flag from Stripe *update_dispute tool
calls. Filing dispute evidence with the card network is one-shot: once
update_dispute is called with submit: true, the evidence is submitted and
cannot be amended or resubmitted. This policy keeps that consummating step
off the agent path while leaving the drafting step fully functional — the
initiate-vs-approve separation.
Concretely: when an *update_dispute call carries a submit key in its
arguments, the policy emits a transform that removes the key and passes the
rest of the call (the dispute ID and the evidence draft —
cancellation_policy_disclosure, duplicate_charge_explanation,
uncategorized_text) through unchanged. The agent's evidence draft lands on
the dispute; a human then reviews and submits it in the Stripe dashboard,
where the submitting person is recorded. Calls without a submit key, and
every other tool, pass through untouched with no transform.
There is no identity-group exemption: evidence filing is human-gated for every caller, including privileged users, because the point of the gate is that the irreversible decision is taken by a person, not by whichever service identity the agent happens to run as.
Compliance alignment
- SOC 2 CC6.3 — supports role-based access with separation of duties on the agent path: the actor that drafts dispute evidence cannot also be the actor that files it with the card network over MCP.
- SOX SoD (COSO Principle 10) — supports the initiate-vs-approve separation on a financially consequential transaction: the agent initiates (drafts evidence), a human approves (submits in the dashboard).
- SOX 13a-15(f)(2)(ii) — supports transaction authorization by keeping the authorizing action (irreversible submission of dispute evidence, which determines whether disputed funds are recovered) off the automated actor.
- SOX / PCAOB AI human-in-the-loop — supports a draft-only agent posture for dispute consummation, consistent with human-oversight expectations for AI acting on financial records.
Tool name matching
The DTwo gateway prefixes tool names with the configured MCP server name as
<server-name>-<tool-name> (e.g. stripe-mcp-update_dispute), and that
prefix is not standardized across deployments. The policy therefore matches
case-insensitively on lower(input.resource.name) by suffix
(endswith), against one verified name:
*update_dispute— the legacy per-resource tool from@stripe/mcpv0.8.x / the Claude Desktop.dxtmanifest (verified from thestripe/airepo history). Its argument shape is{ dispute, evidence?: { cancellation_policy_disclosure?, duplicate_charge_explanation?, uncategorized_text? }, submit?: boolean }.
Suffix matching is deliberately broad for a control gate: a hypothetical
bulk_update_dispute, or an aggregator slug like STRIPE_UPDATE_DISPUTE
(unverified — Composio-style naming), also ends with the suffix and is
also transformed, which is the intended fail-safe direction. Verify the
exact name your gateway sends with the dump-input debug technique before
relying on this in production.
Argument shape
The policy reads input.payload.args via object.get at every step, so a
missing payload, missing args, or missing submit key simply means the
transform never fires and the call passes through — there is nothing to
strip, and a transform-only policy has nothing to deny.
The transform fires on presence of the submit key, not on
submit == true. Stripe's form-encoded API treats string encodings like
"true" as truthy, so matching only the boolean would leave an encoding
bypass; and removing an explicit submit: false is a semantic no-op
(false is Stripe's default). Stripping on presence closes the bypass
without changing behavior for compliant callers.
When it fires, the transform emits transformed_payload =
object.remove(args, ["submit"]) — the original arguments minus the flag,
with the dispute ID and the entire evidence object preserved verbatim.
Examples
Allowed untouched — drafting evidence without submitting
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "stripe-mcp-update_dispute", "type": "tool" },
"payload": {
"name": "stripe-mcp-update_dispute",
"args": {
"dispute": "dp_1OABCD2eZvKYlo2C",
"evidence": { "duplicate_charge_explanation": "Two distinct orders; receipts attached." }
}
}
}
}
allow = true, no transform — the draft reaches Stripe as sent.
Transformed — submit flag stripped
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "stripe-mcp-update_dispute", "type": "tool" },
"payload": {
"name": "stripe-mcp-update_dispute",
"args": {
"dispute": "dp_1OABCD2eZvKYlo2C",
"evidence": { "uncategorized_text": "Customer signed the cancellation policy on 2026-05-02." },
"submit": true
}
}
}
}
allow = true, transform.transformed_payload = { "dispute": "dp_1OABCD2eZvKYlo2C", "evidence": { ... } }
— the evidence draft is saved, the filing is not; a human submits from the
Stripe dashboard.
Composition
This policy is single-purpose — it removes only the dispute-submission consummation step. Pair it with:
- An API-write allowlist / escape-hatch policy on
*stripe_api_write— the current official Stripe MCP server routes all writes through thestripe_api_writemeta-tool, including dispute updates. A dispute submission made through that tool never matches*update_dispute, so this transform cannot see it (see Known limitations). Denying or endpoint-allowlistingstripe_api_writeis what closes that route. - A default-deny-unknown-tools allowlist (PF-28) — catches renamed or aggregator-specific dispute tools this suffix match cannot anticipate.
- A refund-cap policy on
*create_refund— the other irreversible money-out surface in the legacy Stripe tool set. - Stripe Restricted API Key (RAK) scoping — layer, don't substitute: a key without dispute-write permission is the control that also covers non-MCP access.
Known limitations
- The
stripe_api_writeescape hatch is invisible here. On the current official server (mcp.stripe.com and the v0.9+@stripe/mcpproxy), all writes — includingPOST /v1/disputes/{id}withsubmit: true— go through thestripe_api_writemeta-tool, whose name does not end inupdate_dispute. This policy's verified target is the legacy per-resourceupdate_disputetool (v0.8.x installs, the.dxtmanifest, and@stripe/agent-toolkitembeddings). Deployments on the current server must pair this with an allowlist/deny on*stripe_api_writeor the gate is decorative. - Renamed tools slip past. A
noun_verbserver (e.g. a community server'sdispute_update) or a trailing-token variant (update_disputes,update_dispute_v2) does not end with the exact suffix and passes through untouched. Rely on a PF-28 allowlist to fail unknown names closed. - Suffix breadth (prefix side) is intentional. Any tool name ending
in
update_disputeis transformed, including hypothetical bulk variants. For a human-approval gate this over-inclusion is the safe direction; if a legitimate tool is caught, escalate to your gateway admin. trim_spacenormalizes only standard whitespace. A tool name ending in a zero-width or format character (U+200B, U+FEFF, U+2060) afterupdate_disputedoes not match the suffix and passes through untouched. Any strip list can itself be evaded, so this is not chased in the Rego; the PF-28 allowlist is the backstop.- Presence-based stripping also removes
submit: false. Semantically a no-op (false is Stripe's default), but the call Stripe receives differs byte-for-byte from the call the agent sent. This is the cost of closing the truthy-string-encoding bypass. - Treasury preview tools are out of scope. Stripe's agentic-finance preview tool names are not published; nothing here matches them, and no policy in this store should guess at them.
- MCP path only. The Stripe dashboard, direct API keys, and webhooks are outside the gateway's reach — which is exactly why the human submits from the dashboard. Pair with RAK scoping for the non-MCP surface.
- No identity-based exemptions — by design. There is no break-glass group, so there are no placeholder group names to replace at import time. Submission over MCP is stripped for everyone; humans submit in the dashboard where their identity is recorded.
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.require_human_approval_dispute_submit
# Transform-only policy: never denies. When an *update_dispute call carries
# the irreversible `submit` flag, the transform strips it so the evidence
# draft still lands on the dispute but the one-shot filing with the card
# network stays human-actuated (initiate-vs-approve separation).
default allow := true
# Tool arguments; a missing payload or args resolves to {} so the transform
# condition simply never fires on malformed input (nothing to strip).
args := object.get(object.get(input, "payload", {}), "args", {})
# Ingress pre-invoke gate. `action` is the PARC field; `kind` is its populated
# legacy alias (same value). Accept EITHER: a build that populates only `kind`
# (or a PARC revision that drops `action`) would otherwise fail the match and
# pass a submit:true call straight through — a fail-open submission of the
# one-shot filing. Restricting to pre-invoke also keeps the transform off
# egress hooks, whose payload carries `text`, not `args`.
is_pre_invoke if object.get(input, "action", "") == "tool_pre_invoke"
is_pre_invoke if object.get(input, "kind", "") == "tool_pre_invoke"
# Tool name, lowercased and whitespace-trimmed; a missing resource/name
# resolves to "" (matches nothing). trim_space closes a suffix-match evasion:
# a name with a trailing space/tab/newline would otherwise fail endswith and
# carry its submit flag through untouched.
tool_name := trim_space(lower(object.get(object.get(input, "resource", {}), "name", "")))
# The verified legacy per-resource tool `update_dispute`, matched by suffix
# for portability across the gateway's `<server-name>-<tool-name>` prefixing.
# Suffix breadth is intentional: any name ending in update_dispute (bulk or
# aggregator variants) is also a dispute write and gets the same treatment.
is_update_dispute_tool if {
endswith(tool_name, "update_dispute")
}
# Fire on presence of the `submit` key, not on `submit == true`: Stripe's
# form-encoded API treats string encodings like "true" as truthy, and
# removing an explicit `submit: false` is a semantic no-op (false is the
# API default), so presence-matching closes the encoding bypass without
# changing behavior for compliant callers.
has_submit_key if {
"submit" in object.keys(args)
}
# Strip the submit flag; everything else (dispute ID, evidence draft) passes
# through verbatim. The human consummates submission in the Stripe dashboard.
transform := {"transformed_payload": object.remove(args, ["submit"])} if {
is_pre_invoke
is_update_dispute_tool
has_submit_key
} Canonical source: policy.md on GitHub · raw · raw on this site (.md)
Used in these guides
Related policies
Block Destructive and Mutating Snowflake SQL
Inspects the SQL text that Snowflake MCP tools carry in their query argument and denies any statement in a mutating or destructive class — DROP, TRUNCATE,…
snowflakeguard-warehouse-sqlingresssqlreadonlysoc2pci-dsssox
Block Destructive SQL in BigQuery Queries
Inspects the raw GoogleSQL string carried by BigQuery write-capable query tools and denies any statement in a state-changing class — DML…
Deny Stripe API-Write Escape Hatch
Denies the stripe api write meta-tool — the single raw passthrough on the official Stripe MCP server that can execute any Stripe POST, PATCH, PUT, or DELETE…
Freeze Destructive Microsoft 365 Operations
Denies every Microsoft 365 tool call whose verb segment is delete- or cancel- unless the caller's IdP token carries the placeholder group m365-admin.
Freeze Destructive QuickBooks Operations
Denies every destructive QuickBooks Online (QBO) tool call on the agent channel before it reaches the MCP server.
Gate QuickBooks Money-Movement by Finance Group
Gates the QuickBooks Online money-movement creation tools — create payment, create bill payment, create refund receipt, create transfer, and create deposit —…