New: Agent approvals, red teaming and model scanning. See how
MITRITY
Docs / Tools & Permissions / Agent Capabilities

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.

CapabilitiesBehavior
None configuredEvery tool call falls through to policy rules. Policies are the sole authorization surface.
At least one configuredA 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:

  1. Agent: the AI agent performing actions.
  2. Tool: a tool from the tool catalog.
  3. 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 readsValueChecked against
Action typemcp:stripe__create_customerThe tool definition's action type pattern, here mcp:stripe__*
Operationstripe__create_customerallowed_operations
ScopeThe call's scope or resource argument when it has one, otherwise stripe__create_customerdenied_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_* or regex:^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/* matches POST /v1/charges, but not POST /v1/charges/ch_1/capture. Its operations, such as POST /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) or messages_create (Anthropic API) allows every model of that provider. The rows' other operations allow nothing.
  • A scope names a model, as model:gpt-4o or as the bare id gpt-4o. In allowed_scopes, it limits the capability to that model. In denied_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, or model: 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

FieldTypeDescription
rate_limit_per_minuteintegerMaximum 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 categoryRecommended rate
Database100-500/min
LLM30-60/min
Email/SMS10-30/min
Code execution10-20/min
Storage100-300/min
HTTP outbound60-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's cwd when 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_tokens and the red-team label when they are present.
  • Governed shell: command and cwd.
{
  "agent_id": "agt_data-bot",
  "tool_id": "tool_crm_8k2m",
  "allowed_operations": ["list_contacts", "get_contact"],
  "max_payload_bytes": 10485760
}
FieldTypeDescription
max_payload_bytesintegerMaximum 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:

SizeBytesUse case
1 KB1024Simple API calls, JSON payloads
1 MB1048576Standard file uploads, data exports
10 MB10485760Large file operations
100 MB104857600Bulk 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.

days needs edge v0.22.4 or later. Upgrade your edges before you write days. Edge v0.22.3 refuses every action of a grant whose window has days, 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

FieldTypeDescription
daysarrayThe 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.
startstringStart 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.
endstringEnd 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.
timezonestringIANA 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 monday to friday, 22:00–06:00 is 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 days and no start or end, 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) and blocked_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>) and blocked_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 start equal to end, including 00: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 REST METHOD PATH included), 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 scope or resource argument. After the upgrade, a deny written for a resource can also match a tool's name: *invoice*, meant for invoice ids, also refuses Stripe's list_invoices and create_invoice tools. 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_refund on Stripe also denies stripe__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 as regex:^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 denies create_refund and stripe__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 as REGEX:, is read as regex:.

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

ToolScope patternMatches
Internal CRM (mcp:crm__*)contacts/*Calls whose scope or resource argument is one level under contacts/
Stripestripe__list_*As an allowed scope: calls to Stripe's list_* tools made without a scope or resource argument
OpenAI APImodel:gpt-4oCalls 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 in POST /v1/charges, is accepted;
  • a regex: prefix written in other letters, such as REGEX:^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>:…, or model: 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:

CodeMeaning
204 No ContentCapability removed.
403 ForbiddenThe caller's role is viewer (or otherwise lacks write access to agents).
404 Not FoundNo 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.