Docs/Recipes/Refund Limit
Implemented — deny only; approval_required is planned

Refund Limit Policy

Allow small refunds automatically. Block large refunds until approval is implemented.

Problem

A support agent can issue refunds to customers. You want to:

  • Allow refunds up to $100 automatically — the agent can proceed without human review
  • Block refunds above $100 — these require a human to approve (approval workflow planned)
  • Never allow refunds in non-production environments

Complete Example

from brane import CapabilityDeniedError, Decision, Effect, Runtime

# Tenant configuration (in practice, fetch from your config/DB)
TENANT_LIMITS = {
    "tenant_acme": 100.0,
    "tenant_globex": 250.0,
    "tenant_initech": 50.0,
}

DEFAULT_LIMIT = 100.0

# 1. Create runtime
runtime = Runtime(
    agent_id="support-agent",
    environment="prod",
    tenant_id="tenant_acme",
)

# 2. Register the refund capability
@runtime.capability(
    name="refund_customer",
    type="tool",
    risk="high",
    effect=Effect(
        type="financial",
        reversible=False,
        external=True,
        description="Issues a payment refund",
    ),
    data_namespace="billing.refunds",
    owner="payments-team",
)
def refund_customer(customer_id: str, amount_usd: float, reason: str = ""):
    # Real implementation calls your payments API
    return {"refunded": True, "customer_id": customer_id, "amount": amount_usd}

# 3. Policy: only allow refunds in production
@runtime.before_capability(
    "refund_customer",
    name="refund_require_prod",
    version="1.0",
    priority=200,
)
def require_prod_for_refunds(ctx):
    if not ctx.is_prod:
        return Decision(
            type="deny",
            reason="Refunds can only be issued in the production environment",
        )
    return Decision(type="allow")

# 4. Policy: enforce per-tenant refund limit
@runtime.before_capability(
    "refund_customer",
    name="refund_amount_limit",
    version="1.0",
    priority=100,
)
def refund_amount_limit(ctx):
    amount = ctx.arg("amount_usd", 0)
    limit = TENANT_LIMITS.get(ctx.tenant_id, DEFAULT_LIMIT)

    if amount <= 0:
        return Decision(type="deny", reason="Refund amount must be positive")

    if amount > limit:
        return Decision(
            type="deny",
            # Future: type="approval_required" — pause until a human approves
            reason=(
                f"Refund of ${amount:.2f} exceeds the ${limit:.2f} limit "
                f"for tenant {ctx.tenant_id}. Human approval is required."
            ),
        )

    return Decision(type="allow")

# 5. Use the capability
try:
    # Allowed — within limit
    result = refund_customer("cust_123", 75.00, reason="Duplicate charge")
    print(result)
    # {"refunded": True, "customer_id": "cust_123", "amount": 75.0}

    # Denied — exceeds limit
    refund_customer("cust_456", 349.00, reason="Defective product")

except CapabilityDeniedError as e:
    print(f"Blocked: {e.reason}")
    # Output: Blocked: Refund of $349.00 exceeds the $100.00 limit for tenant tenant_acme. Human approval is required.
    print(f"Action ID: {e.action_id}")

How It Works

Two policies protect the refund_customer capability, running in priority order:

  1. require_prod_for_refunds (priority 200, runs first) — blocks the refund entirely if the environment is not prod. Prevents staging or dev agents from issuing real refunds.
  2. refund_amount_limit (priority 100, runs second) — checks the refund amount against the tenant-specific limit. Blocks amounts over the limit.

Both policies must allow for the refund to proceed. Either can deny.

Adding a Third Policy: Scope Check

@runtime.before_capability(
    "refund_customer",
    name="refund_scope_check",
    version="1.0",
    priority=150,
)
def require_refund_scope(ctx):
    if not ctx.agent_has_scope("refunds:create"):
        return Decision(
            type="deny",
            reason="Agent requires refunds:create scope to issue refunds",
        )
    return Decision(type="allow")

Future: Approval Instead of Deny

Once approval_required decisions are implemented, the limit policy can be updated to pause rather than block:

if amount > limit:
    return Decision(
        type="approval_required",
        reason=f"Refund of ${amount:.2f} requires human approval",
        approval={
            "approver_group": "finance-ops",
            "expiration_minutes": 60,
            "context": {
                "customer_id": ctx.arg("customer_id"),
                "amount": amount,
                "tenant": ctx.tenant_id,
            },
        },
    )

The runtime will pause the action, send the approval request, and resume when a human approves or deny automatically when they reject or the request expires.

Production Notes

  • The policy runs before the refund API call. If the policy denies, no refund is issued and no payment provider API is called.
  • Store tenant limits in your configuration or database, not hardcoded. The policy function is plain Python — fetch from wherever makes sense for your stack.
  • Use action_id from CapabilityDeniedError to correlate the denial with your own request logs.
  • When audit sinks are available, every allowed and denied refund attempt will produce an audit event — creating a complete refund trail for compliance.

Related