# RBAC (https://www.agentenvframework.com/docs/environments/rbac)

> Decide which agents access which tools

## Tool Access by Role

An animation of ten agents on one `office` gateway: agents 1 to 4 have the role `assistant`, 5 to 7 `reviewer`, and 8 to 10 `intern`.

1. Before any agent connects, the harness sends two `POST /tools/disable` requests: `reviewer` loses `send`, and `intern` loses `email_search` and `send`. `assistant` has no rules, so it keeps every tool.
2. Ten agents connect to one `office`, each sending its role in `AgentEnv-Role`. Each agent’s `tools/list` holds exactly the tools its role has on.
3. Calls to listed tools go through to `email` and `contacts`. When agent 6 calls `send` and agent 9 calls `email_search` anyway, the gateway refuses them before they reach a server.
4. Mid-run, the harness disables `contacts_lookup` for `assistant`. The rule applies from the next call, and the gateway doesn’t tell the agents.
5. Agent 2 calls `contacts_lookup` from the list it got earlier and gets a tool error: `Tool 'contacts_lookup' is disabled for role 'assistant'`.
6. The assistants’ next `tools/list` leaves `contacts_lookup` out. Agent 2 finds Sam’s address in old mail with `email_search` instead, and sends.

The harness’s requests and the gateway’s answers:

```http
POST /tools/disable {"role": "reviewer", "tools": ["send"]}
→ 200 {"role": "reviewer", "disabled": ["send"], "allowed": []}
POST /tools/disable {"role": "intern", "tools": ["email_search", "send"]}
→ 200 {"role": "intern", "disabled": ["email_search", "send"], "allowed": []}
POST /tools/disable {"role": "assistant", "tools": ["contacts_lookup"]}
→ 200 {"role": "assistant", "disabled": ["contacts_lookup"], "allowed": []}
```

Each role’s `tools/list`, when the agents connect and after the mid-run rule:

| Role | On connecting | After the mid-run rule |
|---|---|---|
| `assistant` | `email_search`, `send`, `contacts_lookup` | `email_search`, `send` |
| `reviewer` | `email_search`, `contacts_lookup` | `email_search`, `contacts_lookup` |
| `intern` | `contacts_lookup` | `contacts_lookup` |

The calls, in order:

- Agent 1 (`assistant`): `email_search {"query": "Q3"}` reaches its server
- Agent 2 (`assistant`): `contacts_lookup {"name": "Priya"}` reaches its server
- Agent 3 (`assistant`): `send {"to": "dana@example.com", "subject": "Q3 numbers", "body": "Attached."}` reaches its server
- Agent 4 (`assistant`): `email_search {"query": "offsite"}` reaches its server
- Agent 5 (`reviewer`): `email_search {"query": "Q3"}` reaches its server
- Agent 6 (`reviewer`): `contacts_lookup {"name": "Dana"}` reaches its server
- Agent 7 (`reviewer`): `email_search {"query": "budget"}` reaches its server
- Agent 8 (`intern`): `contacts_lookup {"name": "Omar"}` reaches its server
- Agent 9 (`intern`): `contacts_lookup {"name": "Sam"}` reaches its server
- Agent 10 (`intern`): `contacts_lookup {"name": "Priya"}` reaches its server
- Agent 1 (`assistant`): `send {"to": "priya.shah@example.com", "subject": "Q3 review", "body": "Thursday at 10?"}` reaches its server
- Agent 3 (`assistant`): `contacts_lookup {"name": "Omar"}` reaches its server
- Agent 5 (`reviewer`): `contacts_lookup {"name": "Sam"}` reaches its server
- Agent 6 (`reviewer`): `send {"to": "dana@example.com", "subject": "Approved", "body": "Looks good."}` is refused: `Tool 'send' is disabled for role 'reviewer'`
- Agent 9 (`intern`): `email_search {"query": "Sam"}` is refused: `Tool 'email_search' is disabled for role 'intern'`
- Agent 10 (`intern`): `contacts_lookup {"name": "Dana"}` reaches its server
- Agent 2 (`assistant`): `contacts_lookup {"name": "Sam"}` is refused: `Tool 'contacts_lookup' is disabled for role 'assistant'`
- Agent 2 (`assistant`): `email_search {"query": "Sam"}` reaches its server
- Agent 2 (`assistant`): `send {"to": "sam.lee@example.com", "subject": "Offsite dates", "body": "Do the 14th and 15th work?"}` reaches its server

Set the rules before the agents connect, and every agent's `tools/list` and calls follow its role;
change them mid-run, and the next call sees the change, so an agent has to find another way.

The gateway keeps rules that turn tools off or on for a role, and applies them to every tool it
serves. They come with the [gateway topology](https://www.agentenvframework.com/docs/environments/gateway-topology.md), on the card as
`urn:agentenv:disable-tool/v1` and `urn:agentenv:enable-tool/v1`, so `email` and `contacts` know
nothing of roles.

## The Caller's Role

A caller names its role in the `AgentEnv-Role` header, over MCP and `/step` alike, and a request
without the header is `default`. The gateway takes the header as given: a role decides what an agent
is offered, not who may connect.

## Disable and Enable Tools

`POST /tools/disable` and `POST /tools/enable` take a `role` and its `tools`, a list of names or
`"*"` for all of them, and answer with that role's rules:

```bash title="Terminal"
curl -sS -X POST http://localhost:61854/tools/disable -H 'content-type: application/json' \
  -d '{"role": "reviewer", "tools": ["send"]}'
```

```json title="Output"
{"role": "reviewer", "disabled": ["send"], "allowed": []}
```

A body without a `role`, or with `tools` that is neither a list of names nor `"*"`, gets a 400. Tool
names aren't checked, so a rule can name a tool the instance doesn't serve yet.

## What a Role Sees

A tool that is off for a role is left out of that role's `tools/list` over MCP and `list_tools` over
`/step`. If the agent calls it anyway, the call never reaches the server: `/step` answers 403, and MCP
returns the same message as a tool error with `isError: true`.

```bash title="Terminal"
curl -sS -X POST http://localhost:61854/step \
  -H 'AgentEnv-Role: reviewer' \
  -d '{"action": "call_tool", "tool_name": "send", "arguments": {"to": "sam.lee@example.com", "subject": "Q3 plan", "body": "Draft attached."}}'
```

```json title="Output (403)"
{"error": "Tool 'send' is disabled for role 'reviewer'"}
```

The rules cover the gateway's own tools too, such as `get_time`. The gateway doesn't tell an agent
that its tools changed: its next list shows the change, and every call is checked when it arrives.

## How Rules Combine

For each call, the gateway looks for four rules in order, and the first one it finds decides. A tool
that none of them names is on.

| Order | Rule                         |
| ----- | ---------------------------- |
| 1     | The role's rule for the tool |
| 2     | The role's rule for `*`      |
| 3     | Role `*`'s rule for the tool |
| 4     | Role `*`'s rule for `*`      |

Enabling doesn't remove a rule, it overwrites it. Enabling a tool after disabling it leaves an
`allowed` rule, which comes before any rule of role `*`.

Wildcards overwrite more. Tools `"*"` replace all of the role's rules with one, role `"*"` also writes
its rule into every role that has rules, and `"*"` for both replaces every rule of every role.

## Read the Rules

`GET /state` lists each role's rules next to the servers and their tools. The rules live in the
gateway's memory, and no request deletes one: sending `"*"` for both with `enable` leaves a single
rule that turns every tool on.

```json title="GET /state (roles)"
{
  "roles": {
    "*": {"disabled": ["send"], "allowed": []},
    "reviewer": {"disabled": [], "allowed": ["send"]}
  }
}
```

## From a Task

The `modify_env_tool_access` step sends one of these requests to a deployed env. It takes `env_id`, `action`
(`disable` or `enable`), `role` and `tools`, all required; `tools` is always a list, and `["*"]`
means all tools.

```json title="The reviewer-no-send step"
{"id": "reviewer-no-send", "type": "modify_env_tool_access", "env_id": "office", "action": "disable", "role": "reviewer", "tools": ["send"], "depends_on": [{"task_step_id": "office"}]}
```

The step finds the endpoint on the instance's card and fails before sending anything if the card
doesn't list the action. It records the role's rules afterwards in `metadata["tool_access_changes"]`.

## One Role per Agent

[`deploy_agent`](https://www.agentenvframework.com/docs/tasks/important-steps.md#deploy_agent)'s `role` gives an agent its role,
through the agent's `urn:agentenv:agent-config/v1` when that lists `role` among its fields. The SDK
hands it to `run()` as `request.config.role`, and the agent's own code sends it in `AgentEnv-Role`
on its calls to the gateway:

```json title="Two agents on one office"
[
  {"id": "assistant", "type": "deploy_agent", "env_ids": ["office"], "agent_name": "assistant", "role": "assistant", "depends_on": [{"task_step_id": "office"}]},
  {"id": "reviewer", "type": "deploy_agent", "env_ids": ["office"], "agent_name": "reviewer", "role": "reviewer", "depends_on": [{"task_step_id": "office"}]}
]
```

With `reviewer-no-send` after them, both agents share one instance, and only `assistant` can send.
The `verify_a2a_role` step checks that an agent accepts a role and reads it back, not that it sends
the header. The CLI that `build_mcp_cli` builds sends the value of `AGENT_ENV_ROLE`, or `cli` when
that is unset.

## Change Access Mid-Run

A trigger's `permission` action sets the same rules when its condition fires, so a role can gain or
lose a tool partway through a run. [Triggers](https://www.agentenvframework.com/docs/environments/triggers.md) covers the conditions and
the `register_env_triggers` step.

```json title="A permission action"
{"type": "permission", "action": "disable", "role": "assistant", "tools": ["send"]}
```

## Without the Gateway

In the `server` [topology](https://www.agentenvframework.com/docs/environments/topology.md) nothing sits in front of your server, so
there are no rules unless it implements the two extensions itself. `modify_env_tool_access` calls
whatever the card lists, so the same step drives either;
[Env Topology Extensions](https://www.agentenvframework.com/docs/environments/environment-card.md#env-topology-extensions) compares the
two cards.