Excessive Agency in AI Agents: How to Scope Permissions

Isometric illustration of an agent node with a sharp boundary arc cutting off most of a surrounding ring of tool icons

What this costs you if you get it wrong: give an AI agent more tools, broader permissions, or more autonomy than the task in front of it actually requires, and you've built an attack surface that a single manipulated input can walk straight through - this is exactly the risk OWASP's GenAI Security Project ranked third in its 2026 Top 10 for LLM Applications, right behind prompt injection and sensitive information disclosure, and named LLM06: Excessive Agency. In practice, this means a compromised or manipulated agent doesn't need to break your authentication - it just needs to ask, using whatever permissions you already handed it. This post breaks down the three root causes OWASP identifies, walks through a real attack pattern that combines all three, and gives you the two controls - scoped runtime credentials and complete mediation - that actually shrink the blast radius, with working TypeScript for both.


1. What Is Excessive Agency in an AI Agent?

OWASP defines excessive agency as occurring when an LLM-powered system is granted more functionality, permissions, or autonomy than its task requires, enabling it to perform unintended or harmful actions - and breaks the risk into three distinct, independently-fixable root causes. Excessive functionality means the agent can reach tools or capabilities beyond its task's scope - a summarization agent that happens to have delete access to the mailbox it's reading. Excessive permissions means the tools it does need operate with privileges broader than necessary - read access when read-only would do, a single service account shared across every user instead of per-user scoping. Excessive autonomy means high-impact actions proceed without a human checkpoint - sending an email, deleting a record, or executing a financial transaction happens the moment the model decides to, with no approval gate in between.

Why This Risk Got More Urgent in 2026, Not Less

Excessive agency was already tracked in OWASP's 2025 guidance, but the 2026 edition expanded it significantly as agentic deployments moved from demos into production. The reasoning is straightforward: a chatbot with excessive agency can say something wrong; an agent with excessive agency can do something wrong - modify a database, send a message, move money - and that action doesn't roll back the way a bad response does.


2. What Does Excessive Agency Actually Look Like in Production?

In practice, this means understanding how the three root causes combine into a real exploit, not just listing them abstractly. OWASP's own reference scenario is the clearest version of this: an LLM-powered personal assistant summarizes emails using a mailbox extension. That extension includes both read and send capabilities (excessive functionality - send was never needed for summarization), authenticates using broad mailbox permissions (excessive permissions), and doesn't require approval before sending mail (excessive autonomy). A malicious email arrives instructing the agent, via indirect prompt injection, to search the inbox for sensitive information, compile it, and email it to an attacker-controlled address. Because all three excesses are present at once, the agent complies - not because it was "hacked" in the traditional sense, but because nothing in the system's design ever stopped it from being asked.

This is the pattern worth internalizing: excessive agency isn't usually one glaring misconfiguration. It's three individually-reasonable-looking decisions - "let's just give it the standard mailbox scope," "auto-send saves a click," "we'll add approval gates later" - that compound into a working exfiltration path the moment an attacker finds the right prompt.


3. Which Credential Model Actually Limits the Damage?

❌ SHARED HIGH-PRIVILEGE SERVICE ACCOUNT

One long-lived API key or OAuth token, scoped broadly enough to cover every possible task the agent might ever need, reused across every user and every request.

I've audited a client's agent where the "summarize my inbox" feature ran under a mailbox credential that also had send, delete, and calendar-write scopes - none of which the feature used, all of which were exploitable.

✅ SCOPED, PER-TASK RUNTIME CREDENTIAL

A short-lived credential minted per task, scoped to exactly the permissions that task declares it needs - read-only for summarization, no send scope ever issued for that task type.

A successful prompt injection against this agent can only do what a read-only scope allows - there's no send capability to exfiltrate through, because it was never granted.


4. How Do You Actually Scope What an Agent Is Allowed to Do?

The control that matters most here is what OWASP calls complete mediation: never rely on the LLM itself to decide whether an action is authorized. Every downstream system the agent touches must independently enforce authorization and policy checks - the model's output is a request, not an approval. Here's a policy-enforcement layer that sits between an agent's tool-call output and the actual execution, checking every call against an explicit allow-list scoped to that task type:

// agent-policy-gate.ts
// Enforces least-privilege access on every tool call an agent makes.
// The model's decision to call a tool is never sufficient authorization
// on its own - this gate independently checks it against policy.
interface ToolCallRequest {
  taskType: string;
  toolName: string;
  scope: "read" | "write" | "delete";
}

interface TaskPolicy {
  allowedTools: Record<string, Array<"read" | "write" | "delete">>;
}

// Policies are declared explicitly per task type, not inherited from
// a broad default - a new task type starts with zero permissions.
const TASK_POLICIES: Record<string, TaskPolicy> = {
  email_summarization: {
    allowedTools: { mailbox: ["read"] },
  },
  email_triage_with_labeling: {
    allowedTools: { mailbox: ["read", "write"] }, // labeling, not sending
  },
};

export function authorizeToolCall(request: ToolCallRequest): void {
  const policy = TASK_POLICIES[request.taskType];

  if (!policy) {
    // An unrecognized task type gets zero implicit trust - fail
    // closed, not open.
    throw new Error(`No policy defined for task type: ${request.taskType}`);
  }

  const allowedScopes = policy.allowedTools[request.toolName];

  if (!allowedScopes || !allowedScopes.includes(request.scope)) {
    throw new Error(
      `Excessive agency blocked: task '${request.taskType}' is not authorized for ` +
      `'${request.scope}' access on tool '${request.toolName}'.`
    );
  }
}

For the subset of actions that are high-impact regardless of how well-scoped the credential is - sending a message, deleting a record, executing a payment - OWASP's guidance is explicit: bound autonomy with a human-in-the-loop checkpoint. Here's that gate as middleware, logging every decision for audit regardless of outcome:

// human-approval-gate.ts
// Requires explicit human approval before any high-impact action
// executes, regardless of how confident the agent's reasoning was.
import { prisma } from "./db";

interface HighImpactAction {
  actionType: "send_message" | "delete_record" | "execute_payment";
  requestedBy: string; // agent/task ID
  payload: Record<string, unknown>;
}

const HIGH_IMPACT_ACTIONS = new Set(["send_message", "delete_record", "execute_payment"]);

export async function requestApproval(action: HighImpactAction): Promise<{ approvalId: string }> {
  if (!HIGH_IMPACT_ACTIONS.has(action.actionType)) {
    throw new Error(`${action.actionType} is not registered as high-impact - check policy config`);
  }

  // The action is logged as pending BEFORE any execution path can
  // touch it - there is no code path that executes a high-impact
  // action without a corresponding approval record existing first.
  const record = await prisma.pendingApproval.create({
    data: {
      actionType: action.actionType,
      requestedBy: action.requestedBy,
      payload: JSON.stringify(action.payload),
      status: "pending",
    },
  });

  return { approvalId: record.id };
}

export async function executeIfApproved(approvalId: string): Promise<void> {
  const record = await prisma.pendingApproval.findUniqueOrThrow({ where: { id: approvalId } });

  if (record.status !== "approved") {
    // This is the enforcement point - an agent cannot skip straight
    // to execution by calling this function early or twice.
    throw new Error(`Action ${approvalId} is not approved (status: ${record.status})`);
  }

  // Execution logic dispatches by actionType here, reading the
  // stored payload - never re-trusting a fresh value from the agent.
}

5. How Much Does This Actually Reduce Your Attack Surface?

Blast Radius Reduction

R = 1 − (A_needed / A_granted)

R: % reduction in exploitable action surface
A_needed: actions the task actually requires
A_granted: actions the credential currently allows
Broad Service Account (Typical Default Scope)
A_needed=1, A_granted=40 → R=97.5% unused attack surface
A summarization task granted a full mailbox+calendar+contacts scope set has 39 exploitable actions it never needed, sitting there for a prompt injection to reach.
Task-Scoped Credential
A_needed=1, A_granted=1 → R=0% unused attack surface
A compromised prompt still can't send, delete, or write anything - the read-only scope is the entire exploitable surface, by design.

These figures are an illustrative model built on a typical enterprise mailbox/calendar/contacts tool catalog size, not a universal number - audit your own agent's actual granted-vs-needed scope to get the real figure. As a Certified Project Manager, the checklist I now run before any client agent ships to production is short and non-negotiable: list every tool the agent can call, mark which ones the current task actually uses, and treat every unused entry as a finding, not a convenience.


6. Conclusion and Actionable Roadmap

Excessive agency isn't a hypothetical risk category - it's the mechanism behind some of the most damaging real-world agent exploits, and OWASP ranking it third in the 2026 Top 10 for LLM Applications reflects how directly production incidents have traced back to exactly this pattern: too much functionality, too much permission, too much autonomy, compounding together. The fix isn't making the model smarter or harder to manipulate - OWASP's own project leaders explicitly recommend assuming the model will get fooled and building the surrounding system so that when it does, nothing critical breaks. Scoped, per-task credentials and complete mediation through independent authorization checks are how you build that system, and the blast-radius math above shows exactly why the effort is worth it: the difference between a contained incident and a full account compromise is often just the unused scope you never bothered to remove.

Audit what your agents can actually do before an attacker finds out for you: I implement least-privilege credential scoping and human-approval gates for production AI agents on Next.js, TypeScript, and Node - the exact controls in the code above. Contact me today to book a 30-minute agent permissions security audit.

Free Scoping Session

Have Something to Build?

Pick what you're trying to build below, and see exactly what a working engagement with me looks like - timeline, stack, and deliverables.

Product LaunchEst. Timeline: 4 to 8 Weeks

Build a SaaS MVP Roadmap

Turn your idea into a production-ready SaaS - architected, built, and shipped by one engineer, not a handoff chain.

Tech Stack

Next.js 16 + Tailwind v4 + PostgreSQL or MongoDB

Deliverables

Fully functional app with auth, billing, and database integrations.

Included With Your Scoping Call

MoSCoW-scoped feature list and a database architecture roadmap.

🔒 NDA Available⚡ Free scoping call, no obligation