Agent Capabilities
Capabilities grant an agent access to specific tools, with explicit operations, scopes, rate limits and time windows. You manage them on the agent detail page (under Agents → [Agent Name] → Capabilities). They are a defense-in-depth layer that works next to policies.
Capabilities and policies are not equivalent. A capability restricts how an agent may use the tool it covers; policies evaluate every action. Use both.
How capabilities interact with policy rules
A capability only restricts. It applies to the tool calls that its tool's match rules match, and a call it allows goes on to the policy rules:
- A tool call that an enabled capability covers is checked against it first. If the capability refuses the call, because an operation, scope, rate, payload or time-window check fails, the call is denied before policy evaluation runs. If the capability allows the call, the policy rules decide.
- A tool call that no enabled capability covers goes to the policy rules. This holds whether or not the agent has other capabilities.
- With no capabilities configured, all tool calls go to the policy rules. Policies are then the sole authorization surface for that agent.
- A capability on a broken tool definition is not sent to your edges. That is a definition whose match rules cannot be read, or that has no action type pattern, for example after an edit removed it. The calls it was meant for go to the next enabled capability whose tool matches them, or to the policy rules, as if the capability did not exist.
Capabilities do not replace policy rules: policies still run for every tool call that a capability allows. A capability granting get_contact on tool_crm_8k2m does not override a policy that denies CRM reads outside business hours. For a tool call that a capability covers, both must say "yes" for the call to proceed.
| Capabilities | Behavior |
|---|---|
| None configured | Every tool call falls through to policy rules. Policies are the sole authorization surface. |
| At least one configured | A tool call that a capability covers is checked against it, and goes on to policy evaluation if the capability allows it. A tool call that no capability covers goes to policy evaluation. |
Overview
A capability links three concepts:
- Agent: the AI agent performing actions.
- Tool: a tool from the tool catalog.
- Capability: the grant, with operations and constraints.
Each capability specifies:
- Operations: which of the tool's calls are permitted (see Operations).
- Scopes: fine-grained resource access within the tool (see Scopes).
- Rate limits: the maximum actions per time window.
- Payload limits: the maximum payload size in bytes (see Payload limits).
- Time windows: when the capability is active.
- Enabled flag: turns the capability off without deleting it.
Operations
Operations decide which of a tool's calls a capability allows. The edge reads a call's operation from its action type: the operation is everything after the first colon. So the names you list depend on the surface that the tool's definition matches. Only some of the catalog's tools match a surface at all; the tool catalog lists them.
How the edge sees a gateway call
Mitrity Gateway serves each upstream's tools under a namespace, as <namespace>__<tool>. A gateway that serves Stripe's MCP server under the namespace stripe serves its create_customer tool as stripe__create_customer. The edge reads a call to that tool like this:
| The edge reads | Value | Checked against |
|---|---|---|
| Action type | mcp:stripe__create_customer | The tool definition's action type pattern, here mcp:stripe__* |
| Operation | stripe__create_customer | allowed_operations |
| Scope | The call's scope or resource argument when it has one, otherwise stripe__create_customer | denied_scopes and allowed_scopes (see Scopes) |
Operations on a namespaced MCP definition
A namespaced MCP definition has a pattern of the form mcp:<namespace>__…. The catalog's namespaced rows, such as Stripe (mcp:stripe__*) and MCP Delegate (mcp:delegate__*), list their tools by the tools' own names, such as create_customer. Definitions you write for your own upstreams do the same. In allowed_operations on such a definition:
- The tool's own name (
create_customer) or its served name (stripe__create_customer) allows that one tool. *allows every tool of the definition.- Globs and
regex:entries are matched as written, against the served name. Write them in served form:stripe__list_*orregex:^stripe__list_. - A glob written in the tools' own names, such as
list_*, matches no served name. The capability then refuses every call its definition matches.
What the dashboard stores
In the Capabilities dialog, you pick tools by the names the catalog lists, such as create_customer, or you choose Use wildcard *. The API stores those entries and returns them as stored. When the control plane sends the capability to your edges, it adds <namespace>__<name> for each tool name. So the tools you picked are allowed, and the definition's other tools are refused.
On the mesh
Mitrity Mesh Authorizer reads a call as mcp:<service>__<operation>. The operation is the called tool's name, the JSON-RPC method, or a plain REST call's METHOD PATH.
A definition with the pattern mcp:<service>__* matches a called tool's name. It does not match an operation that contains a /, such as the JSON-RPC method resources/read or a REST METHOD PATH, because a glob's * does not match /. For those calls, use one of these:
- A pattern that spells the path out at each depth:
mcp:payments__POST /v1/*matchesPOST /v1/charges, but notPOST /v1/charges/ch_1/capture. Its operations, such asPOST /v1/charges, are read like tool names. - A
regex:definition. Its operations are sent as stored, so write them in served form:payments__POST /v1/charges.
On the LLM provider rows
A call through Mitrity LLM Gateway is llm:send:<provider>:<model>. Its operation is send:<provider>:<model>, and its scope is <provider>:<model>. The OpenAI API and Anthropic API rows list each provider's own method names, and the control plane reads a capability on them in the gateway's terms:
- The gateway serves one call per provider: OpenAI's Chat Completions and Anthropic's Messages. So
chat_completions_create(OpenAI API) ormessages_create(Anthropic API) allows every model of that provider. The rows' other operations allow nothing. - A scope names a model, as
model:gpt-4oor as the bare idgpt-4o. Inallowed_scopes, it limits the capability to that model. Indenied_scopes, it refuses that model. - On an enabled capability, the API refuses a denied scope that cannot be read as a model: a regex that is not written against the gateway's scope, such as
regex:^gpt-4o, ormodel:with no model. Write a regex against the gateway's scope instead:regex:^openai:gpt-4o. A capability stored with such a deny before that check refuses every call to that provider.
On the Shell row
The Shell row (shell:*) matches the governed shell. Its calls are shell:execute, so the operation is execute.
Operation filtering
Grant only the operations the agent needs. This capability lets a billing agent list Stripe customers and invoices, and refuses every other Stripe tool:
{
"agent_id": "agt_billing-bot",
"tool_id": "tool_stripe_builtin",
"allowed_operations": ["list_customers", "list_invoices"]
}
A call to any other Stripe tool, such as stripe__create_refund, is denied at the capability layer, before policies even run, with tool-denied:<tool> (operation not allowed: stripe__create_refund).
Rate limits
Rate limits restrict how often an agent can use a tool. They keep runaway agents from overwhelming resources, and they are a safety net against infinite loops.
{
"agent_id": "agt_sales-bot",
"tool_id": "tool_crm_8k2m",
"allowed_operations": ["list_contacts", "get_contact", "update_contact"],
"rate_limit_per_minute": 60
}
Rate limit fields
| Field | Type | Description |
|---|---|---|
rate_limit_per_minute | integer | Maximum number of actions per minute, counted per agent and tool. Null or 0 means no limit; the value cannot be negative. |
When the rate limit is exceeded, the call is denied at the capability layer, before policy evaluation runs.
Rate limit recommendations
| Tool category | Recommended rate |
|---|---|
| Database | 100-500/min |
| LLM | 30-60/min |
| Email/SMS | 10-30/min |
| Code execution | 10-20/min |
| Storage | 100-300/min |
| HTTP outbound | 60-200/min |
Payload limits
A payload limit caps the size of a single action's payload. It keeps agents from exfiltrating large datasets or overwhelming downstream services through one tool call.
When max_payload_bytes is above 0, the edge refuses an action whose payload is larger than the limit, with tool-denied:<tool> (payload too large: <size> > <limit>) and blocked_by=tool_permission. An action exactly at the limit passes. 0 or an absent value means no limit. The API refuses a negative value with 422; a negative value stored before that check also means no limit.
Payload limits need edge v0.22.4 or later. Edges before v0.22.4 ignore
max_payload_bytes.
The payload is not the request body. It is the sum of the UTF-8 bytes of every parameter's name and value, as the edge holds them. What counts as a parameter depends on the surface:
- Mitrity Gateway and Mitrity MCP Sidecar: the top-level arguments of the
tools/call. A string counts as itself, and any other value as compact JSON. - Admission API (framework built-in tools):
tool_input, flattened the same way, plus the session'scwdwhen the input has none. - Mitrity Mesh Authorizer: the MCP arguments parsed from the request body; an agent-to-agent hop adds its context parameters. A body that is not MCP (egress, TLS passthrough) has a payload of 0, so no limit applies there.
- Mitrity LLM Gateway: the prompt segments, plus
max_output_tokensand the red-team label when they are present. - Governed shell:
commandandcwd.
{
"agent_id": "agt_data-bot",
"tool_id": "tool_crm_8k2m",
"allowed_operations": ["list_contacts", "get_contact"],
"max_payload_bytes": 10485760
}
| Field | Type | Description |
|---|---|---|
max_payload_bytes | integer | Maximum payload size in bytes (see above). 0 or absent means no limit. The API refuses a negative value with 422; one stored before that check reads as no limit. |
Common sizes:
| Size | Bytes | Use case |
|---|---|---|
| 1 KB | 1024 | Simple API calls, JSON payloads |
| 1 MB | 1048576 | Standard file uploads, data exports |
| 10 MB | 10485760 | Large file operations |
| 100 MB | 104857600 | Bulk data transfers |
Time windows
A time window restricts a capability to certain hours, and optionally to certain days of the week. The capability allows an action only while its window is open; outside the window, the action is denied.
daysneeds edge v0.22.4 or later. Upgrade your edges before you writedays. Edge v0.22.3 refuses every action of a grant whose window hasdays, and v0.22.2 and older fail on any window.
{
"agent_id": "agt_batch-processor",
"tool_id": "tool_shell_builtin",
"allowed_operations": ["execute"],
"time_window": {
"days": ["monday", "tuesday", "wednesday", "thursday", "friday"],
"start": "02:00",
"end": "06:00",
"timezone": "Europe/Stockholm"
}
}
Time window fields
| Field | Type | Description |
|---|---|---|
days | array | The days of the week the window opens on: monday, tuesday, wednesday, thursday, friday, saturday, sunday, in lower case and each at most once. Only these seven names are accepted (not mon or Monday). Absent, null or [] means every day. Needs edge v0.22.4 or later. |
start | string | Start time in 24-hour format (HH:MM). It is inside the window. Required without days. With days, leave out both start and end for whole days. |
end | string | End time in 24-hour format (HH:MM). It is not inside the window: 08:00–18:00 admits 08:00:00 through 17:59:59. An end earlier than start crosses midnight, and 00:00 means until midnight. It must not equal start. |
timezone | string | IANA timezone (e.g., Europe/Stockholm, America/New_York, UTC). Empty means UTC. |
How the edge reads a window:
- A window that crosses midnight belongs to the day it opens. With
mondaytofriday,22:00–06:00is open from Friday 22:00 to Saturday 05:59:59, and not from Sunday 22:00 to Monday 05:59:59. A Sunday window that crosses midnight runs into Monday morning. - With
daysand nostartorend, each named day is open whole:{"days": ["saturday", "sunday"]}is the weekend. - Days follow the calendar of the window's time zone. Across a daylight saving change, a whole day lasts 23 or 25 hours, a skipped hour is never inside the window, and a repeated hour is inside it both times.
- Outside the window, the action is denied with
tool-denied:<tool> (outside allowed time window)andblocked_by=tool_permission. - A window the edge cannot enforce is never open. Its grant refuses every action it matches, with
tool-denied:<tool> (time window cannot be enforced: <problem>)andblocked_by=tool_permission; the rest of the agent's profile still applies.
Checks on write
The API checks a window when you write the capability. PUT /api/v1/agents/{agent_id}/permissions refuses with 422 a window that the edge could not enforce:
- a time that is not
HH:MM; - one bound without the other;
- a
startequal toend, including00:00–00:00; - a day that is not one of the seven names, or a day named twice;
- an unknown
timezone; - a field that a window does not have.
All problems come back in one answer, in a fields[] array, and nothing is written:
{
"error": "a capability's lists, limits or time window cannot be enforced as written",
"code": 422,
"fields": [
{
"field": "[1].time_window.days[0]",
"message": "is not a day of the week; use monday, tuesday, wednesday, thursday, friday, saturday or sunday, in lower case"
}
]
}
Spanning midnight
To create a window that spans midnight (e.g., 22:00 to 06:00), set the end time before the start time. The window belongs to the day it opens: the example below opens Monday to Friday at 22:00, so it runs from Friday 22:00 into Saturday morning, and not from Sunday night into Monday:
{
"time_window": {
"days": ["monday", "tuesday", "wednesday", "thursday", "friday"],
"start": "22:00",
"end": "06:00",
"timezone": "Europe/Stockholm"
}
}
Scopes
Scopes narrow a capability within its tool. The edge reads a call's scope from its scope or resource argument, when the call has one. Otherwise, the scope is the call's target: on a gateway call, that is the served name, such as stripe__create_customer.
The edge checks the denied scopes first, then the allowed scopes. Both lists hold globs or regex: entries.
Allowed scopes
When allowed_scopes has entries, a call's scope must match one of them. The entries are matched as written against the scope: the call's scope or resource argument as the agent supplies it, or the served name when the call has neither.
{
"agent_id": "agt_support-bot",
"tool_id": "tool_crm_8k2m",
"allowed_operations": ["get_contact", "update_contact"],
"allowed_scopes": ["contacts/*"]
}
Here, the agent may call get_contact and update_contact only with a scope or resource argument that matches contacts/*. A call without such an argument has the scope crm__get_contact, which does not match, so it is refused. For the same reason, allowed scopes that list tool names (get_contact) refuse every call made without a scope or resource argument.
Allowed scopes constrain what a well-behaved agent may target. They are not a security boundary against a compromised or prompt-injected agent, because such an agent can write a matching scope argument. For hard limits, use allowed_operations, which the edge reads from the action type, and denied_scopes, which from edge v0.22.4 are also checked against names the edge derives itself.
Denied scopes
A call whose scope matches an entry in denied_scopes is refused. On an mcp: call, an edge at v0.22.4 or later also checks the denied scopes against the names it derives itself, which no argument changes:
- the served name:
<namespace>__<tool>on the gateway,<service>__<operation>on the mesh (a RESTMETHOD PATHincluded), and<tool>on the sidecar; - the bare name after the namespace:
<tool>or<operation>; - the call's target, which on the mesh is the destination service.
Any match denies, and the reason names the value that matched, as in tool-denied:<tool> (scope denied: stripe__create_refund). Calls on the other surfaces (LLM gateway calls, agent-to-agent hops, mesh egress, built-in tools and the governed shell) are checked against their scope only.
Checking the edge's own names needs edge v0.22.4 or later. Older edges check a deny against the call's scope only, so a deny written as a tool's name holds only for calls without a
scopeorresourceargument. After the upgrade, a deny written for a resource can also match a tool's name:*invoice*, meant for invoice ids, also refuses Stripe'slist_invoicesandcreate_invoicetools. Before you upgrade, review the denies that could match a tool's name.
Denied scopes on a namespaced MCP definition
On a definition for gateway-served tools, the control plane also reads your denies in served names:
- A tool's own name, or a glob in the tools' own names, also denies its served form:
create_refundon Stripe also deniesstripe__create_refund. - A
regex:deny is matched as written, so write it against the served name:regex:^stripe__create_. On an enabled capability whose tools are picked by name, the API refuses a regex deny that is not written against the served names, such asregex:^create_.
The API also refuses padded entries and a regex: prefix in other letters on an enabled capability (see Upsert capabilities). Capabilities stored before those checks are read like this:
- On a capability whose tools are picked by name, a deny that cannot be read in served names, such as
regex:^create_, makes the capability refuse every call. - An entry with spaces around it is read trimmed, so
" create_refund "also deniescreate_refundandstripe__create_refund. An entry that is*or blank once trimmed denies every call, and one that cannot be read once trimmed counts as unreadable. - A
regex:prefix written in other letters, such asREGEX:, is read asregex:.
The LLM provider rows read stored padded entries and the regex: prefix the same way.
On any edge, the surest way to keep an agent off a tool is to leave the tool out of allowed_operations.
Scope pattern examples
| Tool | Scope pattern | Matches |
|---|---|---|
Internal CRM (mcp:crm__*) | contacts/* | Calls whose scope or resource argument is one level under contacts/ |
| Stripe | stripe__list_* | As an allowed scope: calls to Stripe's list_* tools made without a scope or resource argument |
| OpenAI API | model:gpt-4o | Calls to gpt-4o through the LLM gateway |
Managing capabilities in the dashboard
You manage capabilities inline, on the agent detail page.
View an agent's capabilities
Navigate to Agents → [Agent Name] and scroll to the Capabilities section. Each row shows the tool, the granted operations, the scopes, the rate limit and whether the capability is enabled.
Add a capability
Click Add capability in the Capabilities section header. The dialog asks for:
- Tool: pick from the tools available to your tenant in the tool catalog. The dialog does not list the catalog rows that the edge cannot enforce: the vendor-pattern rows and HTTP Client.
- Operations: a multi-select of the tool names the catalog lists, or Use wildcard * for every tool of the definition; at least one is required.
- Scopes: optional allowed and denied scopes (see Scopes).
- Rate limit: an optional per-minute cap.
A capability saved earlier on a tool the edge cannot enforce still opens in the dialog, with a note, so that you can switch it off or remove it.
The capability is created as soon as you save, and it reaches connected edge nodes within seconds. The dialog has no payload limit or time window: set those through the API (see Upsert capabilities).
Edit a capability
Click Edit in the row's Actions column to change the operations, scopes or rate limit. Changes take effect immediately: the gateway receives the updated capabilities on the heartbeat channel.
Remove a capability
Click Remove in the row's Actions column. The dashboard asks for confirmation, then deletes the capability. After removal, the agent loses that grant: the tool calls it covered go to the policy rules, unless another capability covers them.
API reference
The capability endpoints still use the legacy /permissions path on the MITRITY backend.
List an agent's capabilities
curl https://api.mitrity.com/api/v1/agents/agt_sales-bot/permissions/ \
-H "Authorization: Bearer mk_your-api-key"
Response:
[
{
"id": "perm_8k2m4n",
"tenant_id": "tnt_4f9a",
"agent_id": "agt_sales-bot",
"tool_id": "tool_crm_8k2m",
"tool_name": "Internal CRM",
"allowed_operations": ["list_contacts", "get_contact"],
"allowed_scopes": [],
"denied_scopes": [],
"rate_limit_per_minute": 60,
"max_payload_bytes": null,
"time_window": null,
"enabled": true,
"created_at": "2026-03-01T10:00:00Z",
"updated_at": "2026-03-01T10:00:00Z"
}
]
Upsert capabilities
The PUT endpoint is upsert-only. It creates new capabilities and updates existing ones (matched by tool_id), but it does not remove capabilities for tools omitted from the body. Use the DELETE endpoint for removal.
The API refuses with 422 an enabled capability on a tool definition the edge cannot match, such as one of the catalog's vendor-pattern rows or HTTP Client (http:*). The field error names the capability by its position in the body ([<i>].tool_id), and nothing is written. A switched-off capability on such a definition is accepted, so that you can keep it switched off until you remove it.
For an enabled capability, the API also refuses list entries that cannot be enforced as written. Each one gets a 422 on its own path, [<i>].<list>[<j>]:
- an entry with space before or after it, including a blank entry and
" * "; space inside an entry, as inPOST /v1/charges, is accepted; - a
regex:prefix written in other letters, such asREGEX:^a; - a denied scope that the edge cannot read in the definition's terms: on the OpenAI API and Anthropic API rows, a regex not written
^<provider>:…, ormodel:with no model; on a namespaced MCP definition whose tools are picked by name, a regex not written^<namespace>__….
A switched-off capability's entries are not judged, so its stored values are written back as they are. Scopes explains how such stored entries are read.
curl -X PUT https://api.mitrity.com/api/v1/agents/agt_sales-bot/permissions/ \
-H "Authorization: Bearer mk_your-api-key" \
-H "Content-Type: application/json" \
-d '[
{
"tool_id": "tool_crm_8k2m",
"allowed_operations": ["list_contacts", "get_contact", "update_contact"],
"rate_limit_per_minute": 120,
"max_payload_bytes": 1048576,
"allowed_scopes": ["contacts/*", "deals/*"]
},
{
"tool_id": "tool_stripe_builtin",
"allowed_operations": ["list_customers", "list_invoices"],
"rate_limit_per_minute": 10
}
]'
Delete a capability
Remove a single capability by its permission_id. The endpoint is scoped to (tenant, agent, permission_id): a capability that does not belong to the calling tenant returns 404, without leaking its existence across tenant boundaries.
curl -X DELETE https://api.mitrity.com/api/v1/agents/agt_sales-bot/permissions/perm_8k2m4n \
-H "Authorization: Bearer mk_your-api-key"
Status codes:
| Code | Meaning |
|---|---|
204 No Content | Capability removed. |
403 Forbidden | The caller's role is viewer (or otherwise lacks write access to agents). |
404 Not Found | No capability with this permission_id exists for this agent within the tenant, or the agent is not one you can see (see Access Scope). |
After deletion, later tool calls that match the removed capability's tool go to the policy rules, unless another enabled capability covers them.
Evaluation flow
When the gateway intercepts an agent's tool call, it walks two layers:
Agent tool call
│
▼
Covered by an enabled capability? ──No──► Policy evaluation
│ Yes
▼
Operation in the capability's allowed_operations? ──No──► DENIED (capability)
│ Yes
▼
Scope allowed (denied_scopes first, then allowed_scopes)? ──No──► DENIED (capability)
│ Yes
▼
Within the rate limit? ──No──► DENIED (capability)
│ Yes
▼
Payload within max_payload_bytes? ──No──► DENIED (capability)
│ Yes
▼
Time window open? ──No──► DENIED (capability)
│ Yes
▼
Policy evaluation
│
├── allow ──► Action proceeds
├── hold ──► Awaits human approval
└── deny ──► DENIED (policy)
The first failure decides. A call refused for its payload or its time window still counts toward the rate limit.
A call that a capability covers must pass both layers. A capability that allows read on a tool does not override a policy that denies that read, and a policy that allows a tool does not override a capability that refuses the call.
Best practices
Decide deliberately whether to use capabilities
Capabilities are optional. Many small deployments run on policy rules alone, and that is fine. Add a capability when you want to restrict how an agent uses a particular tool: its operations, scopes, rate, payload size and hours. A capability does not block the tools it does not cover; the policy rules decide those.
Start with read-only
When you onboard a new agent, start with read-only capabilities and expand as needed. You can then observe the agent's behavior before you grant write access.
Set rate limits on all capabilities
Even for trusted agents, set rate limits as a safety net. An agent stuck in a loop can generate thousands of requests per minute without rate limits.
Use scopes for sensitive data
Keep an agent off a sensitive tool by leaving the tool out of allowed_operations. No argument the agent writes changes that. Then use scopes to narrow what the agent may ask for within the tools it has, such as the records it may name in a resource argument.
Review capabilities regularly
Set a quarterly review cadence for agent capabilities. Remove capabilities for tools that are no longer used and tighten scopes based on actual usage patterns.
Document time windows
When you use time windows, document the rationale. A future team member who sees a 02:00-06:00 window needs to know that it is for batch processing, not an arbitrary restriction.
Related documentation
- Tool Catalog: browse the tools available to your tenant
- Destination Allowlists: control where agents can send data
- Writing Policies: policy structure and the rules that run alongside capabilities
- RBAC: role-based access control for team members
- API Overview: the full API reference