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.
| Model | Example | Granularity |
|---|
| RBAC | Agent = SupportAgent | Coarse |
| ABAC | Department = Support + Region = EU | Medium |
| Capability | Read ticket 123 | Fine |
| Resource capability | Read /reports/2026/q1 | Very 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
Start with deny-by-default authorization.
Grant the minimum required capabilities.
Separate read, write, and destructive operations.
Enforce authorization server-side.
Never treat system prompts as access controls.
Bind capabilities to authenticated identity.
Bind permissions to tenant context.
Perform resource-level authorization.
Use short-lived capabilities where practical.
Support revocation.
Separate privileged workflows.
Require human approval for high-impact operations.
Avoid generic shell and unrestricted database tools.
Validate tool parameters.
Validate and minimize tool output.
Protect against SSRF and path traversal.
Audit every authorization decision.
Monitor capability changes and unusual tool usage.
Review capability changes like code changes.
Treat tool definitions and schemas as security-sensitive assets.
Capability Security Checklist
Before exposing an AI tool, ask:
| Question | Expected 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.