# AI agent runtime authorization: budgets and retries

> AI agent runtime authorization: add call budgets, risky tool-combination rules and idempotency keys, because correct scopes still misfire.

- **Published:** October 2, 2026
- **Category:** Guides
- **Author:** Udit Goenka
- **URL:** https://communicate.so/blog/ai-agent-runtime-authorization

---

> **TL;DR:** OAuth scopes tell you what a token may do in principle. Runtime authorization decides whether this call, with these arguments, in this session, should run right now. Add three runtime checks to any support agent that can change records: call budgets so a loop cannot run away, combination rules so risky tool pairs are blocked, and idempotency keys so a retry after an unclear outcome cannot repeat a side effect. Test the policy offline and test the real tool boundary separately.

---

I approach this topic as a failure-mode reviewer, the person whose habit is to ask what happens on the second call. A permission system that grants the right access can still produce a wrong action, because the agent may call too often, combine tools in a harmful order, or retry after a timeout that hid a success. This guide covers the checks that sit between a correct permission and a correct outcome.

It builds on the scope and token controls in the [MCP security guide for support agents](/blog/mcp-security-support-agents), so read that first if your tokens are not yet split by action. Here I assume the scopes are already narrow and ask what is still missing. The answer is a layer that watches behavior over time.

The evidence comes from the MCP specification, OWASP, Stripe's API documentation, the OAuth Rich Authorization Requests standard, and a recent thread in the r/mcp community where practitioners compared notes on testing agent permissions. As of October 2026, none of these checks is built into every client, so you will often implement them in your own tool layer.

## Correct permissions, wrong actions

OWASP's entry on [Excessive Agency](https://genai.owasp.org/llmrisk/llm062025-excessive-agency/) names three root causes, which are excessive functionality, excessive permissions, and excessive autonomy. Scopes address the first two. The third is about what the agent does with the access it legitimately has.

Support work shows the gap clearly. An agent that is allowed to update tickets is allowed to update each ticket, and nothing in a scope says how many updates are reasonable. The same token that fixes one tag can rewrite four hundred.

Three failure patterns recur once permissions are right.

- Too many calls happen when a loop or a confused model repeats an allowed action far past any sensible count.

- Risky combinations happen when two individually safe tools, such as reading a customer export and sending an email, form a dangerous pair.

- Retries after an unclear outcome happen when a request times out, the agent cannot tell if it worked, and it sends the action again.

Each pattern passes an authorization check at every step. The token is valid, the scope is present, and the user consented. The harm lives in the sequence, which is why the control has to live at runtime.

The [AI agent guardrails guide](/blog/ai-agent-guardrails) covers policy boundaries in the abstract. This article makes three of them concrete enough to code.

## Scopes, runtime checks, and approvals do different jobs

It helps to keep the layers separate in your head, because teams often assume one covers the others. A scope is static and attached to a token. A runtime check is dynamic and looks at the call, the arguments, and the history. 

An approval is a person confirming one specific action.

| Layer | Question it answers | Catches a runaway loop | Catches a risky pair | Catches a duplicate retry |
| --- | --- | --- | --- | --- |
| OAuth scope | May this token call this kind of action at all? | ✗ | ✗ | ✗ |
| Call budget | Has this session made too many calls? | ✓ | ✗ | ✗ |
| Combination rule | Is this call allowed after the calls already made? | ✗ | ✓ | ✗ |
| Idempotency key | Has this exact action already been executed? | ✗ | ✗ | ✓ |
| Human approval | Does a person agree to this specific action? | ✓ | ✓ | ✗ |

The table shows why approval alone is not a full answer. A person can approve one action and the executor can still run it twice. A person can also approve each step of a sequence without seeing that the sequence leaks data.

The original poster in the r/mcp thread on [testing whether agent permissions became too broad](https://www.reddit.com/r/mcp/comments/1wbzkpb/how_do_you_test_that_an_ai_agents_permissions) makes the same point about approval. They write that "Approval also doesn't guarantee exactly-once execution; the underlying tool still needs an appropriate retry/idempotency strategy." The poster describes maintaining the open-source Nomos project, so read the framing as that of an interested builder.

## Call budgets: stop loops before the vendor does

A budget is a counter with a ceiling. It is the simplest runtime check, and it stops the widest class of accidents. When the ceiling is reached, the session stops and hands the case to a person.

Your help desk already has ceilings, and you should design below them. Zendesk's [rate limit page](https://developer.zendesk.com/api-reference/introduction/rate-limits/) caps updates to the same ticket at 30 per 10 minutes per user, and caps Suite Team accounts at 200 requests per minute. A looping agent will reach those numbers fast, and when it does, the same API identity your human agents use gets throttled.

A good budget has several dimensions, because loops take different shapes. One agent might call a single tool thousands of times. Another might touch thousands of different tickets once each.

- A total call budget per session ends any loop, whatever its shape.

- A write budget per session is set lower than the read budget, since writes carry the risk.

- A per-target budget limits how many times one ticket or contact can be modified.

- A distinct-target budget limits how many different tickets a session may modify, which bounds mass-edit damage.

- A time window budget limits calls per minute, so bursts slow down before the vendor throttles you.

Set the numbers from observed behavior. Run the agent in a staging workspace, record how many calls a normal task needs, and set the ceiling at a modest multiple of that. A ceiling that normal work never touches is safe to enforce, and one that normal work often touches is too tight.

Budgets also help with cost. A loop that calls a language model repeatedly consumes spend as well as API quota, a topic the [LLM cost optimization guide](/blog/llm-cost-optimization-support) treats in detail.

When a budget trips, make the failure visible. Return a clear error to the model and the user, write it to the log, and flag the conversation for review. A silent stop looks like a bug, and a visible stop becomes a signal you can act on.

## Combination rules: some pairs should never meet

Simon Willison's [lethal trifecta](https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/) is a combination rule in its most general form. An agent that has access to private data, is exposed to untrusted content, and can communicate externally is exploitable, even if each ability alone is harmless. His conclusion for end users is that "the only way to stay safe there is to avoid that lethal trifecta combination entirely."

You can encode that idea as a runtime rule. Track which capabilities a session has used, and deny a call that would complete a dangerous set. Reading raw inbound customer text marks the session as having seen untrusted content, and after that mark, outbound tools require approval or are denied.

The same method covers narrower pairs that are specific to a help desk. These are the ones I would write down first.

- After a bulk export or wide search, deny any outbound send in the same session.

- After reading a message flagged as suspicious, deny any tool that changes account ownership or contact details.

- After a refund above a set amount, require approval for the next refund in the same session.

- When a session reads data for one customer, deny a read for a different customer without a new request from a person.

- Deny any tool call that is not in the session's declared tool set, which also blocks tools added by a later update.

Deny rules should win over allow rules. The original post in the r/mcp thread lists a test where a blocked recipient is denied, "even if another rule requires approval." That precedence matters because approval flows tend to be the most generous path in a system, and a block should survive it.

Combination rules are also where a clean [tool set](/blog/mcp-tool-curation) pays off. If the agent only has the tools a task needs, there are fewer pairs to reason about.

## Idempotency keys: the answer to the unclear outcome

Retries are normal behavior, not a sign of bad design. Networks time out, servers restart, and a model that receives an error will often try again. The danger comes from the case where the first attempt actually worked and the response was lost.

Stripe's [idempotent requests documentation](https://docs.stripe.com/api/idempotent_requests) describes the standard solution. A client generates a unique key, and the server saves the resulting status code and body of the first request for that key, "regardless of whether it succeeds or fails." Subsequent requests with the same key return the same result.

The same page covers details worth copying. Keys can be up to 255 characters and should have enough entropy to avoid collisions, and V4 UUIDs are the suggested form. Keys may be pruned after at least 24 hours, and the server compares incoming parameters with the original request and returns an error if they differ, which prevents accidental misuse.

The rule for an agent is simple. Every write that has a side effect carries an idempotency key, generated once per intended action and reused on every retry of that action. A new intent gets a new key.

There is also an IETF effort to standardize the header, described in the [Idempotency-Key HTTP header field draft](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/) from the HTTP API working group. If the help desk or payment API you call supports a key, pass it. If it does not, add a deduplication table in your own tool layer.

A deduplication table needs only a few columns: the key, the tool name, a hash of the arguments, the stored result, and the time. Before running a write, the layer looks up the key. If it finds a match with the same hash, it returns the stored result, and if the hash differs, it refuses the call.

Which actions need keys? Anything that creates, sends, charges, or refunds. Reads and deletes by ID are idempotent by definition, a point Stripe notes for GET and DELETE requests. 

Replies are the classic trap, because sending the same email twice to a customer is visible and embarrassing.

The same thinking applies when your agent triggers actions through an API. The [AI agent actions guide](/blog/ai-agent-actions-api) covers designing those calls, and the key belongs in every one that writes.

## Bind approval to the exact action

Approvals fail when they are loose. A person approves a draft, the arguments change slightly between approval and execution, and the executor runs the changed version. The approval then covers something the person never saw.

A commenter in the r/mcp thread, [Enough-Photo9140](https://www.reddit.com/r/mcp/comments/1wbzkpb/how_do_you_test_that_an_ai_agents_permissions), calls this payload drift after review. They write that if the execution gate does not verify an HMAC or SHA-256 hash of the canonicalized payload against the approval ticket, "the approval is decorative." Their test is to flip a single character in the approved payload and confirm that the executor aborts.

That test is cheap and worth stealing. Bind each approval to a hash of the action name, the target, and the exact arguments. Add a short expiry so a stale approval cannot be replayed later.

OAuth has a standard for expressing fine-grained, action-level permissions. [RFC 9396, OAuth 2.0 Rich Authorization Requests](https://www.rfc-editor.org/rfc/rfc9396.html) defines an authorization_details parameter that lets a client request permission for a specific type of action with specific details, instead of a broad scope string. Support varies across authorization servers, so check yours before designing around it.

Whether you use that standard or your own ticket format, the principle holds. Re-run authorization after approval and before execution, because things change in between. Policies get revoked, permissions shift, and a target may be reassigned.

- Hash the canonical form of the action and its arguments when the approval is created.

- Verify the hash again at execution, and abort on any mismatch.

- Expire approvals quickly, so a late click cannot authorize a stale plan.

- Pass an idempotency key with the execution so a retry cannot turn one approval into two actions.

- Check that the policy still allows the action at the moment of execution.

## Test the policy and the boundary separately

The same r/mcp thread offers a clear split between two kinds of tests. Policy tests ask whether, given an identity, an action, a resource, and arguments, the policy returns the expected decision, and they "can run offline in CI." Enforcement tests ask whether the real tool boundary actually blocks what the policy denies. The poster adds that "a policy can be correct while application code accidentally bypasses it."

Write both. A policy test is a table of cases with expected decisions, and it runs in milliseconds. An enforcement test runs the agent against a staging system and checks side effects, such as the count of messages sent, and it asserts that the count stays at zero for a denied case.

For a messaging tool, the poster lists expectations that translate well to a help desk. Drafting a reply is allowed. Sending to an approved recipient requires human approval. 

Sending to a blocked recipient is denied. Exporting all messages is denied. Calling an unknown tool is denied. 

Accessing another inbox is denied.

Another commenter, Portotify, who discloses being a founder at Portablemind, adds a subtle failure. They report that their evaluation setup could not tell a model that declined to call a tool apart from a tool that was never available. A test that asserts the agent did not export all messages passes trivially if the export tool was never wired up, and then someone wires it up.

The fix they describe is to declare the expected tool set in each scenario and assert the catalog before asserting behavior. I would add one more case from the thread, which is a deliberately harmful request that is correctly authorized. It passes the permission check and fails a separate safety criterion, so a green permission suite is never mistaken for evidence of good judgment.

Fold these cases into the regression suite you already run. The [agent evaluation and testing guide](/blog/ai-agent-evaluation-testing) describes how to structure that suite, and the permission cases are a new column in it.

## Log every decision, including the denials

A runtime layer produces decisions, and each decision is evidence. Log the allow decisions and the deny decisions, with the rule that fired. A denial that never appears in a log cannot teach you anything.

- Record the session, the tool, the arguments, and the decision for every call.

- Record which rule produced a denial, so you can tune budgets and combinations with real data.

- Record the approval ticket and its hash for every approved action.

- Record the idempotency key and whether the result came from execution or from a stored replay.

- Review denials weekly, since a rule that never fires may be too loose and one that fires constantly may be too tight.

These records are the raw material for the [AI support audit trail](/blog/ai-support-audit-trail), and they let you answer why the agent did or did not act. They also support quality review, which the [support quality assurance guide](/blog/support-quality-assurance-ai) covers from the team's side.

## Roll out in shadow mode first

Do not turn on enforcement everywhere at once. A rule that blocks legitimate work will teach your team to distrust the whole layer. Start by evaluating every call against the policy without blocking, and log what would have been denied.

After a week or two of shadow data, review the would-be denials. Some will show real problems, such as an agent that edits the same ticket twelve times. Others will show a rule that is too tight, and you can adjust it before it affects anyone.

Then enforce in stages. Begin with the checks that carry the least regret, such as budgets and idempotency keys, which almost never block good work. Add combination rules next, and tighten approvals last.

Plan the fallback for every denial. A blocked call should end in a clear handoff to a person, not a dead end for the customer. The [agent fallback design guide](/blog/ai-agent-fallback-design) and the [escalation workflow guide](/blog/support-escalation-workflow) describe how to do that.

If you are still planning the wider rollout, the [implementation guide](/blog/ai-support-agent-implementation) places these controls in the project timeline. Some tasks may not suit an agent at all, a question the [guide on when not to use AI support](/blog/when-not-to-use-ai-support) takes on directly.

## A starter policy for a support agent

This is a concrete starting point you can adapt. The numbers are placeholders to be replaced with values from your own staging data. Treat the structure as the useful part.

- Allow reads of tickets and articles up to a per-session read ceiling.

- Allow draft creation freely, with a ceiling on drafts per ticket.

- Require human approval for sends, bound to a hash and a short expiry.

- Allow tag and status changes up to a small write ceiling, with a per-ticket cap well below the vendor limit.

- Deny deletes, merges, and contact changes unless a person starts them.

- Deny outbound sends once the session has run a bulk export or a wide search.

- Require an idempotency key on every write, and refuse writes that lack one.

- Stop the session and hand off to a person whenever a budget or a deny rule trips.

Where the agent can change CRM data, apply the same policy shape to those calls. The [CRM integration guide](/blog/ai-agent-crm-integration) describes common write paths, and each one needs a key and a ceiling.

Keep the policy in version control, review changes the way you review code, and tie each change to a test case. A policy that lives in a settings page tends to drift. A policy in a repository with tests tends to improve.

## Three scenarios that scopes alone cannot stop

These are illustrations built from the failure patterns above and are not reports of real incidents. Each one passes every permission check, and each one needs a runtime control.

In the first scenario, an agent is asked to tag all tickets about a billing outage. It tags the first batch, hits a vendor throttle, receives errors, and starts again from the top with a slightly different query. Each pass is allowed, and within an hour the agent has updated the same few hundred tickets repeatedly. 

A per-target budget stops this after a few edits, and a total write budget ends the session soon after.

In the second scenario, an agent has a search tool and an email tool, which are both fine on their own. A customer message asks the agent to find every account on a certain domain and email the list to an outside address. Each call is within scope, and the only thing wrong is the sequence. 

A combination rule that denies outbound mail after a wide search blocks it.

In the third scenario, an agent issues a refund and the request times out. The agent cannot tell whether the refund went through, so it issues it again. Without an idempotency key, the customer may receive two refunds, and with a key, the second call returns the stored result of the first.

Real chatbot failures often look like these quiet ones before they look like dramatic ones. The [AI chatbot failures guide](/blog/ai-chatbot-failures) collects public cases, and several involve an automated system doing something allowed in a way nobody intended.

Walk your own workflows through the same three questions. How many times could this action repeat before anyone noticed? Which other tool, used in the same session, would make this action dangerous? 

What happens if the response never arrives?

## Where to put the enforcement layer

A runtime policy is only as good as its position in the path. If the model can reach the help desk by any route that skips the layer, the layer is advice. The cleanest design puts it in the one place every call must pass.

The MCP tools specification says servers MUST implement proper access controls, rate limit tool invocations, and validate all tool inputs. That makes the MCP server a natural home for budgets and combination rules, since it sees every call from a given session. The [MCP server guide for customer support](/blog/mcp-server-customer-support) explains how such a server sits between an assistant and a help desk.

There are three common positions, and each has a trade-off.

- In the MCP server, the policy sees every tool call and can use session history, but you must own the server code or trust the vendor to expose the controls.

- In a gateway in front of the help desk API, the policy applies to every client including non-MCP ones, but it sees less about the session and the model's intent.

- In the client application, the policy can show approval prompts well, but it depends on every user running the same client with the same settings.

Most teams end up with two layers. The server or gateway enforces the hard rules, and the client presents approvals. If a rule matters, enforce it where the model cannot talk its way around it.

The specification also describes step-up authorization, where a server challenges the client for a higher scope when a privileged operation is first attempted. It tells clients to implement retry limits and to track scope upgrade attempts, so repeated failures for the same resource and operation do not loop. That is a budget at the authorization layer, as laid out in the [MCP authorization specification](https://modelcontextprotocol.io/specification/draft/basic/authorization).

If you are weighing a direct API integration against MCP for these checks, the [MCP versus API comparison](/blog/mcp-vs-api-support-automation) lays out where each approach puts the control point.

## Metrics that tell you the layer is working

A policy layer that nobody measures drifts into either uselessness or obstruction. A few simple counts keep it honest. Track them weekly during rollout and monthly afterward.

- The number of sessions that reached a budget ceiling, which shows how often loops or heavy tasks occur.

- The number of denials by rule, which shows which rules do real work.

- The number of approvals rejected by a person, which shows whether the agent proposes bad actions.

- The number of idempotent replays served, which shows how often retries happen and that the protection engages.

- The share of denied calls that turned out to be legitimate on review, which is your false positive rate.

Tie these to the numbers your team already reports. The [customer support KPIs guide](/blog/customer-support-kpis) lists the service metrics that sit beside them, and a spike in handoffs after a new rule is worth a look before you blame the rule.

A low false positive rate with some real denials is the pattern you want. If denials are zero for months, either your agent is well behaved or your rules never fire, and a deliberate test case will tell you which. If denials are constant, the rules are probably tuned for a workflow nobody runs.

## Roles: who owns each control

Runtime authorization crosses teams, and unowned controls decay. Assign an owner to each piece before launch. The owner is the person who answers when the control misfires.

- Support operations owns the approval flow and the handoff text a customer sees after a denial.

- Engineering owns the tool layer, the policy code, and the idempotency store.

- Security owns the combination rules and reviews new tools before they join the catalog.

- A named approver owns each class of approval, with a backup for out-of-hours cases.

Shared queues need clear rules for who sees what when an agent hands off. The [shared inbox guide for AI and humans](/blog/shared-inbox-ai-and-humans) and the [human handoff guide](/blog/ai-human-handoff-support) cover the mechanics, and the [shared inbox](/shared-inbox) page shows how communicate.so presents them.

Finally, keep the first launch small. The launch checklist for a first agent recommends a narrow scope and a measured expansion, and runtime controls fit that approach. Start with read and draft actions, add one write, and let the logs show whether the controls hold.

The [first agent launch guide](/blog/launch-your-first-ai-agent) describes that sequence in more detail.

## Frequently asked questions

### What is runtime authorization for an AI agent?

It is a check made at the moment of each tool call, using the identity, the action, the arguments, and the session history. It complements static scopes, which only say what a token may do in principle. Runtime checks catch loops, risky combinations, and duplicate retries that scopes cannot see.

### How is it different from OAuth scopes?

A scope is attached to a token and does not change during a session. A runtime check looks at behavior over time and at the specific arguments. You need both, and scopes should be narrow before you add runtime checks.

### What is the simplest runtime check to add first?

A total call budget per session. It needs only a counter and a ceiling, and it ends any loop regardless of cause. Add a lower ceiling for writes next.

### How do I choose budget numbers?

Run normal tasks in a staging workspace, record how many calls they need, and set the ceiling at a modest multiple. Normal work should rarely touch it. Review the denials and adjust.

### What is an idempotency key?

It is a unique value the client sends with a write request so the server can recognize a retry of the same request. The server stores the first result and returns it for later requests with the same key. Stripe documents this pattern in its API reference.

### Do all agent actions need idempotency keys?

Actions that create, send, charge, or refund do. Reads are safe to repeat, and Stripe notes that GET and DELETE requests are idempotent by definition. When in doubt, add a key to any write.

### What if the help desk API does not support idempotency keys?

Add a deduplication table in your own tool layer. Store the key, the tool, a hash of the arguments, and the result, and return the stored result on a repeat. Refuse the call if the same key arrives with different arguments.

### What is a combination rule?

It is a rule that depends on the calls a session has already made. For example, deny outbound sends after a bulk export. It encodes the idea that two safe tools can form an unsafe pair.

### Why does the lethal trifecta matter here?

Willison's trifecta names private data, untrusted content, and external communication as a dangerous set. A runtime rule can track which of the three a session has touched and deny the call that would complete the set. That turns a general warning into a check you can test.

### Is human approval enough?

No. Approval confirms intent but does not guarantee exactly-once execution, and the arguments can drift between approval and execution. Bind approvals to a hash, expire them quickly, and pass an idempotency key at execution.

### How do I make an approval tamper-evident?

Hash the canonical form of the action name, target, and arguments when the approval is created. Verify the hash at execution and abort on a mismatch. Add a short expiry.

### What are Rich Authorization Requests?

RFC 9396 defines an authorization_details parameter for OAuth that lets a client ask for permission to perform a specific kind of action with specific details. It is more precise than a broad scope string. Check whether your authorization server supports it before depending on it.

### How do I test a permission policy?

Write a table of cases with expected decisions and run it offline in CI. Then run enforcement tests against a staging system and assert that denied cases produce zero side effects. The two kinds of test catch different bugs.

### What does denied versus never offered mean?

A denied tool exists but the policy blocks the call. A never-offered tool is absent from the agent's catalog. Tests should assert the catalog first, or a missing tool will make a denial test pass for the wrong reason.

### Should I enforce rules immediately?

Start in shadow mode, where the layer logs what it would have denied without blocking. Review a week or two of data, tune the rules, then enforce in stages. Budgets and idempotency keys are the safest to enforce first.

### What happens when a rule blocks a legitimate request?

The session should hand off to a person with a clear explanation, and the denial should be logged. Review denials regularly to find rules that are too tight. A visible denial is better than a silent failure.

### Where does the policy live?

In the tool layer between the model and the help desk, so no call bypasses it. Keep the policy in version control with tests. A policy only in a prompt can be argued with and is not enforcement.

### Does this apply to a read-only agent?

Budgets still help with cost and rate limits, and combination rules still matter if the agent can send anything outward. Idempotency matters little without writes. The more the agent can change, the more of these checks you need.

### What did the r/mcp thread add to this topic?

Practitioners listed failure cases worth testing, including payload drift after approval, policy revocation between approval and execution, mixed batches with one denied item, and retries that duplicate side effects. I quote them with links and note that some commenters disclose their own products. Treat the thread as a source of test ideas rather than as proof of any one tool.

### How does communicate.so approach this?

The published MCP server is read-only and documentation-focused, and authenticated API access uses OAuth client credentials with narrow scopes. Runtime checks for write actions belong in the tool layer of whichever agent you run. Review the actions documentation for the controls your workspace offers.

## Permissions say yes, runtime says when

A correct permission is the start of safe automation, and the checks above cover what comes after it. Budgets end loops, combination rules block harmful pairs, and idempotency keys make retries safe. If you want to see how an agent with action controls fits a real support workflow, explore the [actions](/actions) page on communicate.so.
