# Limit what an agent may do

Grant an AI agent a capability, set limits on the details of each action, and add project rules that deny an action or send it to a person.

An AI agent starts with no permissions. This guide shows you how to give an agent a permission, grant it a capability that limits the details of what it may do, and add rules that hold for the whole project.

## How permissions, capabilities, limits and rules fit together

Four things decide what an agent may do.

- A **permission** is an action on one kind of thing, such as `transfer:accounts`. An agent holds a permission the way a person does: through a role.
- A **capability** names that permission on the agent itself, and can carry limits on it. The console shows a capability as a _permission_ on the agent's **Permissions** tab.
- A **limit** is a condition on the details of one action. It belongs to one capability on one agent: this agent may transfer, up to 5,000.
- A **rule** applies across the project, to the agents and people it names. A rule can deny an action, or make it wait for a person to approve it. The console calls a rule a _policy_.

Each time the agent [asks before acting](/agents/approvals), AuthFI checks the limits and the rules, and whether the agent holds the permission. The names of an agent's capabilities travel in its access token, in the `capabilities` and `scope` claims. The limits do not. They stay with AuthFI, so a changed limit applies to the agent's next call.

For the model behind this, see [How agent sign-in works](/agents/how-it-works#what-an-agent-is-allowed-to-do).

## Before you begin

You need:

- A registered agent. See [Give an AI agent an identity](/quickstart/agent).
- Administrator access to the AuthFI console.

<Steps>

### Give the agent the permission

An agent holds a permission the way a person does: through a role.

1. In the console, open **Identity > Roles & permissions** and select the **Permissions** tab. Choose **New permission**, enter `transfer` as the **Action** and `accounts` as the **Resource**, and choose **Add permission**. The permission's key is `transfer:accounts`.
2. On the **Roles** tab, choose **New role**, enter a **Name** such as `Payments agent`, and choose **Create role**. On the role's page, select `transfer:accounts`, choose **Save changes**, and confirm with **Save**.
3. Choose **Give this role**, then **An AI agent**. Pick the agent and choose **Give the role**.

A permission is about one kind of thing. `transfer:accounts` covers the objects written `account:…`, such as `account:4821`.

### Grant a capability

1. In the console, open **Identity > AI Agents** and select the **Agents** tab.
2. Open the agent, and select its **Permissions** tab.
3. Under "What it may do on the platform", choose **Grant a permission**.
4. Fill in the dialog, then choose **Grant**.

| Field      | Enter                                                          |
| ---------- | -------------------------------------------------------------- |
| Permission | The capability, chosen from the project's permission catalogue |
| Limits     | Optional. The limits on this capability, as JSON.              |
| Reason     | Why the agent needs it. It is recorded with the grant.         |

Choose a permission whose name means something to the people who read it in an approval. This guide uses `transfer:accounts`.

The capability appears in the table on the **Permissions** tab, with its limits in the "Limited to" column.

### Write the limits

In the **Limits** field, name each field of the action's details and the values it may take. The details are the `context` the agent sends when it asks before acting.

```json
{
  "amount": { "max": 5000 },
  "currency": { "in": ["USD", "EUR"] },
  "region": { "not_in": ["sanctioned"] },
  "account_type": "business"
}
```

| Form                  | Passes when                         |
| --------------------- | ----------------------------------- |
| `{ "max": 5000 }`     | The value is a number, 5000 or less |
| `{ "min": 1 }`        | The value is a number, 1 or more    |
| `{ "in": [...] }`     | The value is one of the list        |
| `{ "not_in": [...] }` | The value is not in the list        |
| A plain value         | The value is exactly that           |

The field shows `{"amount": {"max": 2000}}` as an example.

You can also set limits while you register an agent, in the "What it may do" step. There you choose **Add a limit** and pick an operator by name.

| Operator in the register step | Form     |
| ----------------------------- | -------- |
| at most                       | `max`    |
| at least                      | `min`    |
| is one of                     | `in`     |
| is never                      | `not_in` |

### Add a rule for the whole project

1. In the console, open **Security > Triage** and select the **Rules** tab.
2. Under "Policies", choose **New policy**.
3. Enter a "Name", choose an "Effect", choose who the rule applies to in "Applies to", and choose **Create draft**.
4. In the editor, choose **Validate**, then **Enable**, then **Save**.

| Effect          | What it does                                    |
| --------------- | ----------------------------------------------- |
| Deny            | The action is denied. Nothing overrides a deny. |
| Require step-up | The action waits for a person to approve it     |

A rule names whom it applies to, an action, the things it applies to, and a condition on the action's details.

```json
{
  "subjects": ["agent:*"],
  "relation": "transfer:accounts",
  "object": "account:*",
  "condition": { "field": "amount", "op": "gte", "value": 1000 }
}
```

`agent:*` is every agent, shown as **Every agent** in "Applies to". A condition compares a field with `gte`, `gt`, `lte`, `lt`, `eq` or `ne`.

With the effect "Require step-up", the rule above says that transfers of 1,000 or more need a person. It is one rule, and it holds for every agent you register from then on.

A new policy is marked "Draft". After you enable it, it is marked "Live".

</Steps>

## Check that it works

Test a decision in the console first.

1. Open **Security > Authorization** and select the **Evaluate** tab.
2. Under "Would this be allowed?", enter the "Subject", the "Action" and "On what". For the example agent, the subject is `agent:agt_7f3a9c21`, the action is `transfer:accounts` and the object is `account:4821`.
3. Choose **Check**.

| The console answers | Meaning                       |
| ------------------- | ----------------------------- |
| Yes                 | The action is allowed         |
| No                  | The action is denied          |
| Not on their own    | The action waits for a person |

Then ask AuthFI to decide, as the agent does. A transfer of 100 is inside every limit, and below the 1,000 at which the rule asks for a person.

First bind a transaction token to the action. A transaction token names one action and its details, and lasts 60 seconds.

```bash
curl https://acme.authfi.io/apis/regional/v1/ai/auth/transaction-token \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "transfer:accounts",
    "resource": "account:4821",
    "context": { "amount": 100, "currency": "USD", "account_type": "business" }
  }'
```

Then ask for the decision, with the `transaction_token` from the response.

```bash
curl https://acme.authfi.io/apis/regional/v1/agents/authorize \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "agt_7f3a9c21",
    "transaction_token": "'"$TRANSACTION_TOKEN"'",
    "action": "transfer:accounts",
    "resource": "account:4821",
    "context": { "amount": 100, "currency": "USD", "account_type": "business" }
  }'
```

`$ACCESS_TOKEN` is the agent's access token. `agt_7f3a9c21` is its agent ID, shown as "Client id" on the agent's **Connect** tab. Both requests are explained in [Ask before a sensitive action](/agents/approvals).

```json
{ "data": { "decision": "allow", "reason": "relationship" } }
```

Now break a limit. With the limits above, a transfer of 10,000 is over the `amount` limit. Send the first request again with `"amount": 10000`. AuthFI does not bind a token to it, and answers `403`:

```json
{ "error": "invalid_body", "error_description": "constraint_violated" }
```

An agent never holds a transaction token for an action outside its limits, so it cannot go on to ask for a decision.

## How a limit is evaluated

With the limits and the rule above, the same capability gives different answers for different details.

| The agent asks for                                                 | Answer                                                 |
| ------------------------------------------------------------------ | ------------------------------------------------------ |
| `{ "amount": 100, "currency": "USD", "account_type": "business" }` | `allow`                                                |
| `{ "amount": 1200, ... }`                                          | `approval_required`: the rule sends it to a person     |
| `{ "amount": 10000, ... }`                                         | No transaction token: `403` with `constraint_violated` |
| `{ "currency": "GBP", ... }`                                       | No transaction token: `403` with `constraint_violated` |
| No `amount` at all                                                 | No transaction token: `403` with `constraint_violated` |

A field named in `max`, `min` or `in` must be sent, and so must a field limited to one value, such as `account_type`. A request that leaves it out is denied, so an agent cannot pass a limit by omitting the thing it limits.

The limits are checked again when the agent asks for the decision. If a limit was lowered after the token was bound, the answer is `deny` with a reason such as `constraint_violated: amount max`.

## The decision and its reason

Every decision comes with its reason.

| `decision`          | `reason`                         | Meaning                                |
| ------------------- | -------------------------------- | -------------------------------------- |
| `allow`             | `resource_grant`, `relationship` | Something granted it                   |
| `deny`              | `no_match`                       | Nothing granted it                     |
| `deny`              | `policy_deny`                    | A rule denies it                       |
| `deny`              | `constraint_violated: …`         | A limit on the capability was exceeded |
| `approval_required` | `policy_step_up`                 | A rule sends it to a person            |

The same reason is written to the [audit record](/security/audit), so you can see afterwards which limit stopped which action.

## The order of a decision

AuthFI makes each decision in this order. The first check that fails gives the answer.

1. Is the agent known and not revoked? If not, the response is `401`.
2. Do the action's details pass the limits on the capability? If not, `deny`.
3. Does a rule deny it? If so, `deny`.
4. Is it granted? If not, `deny`.
5. Does a rule ask for a person? If so, `approval_required`.
6. Otherwise, `allow`.

## What is checked for a delegated agent

An agent is `delegated` or `autonomous`. See [Two types of agent](/agents/how-it-works#two-types-of-agent).

A delegated agent that acts for a person through the [MCP gateway](/agents/mcp-gateway) can do only what both of them may do. The agent's grants and the person's grants are both checked. If either one does not allow the call, it is denied.

## Troubleshooting

| What you see                                           | Cause                                                        | Fix                                                                               |
| ------------------------------------------------------ | ------------------------------------------------------------ | --------------------------------------------------------------------------------- |
| `deny` with `no_match`                                 | Nothing grants the agent this action on this object          | Give the agent a role that carries the permission, and name an object of its kind |
| `403` with `constraint_violated` when you bind a token | A detail is outside a limit, or a limited field was not sent | Send every limited field, within its limit                                        |
| `deny` with `constraint_violated: …`                   | A limit was lowered after the token was bound                | Bind a new token, within the current limits                                       |
| `deny` with `policy_deny`                              | A project rule denies the action                             | Change the policy. No grant overrides a deny.                                     |
| `401`                                                  | The agent is unknown or revoked                              | Check the agent ID. A revoked agent stays revoked.                                |

A decision of `approval_required` is not a failure. The action waits for a person. See [Wait for a person](/agents/approvals#wait-for-a-person).

## Next steps

<Cards>
  <Card href="/agents/approvals" title="Ask before a sensitive action">
    Get a decision for one action, and wait for a person when a rule requires one.
  </Card>
  <Card href="/agents/mcp-gateway" title="The MCP gateway">
    Grant tools one by one, and apply your rules to every tool call.
  </Card>
  <Card href="/agents/revocation" title="Revoke an agent">
    End an agent's access in one action.
  </Card>
  <Card href="/concepts/access" title="Roles, groups and grants">
    How grants, groups and rules decide access across the project.
  </Card>
</Cards>
