---
key: "agent-tool-authorization"
title: "Authorize agent tools outside the model"
url: "https://softwaresecurity.com/domains/ai-emerging/agent-security/tool-authorization/"
api: "https://softwaresecurity.com/api/v1/articles/agent-tool-authorization.json"
domain: "ai-emerging"
topic: "AI security"
type: "Guide"
identifiers: ["CWE-863","CWE-367","LLM03:2026","LLM01:2026","ASI02:2026","ASI03:2026","ASI09:2026","V8","V8.3.1","V8.3.2","V15.4.2"]
tags: ["Access control","LLM application security","AI agents and tools"]
stages: ["design","build"]
roles: ["developer","appsec-engineer","platform-sre"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Authorize agent tools outside the model

Bind each proposed action to the user, resource and approved arguments before allowing a tool to cause a side effect.

Canonical URL: https://softwaresecurity.com/domains/ai-emerging/agent-security/tool-authorization/

## In short

Never let model output decide whether a tool runs. A gateway takes the user and tenant from the session, validates arguments against a closed schema and checks current permission on every call. For consequential actions, store an approval bound to the exact arguments and resource version, consume it and create the operation in one transaction, and send the operation ID to the provider as an idempotency key so a retry cannot repeat the effect.

- Excessive Agency is LLM03:2026 and Tool Misuse and Exploitation is ASI02:2026: risk grows with every permission, tool and autonomous step an agent holds.
- Schema validity is not permission. A well-formed invoice ID can still name another customer’s invoice.
- An approval binds the user, tenant, tool, resource version and every material argument, plus an expiry; any change needs a new decision.
- Reading a consumed flag and then updating it races. Consume the approval and create the operation in one atomic statement or transaction.
- A unique local row stops duplicate dispatch, but only a provider idempotency key or reconciliation stops a duplicate external effect after a crash.
- Approval screens must show the exact arguments the gateway binds; a model-written summary can hide a changed recipient (ASI09:2026).

## Recommended defaults

| Setting | Value | Note | Source |
| --- | --- | --- | --- |
| Approval binding | User, tenant, tool, resource ID and version, every material argument, expiry | — | OWASP AI Agent Security Cheat Sheet |
| Approval use | Single use, consumed in the same transaction that creates the operation | — | ASVS v5.0.0-15.4.2 (check and act as one atomic operation) |
| Permission check | At execution time, against current state; revocation applies immediately | — | ASVS v5.0.0-8.3.2 |
| Provider idempotency key | The operation ID, reused on every retry of that operation | Reconcile ambiguous outcomes instead of retrying with a new key. | — |
| Model-supplied arguments | Closed schema; reject unknown fields and any user, tenant or role field | — | — |

## Verify it

1. Replay the exact approved call twice and confirm one operation row and one provider charge.
2. Change the amount, recipient or invoice version and confirm denial before any provider call.
3. Revoke the user’s permission after approval and confirm execution is denied.
4. Run eight workers against the same approval at once and confirm exactly one operation exists.
5. Stop the worker after the provider call but before the result is stored, restart it, and confirm the retry reuses the same idempotency key.

## What you will learn

- Bind approval to immutable action details and current permission
- Find the race in a check-then-consume approval
- Distinguish retry identity from a new authorized payment

Audience: Application, platform and software security engineers

Before you start:

- Distinguish model proposals from user authority, approved parameters and durable side effects.

Time: About 18 minutes: 6 to read and 12 for the exercise

## A proposed action is not a permission grant

A model can propose a tool call because of a user request, a retrieved document or an attacker-controlled response (see /domains/ai-emerging/llm-security/prompt-injection-boundaries/). The tool gateway must decide independently whether the authenticated user and agent may perform it. A system prompt saying “only act when authorized” cannot substitute for that decision. The OWASP Top 10 for LLM Applications 2026 ranks this Excessive Agency (LLM03:2026), and the Agentic Top 10 calls the same failure Tool Misuse and Exploitation (ASI02:2026).

List each capability by its actual effects: reading records, sending information, modifying state, spending money or executing code. Separate narrow operations instead of giving one generic shell or unrestricted HTTP tool enough authority to do all of them. The model must not choose its own authenticated principal or expand its credential scope.

## Worked payment: approval is a record, not a sentence

The user approves payment P7 for invoice I7: 5000 cents to recipient V1, under invoice version 4. Store those details with the authenticated user, tenant and expiry. A later proposal for 9000 cents or recipient V2 is a different action. Comparing only the tool name or invoice ID would let a material change pass under the old approval.

Current authorization remains necessary. If the user loses payment authority after approval but before execution, the old approval must not override that revocation. The code below puts every condition in one `UPDATE ... RETURNING` statement inside a `BEGIN IMMEDIATE` transaction: the approval must be unconsumed and unexpired, every argument and the invoice version must match, and the user must still hold the `pay` permission. Only if that statement changes a row does the transaction insert the operation. Two workers that both read `consumed=false` separately could otherwise both execute.

The local transaction guarantees at most one authorized operation, but the external payment needs its own idempotency. A crash after the provider commits and before the worker stores the result is not solved by asking the model to avoid retries. Send the operation ID as the provider’s idempotency key, for example an `Idempotency-Key` header, and reconcile ambiguous outcomes by querying the provider with the same key. Approval, concurrency control and external idempotency solve related but different problems.

Keep the approval display trustworthy: it must show the same target and values the gateway binds. A model-generated summary saying “pay the approved invoice” can conceal a changed recipient. The execution service loads authoritative values instead of accepting a model-selected user or tenant field.

```python
import sqlite3
import uuid

SCHEMA = """
CREATE TABLE permissions (
  user_id TEXT, tenant TEXT, action TEXT, PRIMARY KEY (user_id, tenant, action)
);
CREATE TABLE approvals (
  id TEXT PRIMARY KEY, user_id TEXT NOT NULL, tenant TEXT NOT NULL,
  invoice_id TEXT NOT NULL, invoice_version INTEGER NOT NULL,
  amount_cents INTEGER NOT NULL, recipient TEXT NOT NULL,
  expires_at INTEGER NOT NULL, consumed_at INTEGER
);
CREATE TABLE operations (
  id TEXT PRIMARY KEY, approval_id TEXT NOT NULL UNIQUE, status TEXT NOT NULL
);
"""


def claim_payment(db, call, now):
    """Consume approval and create the operation in one transaction.

    `call` holds the authenticated user and tenant from the session plus the
    proposed arguments.
    Returns the operation id, which is also the provider's idempotency key, or None.
    """
    db.execute("BEGIN IMMEDIATE")  # take the write lock before reading the approval
    try:
        row = db.execute(
            """UPDATE approvals SET consumed_at = :now
               WHERE id = :approval_id AND consumed_at IS NULL AND expires_at > :now
                 AND user_id = :user_id AND tenant = :tenant
                 AND invoice_id = :invoice_id AND invoice_version = :invoice_version
                 AND amount_cents = :amount_cents AND recipient = :recipient
                 AND EXISTS (SELECT 1 FROM permissions
                             WHERE user_id = :user_id AND tenant = :tenant
                               AND action = 'pay')
               RETURNING id""",
            {**call, "now": now},
        ).fetchone()
        if row is None:
            db.execute("ROLLBACK")
            return None  # changed, expired, revoked, foreign or already used: no effect
        operation_id = str(uuid.uuid4())
        db.execute(
            "INSERT INTO operations (id, approval_id, status) VALUES (?, ?, 'pending')",
            (operation_id, call["approval_id"]),
        )
        db.execute("COMMIT")
        return operation_id
    except BaseException:
        db.execute("ROLLBACK")
        raise


def connect(path):
    # isolation_level=None: the code above issues BEGIN, COMMIT and ROLLBACK itself.
    return sqlite3.connect(path, isolation_level=None, timeout=10)
```

## Build a deterministic execution contract

Derive caller identity from the trusted request channel and validate tool arguments against a closed schema that rejects unknown fields. Then perform resource and action authorization against current application state. Schema validity establishes shape, not permission: a well-formed invoice identifier may still name another customer’s invoice.

Use scoped credentials and fixed destination policies in the execution service. For delegated tasks, preserve both user and workload limits. Do not let a tool response, downloaded file or agent-to-agent message supply a new authority grant. Record the proposed action, policy decision and resulting effect with sensitive fields redacted.

## Bind consent to the action that will run

For operations requiring user approval, show the concrete target, arguments and consequences through a trustworthy interface. Store an approval bound to that action, authenticated user, relevant resource version and expiry. If arguments or material state change, obtain a new decision instead of reusing approval for the old proposal.

Address duplicate delivery and time-of-check/time-of-use changes. Use idempotency keys and transaction-level checks appropriate to the operation. A user approving one payment does not authorize repeated payments after retries, and an approval recorded before account access is revoked should not override the latest policy.

## Make approvals hard to exploit

An approval is only as good as what the approver sees. People tend to trust fluent, confident agents, and the OWASP Top 10 for Agentic Applications 2026 lists the abuse of that trust as Human-Agent Trust Exploitation (ASI09): an attacker, or a misaligned agent, steers a person into approving an action or disclosing information they would refuse if they saw the real request, and the person's approval then hides the agent's part in the audit trail.

Render the approval from the stored action, not from the model's explanation: the exact recipient, amount, account, record, diff or command, and the data that will leave. Mark what the agent wrote as agent-generated and show where each input came from. Separate preview from effect, so nothing changes state while the person is still looking. For high-risk actions, ask for independent confirmation, such as re-authentication, a second approver or a check against a record the agent did not supply. Keep approvals rare enough to mean something: let low-risk actions run under policy and send only consequential ones to a person, because a stream of routine prompts trains people to click through. Give users a plain way to report an agent that pressured or misled them, log every request and decision where the agent cannot change it, and compare each agent's actions with its approved workflow so unusual detours stand out. These follow OWASP's ASI09 mitigations.

## Test effects under adversarial proposals

Feed the gateway controlled tool-call fixtures without depending on model behavior. Test allowed actions, another tenant’s resource, changed approved arguments, expired approval, replay and a revoked user. Inspect the resulting ledger or datastore. A refusal sentence is irrelevant if the tool still executed, while a denial-only implementation is incomplete if it breaks every legitimate action. For an agent that keeps proposing calls after denials, the kill switches and run limits in /domains/ai-emerging/agent-security/agent-failure-containment/ stop it.

## Why signed approvals and model-supplied tenants still fail

A signed approval token can establish integrity of its contents while still being replayable. Integrity, expiry, audience and one-time state are separate properties. Decide whether the operation permits repeated use; for this example it does not.

A gateway that checks tenant from the model’s JSON lets the caller choose the security context it is supposed to constrain. Derive identity and session authority from the trusted channel, and validate resource ownership against authoritative state. Reject extra fields rather than silently accepting a model-supplied principal override.

## Place the gateway where agent standards expect it

The OWASP Agent Control Standard (specification v0.1.0, announced by OWASP on 1 September 2026) defines hook methods such as `steps/toolCallRequest` and `steps/toolCallResult`, where a separate policy component can allow, deny or modify a proposed call and inspect its result. It is an early specification: use it to place your checks consistently across agent frameworks, not as evidence that they are sufficient. Its reference Guardian proceeds when it cannot reach a decision unless you set `ACS_ON_DECISION_FAILURE=deny`, so configure it to fail closed.

The checks in this guide map to ASVS v5.0.0 as follows: authorization enforced at a trusted service layer (8.3.1), permission changes applied immediately (8.3.2) and check-and-act performed as one atomic operation (15.4.2). The agent tool gateway lab (/labs/agent-tool-gateway/) and the agent-tool-boundaries project on this page implement the same binding and crash-recovery behavior.

## Check the result against concrete cases

Each row is one proposal against approval A7 and the ledger state that must follow it. The Agent tool gateway lab on this page has you repair a gateway until the same cases pass.

| Case | Expected result | What it establishes |
| --- | --- | --- |
| Exact call by currently authorized U1 | One durable P7 operation | Required payment succeeds once |
| Amount changed to 9000 | Deny before provider call | Approval binds material values |
| Invoice version changes to 5 | Require fresh decision | Approval does not silently survive material state change |
| U1 permission revoked | Deny | Current policy controls execution |
| Two simultaneous exact calls | At most one P7 created | Approval consumption is atomic |

## Guided practice

Evaluate the proposals and then interleave two workers. You can complete this exercise from the state record and timeline.

### Supplied material

```text
Approval A7: user U1, tenant North, action pay, invoice I7 version 4, 5000 cents to V1, operation P7, expires 15:00, consumed=false.
Policy: current payment permission required; exact argument and version match; one operation per approval. Time now 14:00.
C1 exact A7 arguments, U1 authorized.
C2 exact except recipient=V2.
C3 exact after permission revoked.
Race: W1 reads consumed=false; W2 reads consumed=false; W1 calls provider; W2 calls provider; both set consumed=true.
Proposed repair: transaction conditionally consumes A7 and inserts unique operation P7; worker dispatches P7 with stable provider key P7.
```

### 1. Evaluate the calls

Decide C1–C3 and state which evidence the gateway must trust.

Worked answer: C1 can proceed if the transaction acquires the approval. C2 fails recipient binding. C3 fails current permission. Identity and authoritative invoice/permission state come from the application, not the proposal’s self-declared fields.

### 2. Locate the concurrency defect

Explain why `consumed=true` at the end does not show one execution.

Worked answer: Both workers observed the same old false value and performed the effect before either update controlled the other. The final Boolean hides two calls. The condition and operation creation must be coordinated atomically.

### 3. Separate local and remote guarantees

Explain what the proposed transaction fixes and what provider behavior is still needed.

Worked answer: It permits at most one local P7 operation under A7. A provider commit followed by worker crash still needs stable provider deduplication/query semantics and reconciliation; a local unique row alone cannot undo or uniquely identify an unrelated remote effect.

### 4. Choose assertions

State what to inspect after exact, changed, revoked and duplicate proposals.

Worked answer: Inspect durable operations and provider test ledger. Exact authorized input creates one P7; changed and revoked inputs create none; duplicates resolve to the same P7 outcome without another logical charge. A refusal response is not the assertion.

Deliverable: A verdict for C1–C3, an explanation of the race, and the database and provider assertions that would prove at most one payment.

Scope: The approval, workers and provider are synthetic. The exercise covers one approval and one provider; it does not test multi-step plans or delegated agents.

## Executable practice

- [Excessive agency lab: Bind an agent’s action to the approved arguments](https://softwaresecurity.com/labs/agent-tool-gateway/)
- [Enforce retrieval and tool boundaries across crashes: project ZIP](https://softwaresecurity.com/downloads/projects/agent-tool-boundaries-26b9a352bb160338.zip) — Python 3.11+ · standard library

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [OWASP AI Agent Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/AI_Agent_Security_Cheat_Sheet.html)
- [OWASP Top 10 for LLM Applications 2026 (LLM03 Excessive Agency)](https://genai.owasp.org/resource/owasp-genai-llm-top-10-2026/). 2026 edition (3 August 2026).
- [OWASP LLM06:2025 Excessive Agency (2025 list; LLM03 in 2026)](https://genai.owasp.org/llmrisk/llm062025-excessive-agency/). 2025 edition.
- [OWASP Top 10 for Agentic Applications 2026 (ASI02, ASI03, ASI09)](https://genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026/). Version 2026 (December 2025).
- [OWASP Agent Control Standard (v0.1.0)](https://genai.owasp.org/resource/agent-control-standard-acs/). 1 September 2026.
- [OWASP Authorization Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html)