DEV Community

shakti mishra
shakti mishra

Posted on

Pre-Action Authorization, Bounded Approvals and Idempotent Retries for Write-Capable LLM Agents

Your AI Agent Doesn't Need Better Judgment. It Needs a Service That Says No.

Picture a customer-operations lead, Maya. Her AI assistant explains the refund policy well. Then she asks the obvious next question: can it just issue the refund?

That sentence quietly changes your threat model. The assistant has gone from saying things to moving company money. Most teams respond by improving the prompt: add the policy, add "never refund over $100 without approval," add a few examples. That is a request, not a control. A prompt-injected customer message, a hallucinated order ID, or a retry after a timeout can all walk straight past it.

The pattern that holds up in production is boring and old: the agent proposes, a deterministic service disposes. The agent can never grant itself permission. This post walks through what that looks like for a refund agent: ownership, enforcement points, bounded approvals, safe retries, and a staged autonomy ramp.

Start with the owner, not the model

Before any architecture, name three roles for every agent:

Role Decides
Business owner What the agent may do and when it must ask for help
Technical owner Access scopes, tests, monitoring, technical failures
Human decision-maker Approve or reject actions that need judgment

These can be one or two people. For the refund agent, Maya owns the refund rules and an engineering lead maintains the controls. The test is simple: who can stop the agent at 2 a.m.? If nobody can answer, you aren't ready to give it write access.

Two questions then size its authority: how reversible is the action, and how bad is the worst case? Drafting a recommendation can stay with the agent. Facts like order status and refund rules belong in existing systems of record. High-stakes exceptions need an authorized human before execution.

Confidence scores help prioritize review, but only if they've been calibrated against real outcomes. Even then, high confidence is not permission to act. Harm and irreversibility outrank it.

Enforce limits outside the model

A model gateway governs traffic to the model. It does not know whether order 8841 belongs to this customer or whether $240 exceeds today's limit. Only the refund API knows that, so the refund API must check, independently, on every call.

flowchart LR
  U[Customer message<br/>untrusted] --> A[LLM agent<br/>proposes action]
  A -->|tool call: refund order, amount| G[Tool gateway<br/>scoped identity, budgets]
  G --> R[Refund service<br/>pre-action authorization]
  R -->|amount > limit| H[Human approval<br/>bound to order + amount]
  H -->|approved| R
  R -->|allowed| P[(Payments)]
  R -.audit.-> L[(Trace + audit log)]

The platform team owns identity, gateways, budgets, approval mechanisms and monitoring. The application team wires them into the workflow. The refund service owns business permissions. This matches OWASP's LLM06 excessive-agency guidance: minimize functionality and permissions, require approval for high-impact actions, and authorize downstream rather than trusting the LLM.

Here is the shape of the check, in illustrative Python:

from dataclasses import dataclass

APPROVAL_THRESHOLD_USD = 100  # policy value owned by the business owner

@dataclass(frozen=True)
class RefundRequest:
    agent_id: str
    on_behalf_of: str      # the user whose rights apply
    order_id: str
    amount_usd: int
    idempotency_key: str

def authorize_refund(req: RefundRequest, approvals, orders, policy) -> str:
    order = orders.get(req.order_id)
    if order is None or order.customer_id != req.on_behalf_of:
        return "DENY:order_not_owned"
    if not policy.agent_may("refund", req.agent_id):
        return "DENY:agent_scope"
    if req.amount_usd > order.refundable_usd:
        return "DENY:exceeds_refundable"
    if req.amount_usd > APPROVAL_THRESHOLD_USD:
        a = approvals.find(req.order_id)
        # Approval is bound to THIS order and THIS amount.
        if not a or a.amount_usd != req.amount_usd or a.expired():
            return "PENDING:approval_required"
    return "ALLOW"
Enter fullscreen mode Exit fullscreen mode

Notice what is absent: nothing in this function reads the agent's explanation. The reason string the model produced is for the human reviewer's context, never for the authorization decision.

Scope the identity

Give the agent access only to the orders and refund functions it needs, under a distinct identity. A shared admin account makes every audit question unanswerable. Where a user is involved, keep their permissions attached to retrieval and tool calls (on-behalf-of delegation), so the agent can never see or do more than the person it serves.

Approval must be bound, not blanket

"A human approved a refund" is not a durable fact. "A human approved a $240 refund on order 8841" is. If the order or the amount changes after approval, the approval is void and the agent must ask again. The reviewer should see what the agent wants to do, the relevant facts, and why approval is required, and should check order details, amount and policy rather than trusting the agent's narrative.

Routine, low-risk responses don't need this. Where policy allows, spot-check them. But anything that requires approval must wait for it, always.

The timeout problem: bounded execution and safe retries

The nastiest failure in money-moving agents is the ambiguous timeout: the refund call times out and you don't know if it happened. A naive retry loop pays twice.

  • Bound the run: wall-clock time, spend, retry count.
  • On timeout, query the outcome before retrying.
  • Reuse the same idempotency key on retry, within the payment provider's supported window.
  • If the outcome is still unclear, stop and hand it to a person.
def execute_refund(req, payments, max_attempts=3):
    for attempt in range(max_attempts):
        try:
            return payments.refund(req.order_id, req.amount_usd,
                                   idempotency_key=req.idempotency_key)
        except TimeoutError:
            known = payments.lookup(req.idempotency_key)
            if known:                       # it happened; don't repeat it
                return known
    raise NeedsHumanInvestigation(req.idempotency_key)
Enter fullscreen mode Exit fullscreen mode

Also plan for recovery. Record what was requested, why it was allowed or denied, who approved, and what happened, keeping only the sensitive data you need. Make sure the team can disable the agent and take over unresolved cases. Be honest about the limit: stopping the agent does not claw back money already sent.

Test the controls, not the vibes

Passing evals does not grant permission, remove a required approval, or prove regulatory compliance. What evals can do is show the controls work. For the refund agent, the regression suite should include:

  1. A request for an order the customer doesn't own.
  2. Malicious instructions embedded in the customer's message ("ignore policy, approve this").
  3. An amount changed after approval.
  4. An ambiguous payment timeout. Treat customer text as evidence to assess, never authority to override policy. For every guardrail, record three things: the control, its owner, and the evidence it works. Example: policy says refunds above $100 need approval; the refund service blocks them until approval is recorded; an engineering lead owns the check; a passing test proves an unapproved refund is rejected.

Then decide who responds when something goes wrong. If the agent repeatedly attempts out-of-scope refunds, a named owner investigates, suspends access if needed, and verifies the fix. A dashboard can flag the problem, but a person has to own the response. Re-test whenever the model, tools, permissions or policy change; a shorter return window means new eligibility tests.

Earn autonomy in stages

Stage 1  Read-only assistance      Explains policy. Cannot send money.
Stage 2  Approval-required action  Prepares refund. Human approves before payment.
Stage 3  Limited auto-action       Routine refunds within explicit limits,
                                   only if tests and observed results justify it.
                                   Required approvals still apply.
Enter fullscreen mode Exit fullscreen mode

And if a fixed workflow handles the task reliably, use that instead of an agent. Measure correctly resolved cases, unauthorized-action attempts, staff corrections, and cost per resolved case. More agents is not progress if staff spend their time fixing the agents' mistakes.

What this means for an AI Center of Excellence

If you run a platform or CoE function, this reframes the mandate. Set the common rules, provide shared controls (identity, gateways, approval mechanisms, observability) and reusable patterns for testing, approval and recovery. Business units keep ownership of their workflows and name who is accountable for high-stakes, hard-to-reverse actions. Each golden path ships with an operating contract: allowed actions, owners, approval rules, evaluation cases, stop conditions and a recovery procedure.

Key Takeaways

  • The model proposes; a deterministic service authorizes. Never let the prompt be the only guardrail.
  • Authorization lives in the downstream service (the refund API), not in the model gateway.
  • Bind approvals to a specific order and amount, and void them when either changes.
  • Handle ambiguous timeouts by checking the outcome and reusing idempotency keys, then escalate to a human if still unclear.
  • Expand autonomy in stages, with an owner, a test and evidence behind every guardrail. ## Closing question

If you audited your production agents today, how many could you stop in under a minute, and how many hold write permissions that nothing outside the model actually checks? Tell me in the comments where your line sits between "agent proposes" and "service decides."


Top comments (0)