Security  

Building Capability-Based Security for AI Tool Plugins

AI agents become useful when they can do more than generate text.

They can:

Read files
Query databases
Create tickets
Send emails
Call APIs
Modify repositories
Deploy applications
Execute workflows

That capability is also the security problem.

An agent that can access every available tool effectively becomes an identity with broad privileges.

The traditional approach is often:

User
 ↓
AI Agent
 ↓
All Available Tools

A safer architecture is:

User
 ↓
AI Agent
 ↓
Capability Policy
 ↓
Allowed Tools
 ↓
Allowed Operations
 ↓
Allowed Resources

This article explores how to implement that model for AI tool plugins using capability-based security.

The central principle is simple:

An AI agent should receive only the capabilities required for the current task.

This is closely aligned with current security guidance from OWASP and Microsoft, which recommends least-privilege tool access, scoped permissions, server-side authorization, and explicit controls for sensitive operations.

What Is Capability-Based Security?

Capability-based security represents authority as an explicit capability.

Instead of saying:

Agent = Administrator

we define:

Agent
    ├── read_orders
    ├── create_ticket
    └── read_customer_profile

The agent does not automatically receive everything associated with an administrator role.

Its authority is the collection of capabilities explicitly granted to it.

A useful model is:

Capability
=
Action
+
Resource
+
Constraints

For example:

read_customer
+
customer/123
+
read-only

is significantly narrower than:

customer_database
+
full_access

Why AI Agents Need Fine-Grained Capabilities

Traditional applications usually have deterministic control flow.

For example:

if (user.CanDeleteOrder)
{
    DeleteOrder(orderId);
}

An AI agent introduces another decision-maker:

User request
     ↓
LLM
     ↓
Tool selection
     ↓
Tool arguments
     ↓
Application

The model may choose:

Which tool?
Which parameters?
Which sequence?
How many times?

That makes tool authorization especially important.

OWASP recommends granting agents the minimum tools required for their specific task and implementing per-tool permission scoping, including distinctions such as read-only versus write operations.

Capability Security vs Role-Based Access Control

RBAC answers:

"What role does this identity have?"

Capability security answers:

"What exact authority has been granted?"

Both approaches can be useful.

ModelExampleGranularity
RBACAgent = SupportAgentCoarse
ABACDepartment = Support + Region = EUMedium
CapabilityRead ticket 123Fine
Resource capabilityRead /reports/2026/q1Very fine

In an AI system, capabilities can be derived from RBAC or ABAC policies.

The important point is that the final tool invocation should still be authorized server-side.

The Basic Architecture

A secure tool architecture can look like this:

                 User
                   |
                   v
             AI Application
                   |
                   v
              Agent/LLM
                   |
                   v
           Capability Policy
                   |
          +--------+--------+
          |                 |
       Allowed           Denied
          |                 |
          v                 X
      Tool Gateway
          |
          v
      Tool Server
          |
          v
       Resource

The model should not be the final authority.

The authorization layer should be.

Never Use the System Prompt as Authorization

A weak implementation might say:

You are a support agent.
Never delete customer records.
Only read customer information.

That is useful as behavioral guidance.

It is not authorization.

A prompt can be influenced by:

User input
Retrieved documents
Tool output
Web pages
Other agents

OWASP specifically recommends server-side enforcement because prompt instructions should not be relied upon to restrict tool access.

The secure architecture is:

LLM says:
"Call delete_customer"

        ↓

Authorization layer

        ↓

Does this agent have
delete_customer capability?

        ↓

NO

        ↓

Reject

Define Capabilities Explicitly

A capability can be represented as a strongly typed object.

For example, in C#:

public sealed record ToolCapability(
    string Tool,
    string Operation,
    string Resource,
    string Effect);

Then define:

var capabilities = new[]
{
    new ToolCapability(
        "customer",
        "read",
        "customer:*",
        "allow")
};

This is only a starting point.

Production systems should normally use a structured authorization model rather than ad-hoc strings.

Add Constraints

A capability should be more specific than:

customer.read

Consider:

customer.read
region = EU
department = support

or:

order.update
orderId = 12345
fields = status

This creates a capability such as:

Update order status
for order 12345

rather than:

Update any order

Capability Object

A more detailed C# model could be:

public sealed record Capability(
    string Tool,
    string Action,
    string ResourceType,
    IReadOnlySet<string> AllowedResources,
    DateTimeOffset ExpiresAt);

Example:

var capability = new Capability(
    Tool: "orders",
    Action: "read",
    ResourceType: "order",
    AllowedResources: new HashSet<string>
    {
        "order:12345",
        "order:12346"
    },
    ExpiresAt: DateTimeOffset.UtcNow.AddMinutes(10));

Now the authorization decision can consider:

Tool
Action
Resource
Expiration

Use Deny by Default

The safest starting policy is:

No capability
    ↓
No operation

Not:

Everything allowed
    ↓
Try to block dangerous operations

OWASP's MCP security guidance explicitly recommends least privilege and deny-by-default authorization for agent/tool systems.

For example:

public bool IsAllowed(
    Capability capability,
    string tool,
    string action,
    string resource)
{
    if (capability.ExpiresAt <= DateTimeOffset.UtcNow)
        return false;

    if (!string.Equals(
        capability.Tool,
        tool,
        StringComparison.Ordinal))
        return false;

    if (!string.Equals(
        capability.Action,
        action,
        StringComparison.Ordinal))
        return false;

    return capability.AllowedResources.Contains(resource);
}

The important behavior is:

Unknown
→ Deny

Expired
→ Deny

Wrong tool
→ Deny

Wrong action
→ Deny

Wrong resource
→ Deny

Separate Read and Write Capabilities

Do not combine:

customer.read
customer.write
customer.delete

into:

customer.full_access

Instead:

customer.read
customer.update
customer.delete

should be independent capabilities.

This allows:

Support Agent
    ↓
customer.read

while:

Customer Administrator
    ↓
customer.read
customer.update

and:

Privileged Workflow
    ↓
customer.delete

has a separate authorization path.

Separate Sensitive Tools

High-risk tools should not sit in the same unrestricted tool collection as ordinary read operations.

For example:

General Agent
 ├── search_documents
 ├── get_customer
 └── get_order

Privileged Agent
 ├── refund_order
 ├── delete_customer
 └── deploy_application

OWASP recommends separate tool sets for different trust levels and explicit authorization for sensitive operations.

Add Human Approval for High-Risk Capabilities

Some capabilities should require an additional control.

For example:

read_customer
    ↓
Automatic

update_customer
    ↓
Policy check

delete_customer
    ↓
Policy check
+
Human approval

Sensitive actions can include:

Delete
Refund
Transfer money
Send external email
Change permissions
Deploy production
Rotate credentials

The confirmation should happen outside the model's reasoning.

The user should see:

Action:
Delete customer

Resource:
Customer 12345

Reason:
Requested by current workflow

Approve?
[Yes] [No]

OWASP recommends explicit user confirmation for destructive, financial, and data-sharing operations.

Do Not Ask the LLM to Approve Its Own Action

This is unsafe:

Agent:
"Should I delete the customer?"

LLM:
"Yes."

Agent:
"Delete customer."

The same decision-maker is both requesting and approving the operation.

Instead:

Agent
 ↓
Authorization Service
 ↓
Human Approval
 ↓
Tool

Short-Lived Capabilities

Capabilities should normally have limited lifetimes.

For example:

ExpiresAt =
    DateTimeOffset.UtcNow.AddMinutes(10);

This limits the impact of leaked or stale authority.

Microsoft's current guidance for least-privilege AI agents recommends scoped access and short-lived credentials, while OWASP's MCP guidance similarly recommends narrow scopes and ephemeral credentials where possible.

Avoid Permanent Agent Permissions

Avoid:

Agent A
    ↓
Permanent database.write

Prefer:

Task starts
    ↓
Capability issued
    ↓
Task executes
    ↓
Capability expires

This makes authority proportional to the task.

Bind Capabilities to an Identity

A capability should not be treated as an anonymous permission.

It should be associated with:

User
Agent
Application
Session/task
Tenant

For example:

public sealed record CapabilityContext(
    string UserId,
    string AgentId,
    string TenantId,
    string TaskId);

Then the authorization layer can evaluate:

Who?
Which agent?
Which tenant?
Which task?
Which capability?
Which resource?

Multi-Tenant AI Systems Need Tenant Boundaries

Consider:

Tenant A
    ↓
Agent A
    ↓
Customer data

Tenant B
    ↓
Agent B
    ↓
Customer data

A capability such as:

customer.read

is not sufficient.

It should be constrained by tenant:

customer.read
tenant = tenant-a

The authorization layer must never trust:

tenantId

because it came from the model.

The server should derive tenant identity from authenticated context.

Prevent Cross-Tenant Resource Access

An unsafe tool might accept:

{
  "tenantId": "tenant-b",
  "customerId": "123"
}

and trust both fields.

A safer implementation derives the tenant from the authenticated principal:

var tenantId =
    userContext.TenantId;

var customer =
    await repository.GetCustomerAsync(
        tenantId,
        customerId,
        cancellationToken);

The agent supplies:

customerId

but not the authoritative tenant identity.

Capability-Based Tool Contracts

Tool schemas should also reflect authorization boundaries.

Instead of:

{
  "name": "execute_sql",
  "description": "Execute SQL against the database"
}

prefer domain-specific operations:

{
  "name": "get_customer",
  "description": "Read a customer record by ID"
}

The second tool has a much smaller capability surface.

OWASP recommends avoiding unrestricted tools such as arbitrary shell access and instead exposing narrowly scoped tools with explicit allowed operations and resources.

Avoid Generic execute_command

This is dangerous:

execute_command(command)

because the capability becomes:

Run arbitrary operating-system commands

A safer design might expose:

get_build_status
restart_test_service
read_deployment_logs

Each operation can have its own authorization policy.

Avoid Generic Database Tools

This:

execute_sql(sql)

creates a large capability.

Prefer:

get_customer
list_orders
get_order_status

If a specialized reporting workflow truly requires SQL, isolate it behind:

Read-only database
+
Restricted schema
+
Query validation
+
Timeout
+
Resource limits

Tool Authorization Middleware

Authorization should happen immediately before execution.

A simplified C# middleware might look like:

public async Task<ToolResult> ExecuteAsync(
    ToolRequest request,
    ToolContext context,
    CancellationToken cancellationToken)
{
    var decision =
        await authorization.AuthorizeAsync(
            context,
            request,
            cancellationToken);

    if (!decision.Allowed)
    {
        return ToolResult.Denied(
            decision.Reason);
    }

    return await toolExecutor.ExecuteAsync(
        request,
        cancellationToken);
}

The critical property is:

Authorization
    ↓
Tool execution

not:

LLM
    ↓
Tool execution
    ↓
Authorization later

Once the tool has executed, authorization is too late.

Authorization Must Be Server-Side

The client can send:

{
  "tool": "refund_order",
  "approved": true
}

That does not mean the request is authorized.

The server should determine:

Authenticated identity
+
Current policy
+
Capability
+
Resource
+
Action

and make its own decision.

OWASP identifies reliance on client-provided identity or authorization context as a major MCP authorization weakness.

Policy Decision Point

For larger systems, centralize authorization.

Agent
 ↓
Tool Gateway
 ↓
Policy Decision Point
 ↓
Allow / Deny
 ↓
Tool

The policy engine can evaluate:

Subject
Action
Resource
Environment

For example:

Subject:
agent-support-01

Action:
read

Resource:
customer:12345

Context:
tenant=tenant-a

Result:

ALLOW

Capability Gateway Example

A tool gateway can expose:

public interface ICapabilityAuthorizer
{
    Task<AuthorizationDecision> AuthorizeAsync(
        CapabilityRequest request,
        CancellationToken cancellationToken);
}

Then:

public sealed record CapabilityRequest(
    string AgentId,
    string UserId,
    string TenantId,
    string Tool,
    string Action,
    string Resource);

This makes authorization explicit rather than embedding it inside every tool implementation.

Use Resource-Level Authorization

Do not stop at:

agent can call orders.read

Check:

agent can call orders.read
AND
agent can read this particular order

For example:

orders.read
resource = order:12345

This prevents an agent with legitimate read access from automatically gaining access to every record.

Attribute-Based Conditions

Capabilities can include conditions.

For example:

Action:
read_customer

Conditions:
tenant = tenant-a
region = EU
classification <= Internal

Another example:

Action:
refund_order

Conditions:
amount <= 100
currency = USD
humanApproval = true

This is where capability-based authorization becomes more powerful than simple role checks.

Tool Result Security

Authorization does not end when the tool returns.

Tool output can contain:

Customer data
Secrets
Internal URLs
Prompt injection
Untrusted instructions

OWASP recommends treating tool responses as untrusted data because returned content can influence subsequent agent behavior.

Therefore:

Tool
 ↓
Output validation
 ↓
Redaction
 ↓
Agent context

is preferable to:

Tool
 ↓
Raw output
 ↓
LLM

Redact Sensitive Fields

Suppose the database returns:

{
  "name": "Alice",
  "email": "[email protected]",
  "passwordHash": "...",
  "internalNotes": "..."
}

The tool should return only what the agent needs:

{
  "name": "Alice",
  "email": "[email protected]"
}

Do not rely on the LLM to ignore sensitive fields.

Data minimization belongs at the tool boundary.

Prevent Capability Leakage

Never expose internal authorization details unnecessarily.

Avoid returning:

Agent has:
database.admin
secrets.read
production.deploy

to the model unless the information is genuinely required.

A denied result can simply say:

{
  "status": "denied",
  "reason": "Operation is not permitted."
}

Detailed authorization logs should go to the security telemetry system.

Capability Revocation

Capabilities should be revocable.

For example:

User account disabled
        ↓
Capabilities revoked
        ↓
Tool calls rejected

Similarly:

Agent compromised
        ↓
Revoke agent capabilities
        ↓
No further privileged calls

This is especially important for long-running agents.

Token Revocation vs Capability Revocation

These are related but different.

Token
=
Authentication credential

while:

Capability
=
Authorization authority

Revoking a token may prevent authentication.

Revoking a capability can remove one specific authority while leaving other operations intact.

For example:

Agent
 ├── customer.read
 ├── order.read
 └── order.refund

If refund behavior is compromised:

Revoke:
order.refund

Keep:
customer.read
order.read

This is a more targeted response.

Prevent Scope Creep

A common failure mode is gradual permission expansion.

It starts as:

read_customer

Then someone adds:

update_customer

Then:

delete_customer

Then:

database.admin

The agent has accumulated privileges that are no longer related to its original purpose.

OWASP identifies privilege escalation through scope creep as a specific MCP security risk.

Add Capability Review

Treat capability changes like code changes.

For example:

Pull Request
    ↓
Capability diff
    ↓
Security review
    ↓
Approval
    ↓
Deployment

A useful review output could be:

Added:
orders.update

Removed:
orders.read

Changed:
orders.refund
  maxAmount: 100 → 1000

This makes authorization changes auditable.

Version Tool Definitions

Tool definitions themselves are security-sensitive.

OWASP recommends inspecting and pinning tool descriptions and schemas because changes can introduce unexpected behavior or create tool-poisoning risks.

For a controlled tool ecosystem:

Tool Definition
    ↓
Version
    ↓
Hash
    ↓
Approved

Then detect changes:

Approved hash
      ≠
Current hash
      ↓
Review required

Capability Versioning

Capabilities can also be versioned.

For example:

orders.refund:v1

might allow:

amount <= 100

while:

orders.refund:v2

allows:

amount <= 500
+
human approval

This creates a clear authorization migration path.

Audit Every Tool Call

A security audit record should answer:

Who?
Which agent?
Which tenant?
Which tool?
Which action?
Which resource?
Which capability?
Allowed or denied?
Why?
When?
What was the outcome?

For example:

{
  "timestamp": "2026-08-11T10:15:00Z",
  "userId": "user-123",
  "agentId": "support-agent",
  "tenantId": "tenant-a",
  "tool": "orders",
  "action": "read",
  "resource": "order:12345",
  "decision": "allow"
}

Avoid logging secrets or unnecessary sensitive payloads.

OWASP recommends centralized logging and auditing of authentication and authorization decisions.

Detect Suspicious Capability Usage

Authorization logs can also become security signals.

For example:

Normal:
orders.read

Suddenly:

orders.refund

or:

database.export

The system can trigger:

Alert
+
Human review
+
Capability revocation

This is more effective than logging without analyzing the events.

Example Capability Policy

A simplified policy could be represented as:

{
  "agent": "support-agent",
  "capabilities": [
    {
      "tool": "customer",
      "actions": ["read"],
      "resource": "tenant/customer/*"
    },
    {
      "tool": "orders",
      "actions": ["read"],
      "resource": "tenant/order/*"
    }
  ]
}

A privileged action remains outside the normal capability set:

orders.refund

and therefore requires a separate workflow.

Capability Evaluation Flow

A complete request might look like:

User
 ↓
Agent
 ↓
Tool request
 ↓
Authenticate caller
 ↓
Resolve tenant
 ↓
Resolve capability
 ↓
Check expiration
 ↓
Check tool
 ↓
Check action
 ↓
Check resource
 ↓
Check policy conditions
 ↓
Human approval if required
 ↓
Execute tool
 ↓
Validate output
 ↓
Audit
 ↓
Return result

This is the security boundary.

Example Secure Tool Flow in C#

public async Task<ToolResult> HandleAsync(
    ToolRequest request,
    RequestContext context,
    CancellationToken cancellationToken)
{
    var capabilityRequest =
        new CapabilityRequest(
            context.AgentId,
            context.UserId,
            context.TenantId,
            request.Tool,
            request.Action,
            request.Resource);

    var decision =
        await authorizer.AuthorizeAsync(
            capabilityRequest,
            cancellationToken);

    if (!decision.Allowed)
    {
        await audit.LogDeniedAsync(
            capabilityRequest,
            decision.Reason,
            cancellationToken);

        return ToolResult.Denied(
            "Operation is not permitted.");
    }

    if (decision.RequiresApproval)
    {
        return ToolResult.PendingApproval(
            request.Tool,
            request.Action,
            request.Resource);
    }

    var result =
        await executor.ExecuteAsync(
            request,
            context,
            cancellationToken);

    var safeResult =
        outputSanitizer.Sanitize(result);

    await audit.LogAllowedAsync(
        capabilityRequest,
        cancellationToken);

    return safeResult;
}

The example demonstrates the sequence:

Authorize
 ↓
Approve if necessary
 ↓
Execute
 ↓
Sanitize
 ↓
Audit

Common Mistakes

Trusting the LLM

The model is not an authorization engine.

Trusting Client-Provided Roles

The server should derive identity and authorization context from trusted authentication infrastructure.

Giving Agents Full Database Access

Prefer domain-specific tools.

Using One Token for Every Tool

Use scoped identities and credentials.

Giving Every Agent the Same Tools

Tool access should reflect task requirements.

Ignoring Resource-Level Authorization

Being allowed to call orders.read does not necessarily mean the agent can read every order.

Allowing Arbitrary URLs

URL-fetching tools can create SSRF risks if the destination is not restricted. OWASP explicitly recommends strict allowlists for such tools.

Allowing Arbitrary Shell Commands

Avoid generic:

execute_command(command)

for normal agent workflows.

Skipping Human Approval

High-impact actions need stronger controls.

Logging Secrets

Audit tool activity without storing sensitive credentials or unnecessary payloads.

Never Reviewing Capability Changes

Authorization policies can drift just like application code.

Troubleshooting

Agent Says It Has Permission but Tool Rejects It

This is often expected.

The model's context may contain:

"I can perform this action."

but the server evaluates the actual authorization.

Check:

Identity
Tenant
Tool
Action
Resource
Capability
Expiration
Policy

Tool Works for One User but Not Another

Compare the capability sets.

For example:

User A:
orders.read

User B:
orders.read
orders.update

The difference should be visible in authorization telemetry.

Agent Can Read Data From Another Tenant

Treat this as a critical authorization defect.

Check whether tenant identity comes from:

Authenticated context

or:

LLM-supplied parameter

The latter should not be trusted.

A Tool Suddenly Has More Permissions

Compare the capability policy version and tool definition hash.

Possible causes include:

Policy change
Configuration drift
Tool update
New scope
Identity mapping change

Sensitive Tool Executes Without Approval

Verify that approval is enforced server-side.

Do not rely solely on the agent's instructions to request confirmation.

Best Practices

  1. Start with deny-by-default authorization.

  2. Grant the minimum required capabilities.

  3. Separate read, write, and destructive operations.

  4. Enforce authorization server-side.

  5. Never treat system prompts as access controls.

  6. Bind capabilities to authenticated identity.

  7. Bind permissions to tenant context.

  8. Perform resource-level authorization.

  9. Use short-lived capabilities where practical.

  10. Support revocation.

  11. Separate privileged workflows.

  12. Require human approval for high-impact operations.

  13. Avoid generic shell and unrestricted database tools.

  14. Validate tool parameters.

  15. Validate and minimize tool output.

  16. Protect against SSRF and path traversal.

  17. Audit every authorization decision.

  18. Monitor capability changes and unusual tool usage.

  19. Review capability changes like code changes.

  20. Treat tool definitions and schemas as security-sensitive assets.

Capability Security Checklist

Before exposing an AI tool, ask:

QuestionExpected Answer
Who can call it?Explicit identity
What action is allowed?Explicit capability
Which resources?Explicit scope
Which tenant?Authenticated context
How long?Limited lifetime
Can it be revoked?Yes
Does it modify data?Clearly classified
Does it require approval?Defined by risk
Are parameters validated?Yes
Is output sanitized?Yes
Is the call audited?Yes
Can the tool execute arbitrary code?Preferably no
Can permissions drift?Detect and review
Can tool definitions change silently?Detect and review

Frequently Asked Questions

What is capability-based security?

Capability-based security represents authority as an explicit permission to perform an action against a particular resource.

For AI agents, this can mean:

Tool
+
Action
+
Resource
+
Constraints

rather than simply assigning a broad role.

Is capability-based security the same as RBAC?

No.

RBAC assigns permissions through roles.

Capability-based security focuses on explicit authority.

The two can be combined. A role can determine which capabilities an agent receives.

Should the LLM decide whether it has permission?

No.

The LLM can select a tool, but the final authorization decision should be made by trusted server-side policy enforcement. OWASP specifically recommends server-side enforcement rather than relying on prompts or client-side controls.

Should every tool require human approval?

No.

Human approval should be proportional to risk.

A read-only search may not require approval.

A financial transfer or destructive operation may.

Why are short-lived capabilities useful?

They reduce the lifetime of authority.

If a capability is leaked or misused, its usefulness decreases after expiration.

Should each MCP server have a separate credential?

Where appropriate, yes.

OWASP recommends scoped, per-server credentials rather than sharing broad tokens across servers.

Can capability security prevent prompt injection?

Not by itself.

Prompt injection can influence model behavior.

Capability enforcement limits what the resulting tool call can actually do.

That is why server-side authorization remains important.

Can I use capability-based security without MCP?

Yes.

The model applies to any AI system that invokes tools:

AI Agent
+
Plugins
+
Function Calling
+
MCP
+
Internal APIs

MCP is one implementation context, not a prerequisite.

Should tools expose generic database or shell access?

Generally, avoid this for ordinary agent workflows.

Narrow domain-specific tools produce smaller and more controllable capability surfaces.

How should I secure multi-tenant agents?

Bind authorization to the authenticated tenant context and perform resource-level authorization on every request.

Never trust a tenant identifier supplied solely by the model.

Conclusion

AI agents should not receive authority simply because a tool exists.

A secure design starts with:

Agent
 ↓
Capabilities
 ↓
Policy
 ↓
Authorized Tool
 ↓
Authorized Resource

rather than:

Agent
 ↓
All Tools

The capability model gives developers a practical way to implement least privilege:

customer.read

is better than:

customer.full_access

and:

order.refund
amount <= 100
human approval required

is better than:

order.admin

The most important security boundary is the tool execution layer.

The model can request:

delete_customer

but that request should pass through:

Authentication
 ↓
Capability check
 ↓
Resource authorization
 ↓
Policy evaluation
 ↓
Human approval
 ↓
Tool execution

before anything destructive happens.

Current OWASP guidance highlights excessive permissions, scope creep, insufficient authorization, tool poisoning, and cross-server interactions as important risks in MCP and agentic systems.

The practical objective is therefore not to make an AI agent incapable of doing dangerous things.

It is to make dangerous capabilities:

Explicit
+
Scoped
+
Time-limited
+
Auditable
+
Revocable
+
Policy-controlled

That is the foundation of capability-based security for AI tool plugins.

Give the agent enough authority to complete the task, but never more authority than the task requires.