You can authenticate an agent perfectly and still hand it a loaded gun. Authentication answers who the agent is and whose authority it is borrowing — we covered that in workload vs delegated identity. This piece answers the other half: given a valid identity, what are the credentials it holds actually allowed to do? That is authorization, and it is where least privilege lives.

The reason to do this today rather than later is that the market just repriced it. On July 28, Cyera agreed to acquire Oasis Security for about $1 billion to govern non-human identities — the API keys, service accounts, and agent tokens your software carries — citing a ~500% surge in machine identities inside Fortune 500 firms in six months. Agent access control is now a category buyers expect you to have thought about. Our governance playbook covers the org-level moves — inventory, owners, lifecycle. This is the companion how-to it points to: scoping the credentials of one shipped agent, in code.

The default that hurts you: one fat token#

Most agents start with a single, long-lived API key that can do everything the account can. Then a tool gets added, then a sub-agent, then an MCP server — and every one of them can spend that key on anything its scope allows. That is the confused deputy waiting to happen: legitimate authority, wrong task. Prompt injection makes it worse, because an attacker who can steer the model inherits whatever the token can reach. Least privilege is the fix, applied at four layers.

1. Scope the key itself#

Start at the provider. Issue read-only scopes where the agent only reads, and mint separate keys per capability so a leak is contained to one job. A billing-reader key and a repo-writer key should never be the same string. Most providers support scoped keys; use them, and stop reaching for the account-admin token because it was faster.

2. Enforce a per-tool allowlist in your own code#

The model should not decide which tools exist — your code should. Keep an explicit allowlist and deny by default:

ALLOWED_TOOLS = {"search_docs", "read_ticket", "post_comment"}

def dispatch(tool_name, args):
    if tool_name not in ALLOWED_TOOLS:
        raise PermissionError(f"tool '{tool_name}' not in allowlist")
    return TOOLS[tool_name](**args)

Three lines. Now a hallucinated or injected tool call fails closed instead of executing. Adding a capability becomes a deliberate edit, not an accident.

3. Exchange for short-lived credentials, per task#

Replace the static secret with a token minted for one task and expiring in minutes. OAuth 2.1 with RFC 8693 token exchange lets the agent trade its identity plus the user's grant for a token scoped to a single action and audience:

token = sts.exchange(
    subject_token=user_grant,        # whose authority
    actor_token=agent_identity,      # which agent
    scope="invoices:read",           # exactly one action
    audience="billing-api",          # exactly one service
    lifetime=300,                    # 5 minutes
)

If that token leaks, the blast radius is one task and five minutes — not your account, forever. For the full plumbing — cloud STS, Vault dynamic secrets, and a token broker — see How to Give an AI Agent a Short-Lived, Scoped Credential.

4. Under MCP's stateless spec, scope the handle#

The 2026-07-28 MCP specification removed protocol-level sessions. Servers that need cross-call state now mint an explicit handle and the client passes it back as an ordinary tool argument. Treat that as a scoping lever: bind the handle to exactly the resource and permission the task needs, so a later, manipulated tool call cannot travel outside it.

open_dataset(id="q3-invoices")  ->  handle "ds_9f3k2"  # scoped to q3-invoices only
query(handle="ds_9f3k2", ...)                          # cannot reach q4, or anything else

Because tools/list no longer varies per connection, do per-caller narrowing here — at the handle and its arguments — not by hiding tools from some callers.

Least privilege is not an audit you pass once. It is a default you encode, so the next tool a teammate adds cannot silently widen the blast radius.

The artifact that proves it: a permission manifest#

Write down what the agent holds. One checked-in file — scope, TTL, and a human owner for every credential:

credentials:
  - name: billing_reader
    scope: invoices:read
    ttl: 300s
    owner: dex
  - name: ticket_writer
    scope: tickets:comment
    ttl: 900s
    owner: priya

This is the answer to "what can your agent access?" — and, not coincidentally, exactly what an enterprise security questionnaire is trying to pull out of you. Written once, it turns a sales-blocking scramble into a lookup. And it makes the two highest-risk gaps visible at a glance: any credential with no expiry, and any credential no human owns. Close those two first; the rest is tightening scopes toward the minimum. Do it now, while your surface still fits in one file — retrofitting least privilege onto a widely-deployed agent costs far more than defaulting to it today.