# Roles, confirmation and governance

> How MentorAgent checks roles and asks for confirmation before an action runs, on every channel, and what it leaves to your code.

- URL: https://docs.mentoragent.net/concepts/governance
- Verified with: MentorAgent 1.0.0
- Last updated: 2026-10-14
- Packages: Novior.MentorAgent, Novior.MentorAgent.Server

This page explains how MentorAgent decides whether an [action](https://docs.mentoragent.net/concepts/agent-levels#level-1-actions) may run: the role check, the user's confirmation, and what stays in your code. The rules live on the method's attribute, not in the prompt. A model can be persuaded to do anything, but an attribute checked in code cannot.

```csharp title="CaseService.cs"
public class CaseService(ICaseStore store)
{
    [MentorAction(
        Description = "Gets the status of a support case",
        ReadOnly = true)]
    public Task<string> GetStatus(string caseNumber)
        => store.GetStatusAsync(caseNumber);

    [MentorAction(
        Description = "Closes a support case with a resolution",
        RequiresConfirmation = true,
        RequiredRoles = ["Supervisors"])]
    public Task CloseCase(string caseNumber, string resolution)
        => store.CloseAsync(caseNumber, resolution);
}
```

`GetStatus` only reads, so it declares `ReadOnly = true`: it never asks for confirmation. `CloseCase` changes data: only a user in the `Supervisors` role may run it, and only after confirming. A method with `[Description]` alone is an action too, but it has no rules.

## Two checks, in this order

1. **Role.** `RequiredRoles` lists the roles that may run the action: any one of them is enough. MentorAgent finds the signed-in user (see below) and checks the roles with `ClaimsPrincipal.IsInRole`. That method reads the `ClaimTypes.Role` claims, unless your authentication sets another role claim type. A user without the role is refused before anything else happens: no confirmation box appears, and the model receives an authorization error.
2. **Confirmation.** `RequiresConfirmation` asks the user before the method runs. A `[MentorAction]` that is not read-only also asks: see [Confirmation by default](https://docs.mentoragent.net/concepts/governance#confirmation-by-default). The widget shows a confirmation box. A front end of your own, such as a React, Angular, Vue or mobile app, receives the request and sends back the answer. If the user cancels, the method does not run. After the user confirms, MentorAgent checks the roles again, so a user who lost the role while the box was open is refused. The model is told that the user declined, that nothing was checked or changed, and that it must not give another reason.

> **Important:**
> A method with `[Description]` alone has no rules. Anyone who reaches the agent can call it, anonymous users included, and it never asks for confirmation. Use `[MentorAction]` for every method that changes data: it asks for confirmation unless you declare `RequiresConfirmation = false`. Roles stay open until you set `RequiredRoles`.

The rules come with the class. MentorAgent finds the class in the assemblies you list in `ScanAssemblies`, and calls it through dependency injection:

```csharp title="Program.cs"
builder.Services.AddScoped<CaseService>();
builder.Services.AddMentorAgent(options =>
{
    options.AppName = "Case management";
    options.ChatClient = chatClient;
    options.ScanAssemblies = [typeof(CaseService).Assembly];
});
```

The role check needs to know who the user is. MentorAgent finds the user where the request comes from:

- **On the SignalR hub, on `/mentor/chat` and on Agent-to-Agent (A2A)**, the user is the caller of the request: the user of the hub connection, or `HttpContext.User`. MentorAgent reads it before it asks your `AuthenticationStateProvider`. This works in a Blazor Web App too, whatever the order of `AddRazorComponents()` and `AddMentorAgentServer()`: see [Blazor WebAssembly and Auto](https://docs.mentoragent.net/configure/blazor-webassembly#security-and-roles).
- **In a Blazor circuit**, the connection that Blazor Server keeps with each browser tab, the user is the one that your `AuthenticationStateProvider` reports.
- **If your own provider adds claims or roles**, they are not seen on the hub, on `/mentor/chat` or on A2A. Add them with an `IClaimsTransformation`: ASP.NET Core applies it to `HttpContext.User`, and so to the user of the hub connection.
- **If nobody is signed in**, an action with `RequiredRoles` is refused for everyone. This is also the case in a .NET MAUI app that has no `AuthenticationStateProvider`.

## Confirmation by default

A `[MentorAction]` that declares neither `ReadOnly = true` nor `RequiresConfirmation` counts as an action that changes data. It asks for confirmation, like one declared `RequiresConfirmation = true`:

```csharp title="CaseDefaults.cs"
public class CaseDefaults(ICaseStore store)
{
    // Declares nothing, so it counts as a write and asks for confirmation
    [MentorAction(Description = "Archives a support case")]
    public Task ArchiveCase(string caseNumber)
        => store.CloseAsync(caseNumber, "Archived");

    // Only reads, so it never asks
    [MentorAction(Description = "Checks whether a support case is open", ReadOnly = true)]
    public Task<string> IsCaseOpen(string caseNumber)
        => store.GetStatusAsync(caseNumber);

    // A write that runs without asking, because you say so
    [MentorAction(Description = "Closes a duplicate support case", RequiresConfirmation = false)]
    public Task CloseDuplicate(string caseNumber)
        => store.CloseAsync(caseNumber, "Duplicate");
}
```

- `ArchiveCase` declares nothing, so it asks.
- `IsCaseOpen` only reads, so it never asks. Model Context Protocol (MCP) clients receive it with `readOnlyHint`, and some of them skip their own confirmation for such a tool. Declare `ReadOnly = true` only on methods that really only read.
- `CloseDuplicate` runs without asking, because `RequiresConfirmation = false` is your declaration.

An action that asks is not offered to MCP clients and is not on the A2A agent card, because nobody can answer there. Declare `RequiresConfirmation = false` on a write that must stay available on those channels.

This default covers `[MentorAction]` methods only. A method with `[Description]` alone asks for nothing. The first time MentorAgent reads your actions, it logs a warning for the writes that run without confirmation. MentorAgent reads them:

- at startup, when the host maps the A2A agent or turns on [`WarmUpAtStartup`](https://docs.mentoragent.net/reference/options#warmupatstartup);
- at the first MCP request or the first request for the tour;
- otherwise, at the first message.

The warning lists the `[MentorAction]` methods that declare `RequiresConfirmation = false`, and the methods with only `[Description]` that are not declared read-only with [`IsReadOnlyAction`](https://docs.mentoragent.net/reference/options#isreadonlyaction).

To turn the default off, set [`RequireConfirmationForWrites`](https://docs.mentoragent.net/reference/options#requireconfirmationforwrites) to `false`. Then only `RequiresConfirmation = true` asks.

## Tool filtering and confirmation

[`EnableToolFiltering`](https://docs.mentoragent.net/reference/options#enabletoolfiltering) sends the model only the actions relevant to the message. The filter ranks actions by meaning, but its score does not tell whether a message needs data: a question about your data can score below a greeting. So two rules apply, within `ToolFilterMaxTools`:

- When no action is relevant enough, the three best-ranked actions that need no confirmation and no role are sent anyway. An action without confirmation can therefore reach the model on any message.
- When every action it selects needs confirmation, it also sends up to three of the best-ranked actions that do not need confirmation. So an action that needs confirmation does not travel alone.

The filter reads what you declare, even with `RequireConfirmation = false`:

- Every `[MentorAction]` that asks for confirmation, the writes that ask by default included, is kept out of the actions sent when a message matches nothing.
- A write that declares `RequiresConfirmation = false`, and a method with `[Description]` alone, can be offered on any message: use `[MentorAction]` for any method that changes data.
- `ReadOnly = true` says that an action only reads. MentorAgent takes your word for it and does not check it. An action that needs confirmation is never read-only.

## Specialists and teams are gated too

A method behaves the same whether the coordinator calls it or a [specialist or team member](https://docs.mentoragent.net/concepts/agent-levels) does. Specialists and team members call their tools themselves, inside an Agent Framework workflow, where the coordinator's middleware cannot see the calls. So MentorAgent wraps each of their tools, and these travel with the tool:

- the role check and the confirmation;
- the progress shown in the widget;
- the navigation after the action ([`NavigateTo`](https://docs.mentoragent.net/reference/attributes#mentoraction));
- the metrics.

Tools named by an agent defined in YAML are wrapped in the same way.

Remote agents are different. They are agents on other hosts that your agent calls over A2A, listed in [`RemoteAgents`](https://docs.mentoragent.net/reference/options#remoteagents). Their tools run on the other host, under that host's rules, so none of your roles applies to what they do. Any user who reaches your agent can reach them. To restrict a remote agent, do one of these:

- protect the `/a2a` endpoint on the remote host and give roles to the actions there, as in [A2A server](https://docs.mentoragent.net/configure/a2a-server#security-and-roles);
- leave the agent out of `RemoteAgents` on hosts whose users may not use it.

## Where nobody can answer

Some channels have no person at the other end to confirm anything. There, an action that needs confirmation is not left waiting: it is not offered to the caller (it is *withheld*), or it is refused when called. The table shows what happens on each channel:

| Channel | Action with `RequiredRoles` | Action with `RequiresConfirmation` |
|---|---|---|
| Widget in Blazor Server, WebAssembly or Auto | Runs if the signed-in user holds one of the roles, otherwise refused | Confirmation box before it runs |
| Widget in .NET MAUI | Refused, unless your app registers an `AuthenticationStateProvider` that returns the signed-in user | Confirmation box before it runs |
| SignalR hub and SSE, from your own front end | Checked on the user of the request; anonymous callers are refused | Request to the client, answered on `POST /mentor/approve` |
| MCP server | Withheld, unless `McpCallerPrincipal` identifies a caller that holds the role | Never published |
| A2A server | Left out of the agent card, the description that A2A clients read. If the model calls it, it runs only for an authenticated caller who holds the role. An anonymous caller is refused | Left out of the agent card, and refused at once if the model calls it |

[Channels](https://docs.mentoragent.net/concepts/channels) describes each channel.

> **Caution:**
> The hub, SSE, `/mcp` and `/a2a` endpoints are open until you protect them. A confirmation raised over SSE, or on a hub connection with no signed-in user, has no owner: whoever holds its `actionId` can answer it. Keep those endpoints authenticated and treat the `actionId` as a secret.

## What stays in your code

The role check reads roles, not records: there are no per-record or per-company permissions. Filtering the data a method returns, and deciding who may see which record, stays in your method, as it does without an agent.

## Tools you cannot annotate

Tools that come from an MCP server or from a skill have no attribute to put rules on. Two options gate them with a confirmation, so the user must confirm before one of these tools runs:

- [`RequiresConfirmation`](https://docs.mentoragent.net/reference/options#mentormcpserverrequiresconfirmation) on a `MentorMcpServer` gates every tool of that server;
- the [`RequiresApproval`](https://docs.mentoragent.net/reference/options#requiresapproval) predicate gates tools by name, for actions, MCP tools and skills alike.

```csharp title="Program.cs"
// By tool name: an action's snake_case name, or the name an MCP server gives its tool
options.RequiresApproval = tool =>
    tool.StartsWith("delete_", StringComparison.Ordinal)
    || tool is "send_email" or "write_file";
```

The predicate receives the tool name, not the method name. For an action it is the method name in snake case, with a trailing `Async` dropped first: `CloseCaseAsync` is `close_case`. For an MCP tool it is the name its server gives it. Renaming a method renames its tool, and a predicate written for the old name stops matching without any error.

Gate anything with side effects, anything that touches sensitive data and anything irreversible.

## The global switch

`options.RequireConfirmation` is `true` by default. It turns the confirmation boxes of every source on or off at once: attributes, the default for writes, MCP servers and `RequiresApproval`. With `false`, no action asks for confirmation. Role checks still run. The only approvals that still appear are those for the MCP servers that the model provider calls directly ([`HostedMcpServers`](https://docs.mentoragent.net/reference/options#hostedmcpservers)).

Three names look alike:

| Name | Where you set it | What it does |
|---|---|---|
| `RequiresConfirmation` | On `[MentorAction]` | Asks for confirmation before this action runs |
| `RequireConfirmationForWrites` | In `AddMentorAgent` | Makes a `[MentorAction]` that is not read-only ask for confirmation by default |
| `RequireConfirmation` | In `AddMentorAgent` | Turns all confirmation boxes on or off |

## Blocking and Native modes

You need this section only if you use Agent Framework workflows or hosts, or want `AutoApprovalRules`. `HitlMode` chooses how a confirmation travels inside the server. HITL means human in the loop, and Agent Framework calls a confirmation an *approval*. The confirmation box, the client protocol and the model calls are the same in both modes.

|  | `Blocking` (default) | `Native` |
|---|---|---|
| Mechanism | MentorAgent's middleware waits for the answer | Agent Framework's `ApprovalRequiredAIFunction` and approval request and response contents |
| Agent runs | One for the whole turn: the run waits for the answer | One more after each round of confirmations, approved or declined |
| Why choose it | Fewer moving parts | Interoperability with Agent Framework workflows and hosts |

A declined confirmation reads the same to the model in both modes. Stop is safe in both modes too. With `Native`, Stop pressed while a box waits leaves that request open. The next message answers it as not run, in the same model call as the new question, and the conversation goes on. If the run that carried the user's answer fails, MentorAgent treats the action as of unknown outcome and never runs it again. One case is still open: see [Known issues](https://docs.mentoragent.net/whats-new#known-issues).

In `Native` mode, `AutoApprovalRules` can approve a call before any box appears. The rules run in order, and the first one that returns `true` approves the call:

```csharp title="Program.cs"
options.HitlMode = MentorHitlMode.Native;
options.AutoApprovalRules =
[
    // Approve reads without asking; every other gated tool still prompts.
    ctx => ValueTask.FromResult(
        ctx.FunctionCallContent.Name.StartsWith("get_", StringComparison.Ordinal)),
];
```

Setting a rule also switches on Agent Framework's "don't ask again": after the user approves a call, matching calls stop asking for the rest of the session. A rule can skip the question, never the role check: `RequiredRoles` is checked inside MentorAgent's own tool wrapper, which approval rules never reach. A call that [`RequireActionForChangeRequests`](https://docs.mentoragent.net/reference/options#requireactionforchangerequests) requires is never approved by a rule: the box always appears. The calls that the model makes on its own stay with the rules.

The tools of specialists and team members always use the blocking flow, even with `HitlMode = Native`. So `AutoApprovalRules` never approve them, and "don't ask again" does not remember them. If a rule must approve a tool without showing a box, make it an action of the coordinator, not a tool of a specialist or a team member.

## A change that is only described

The checks on this page run when the model calls an action. A model can also answer "Stock updated" without calling anything. Then no box appears, and nothing changes.

[`RequireActionForChangeRequests`](https://docs.mentoragent.net/reference/options#requireactionforchangerequests) closes this gap. It is off by default. A classifier model reads each message. When the message asks for one of your actions that need confirmation, and gives every input that the action needs, the first model call must call that action. The box then comes before any sentence. A request that leaves an input out is not forced, and the agent may ask for it.

The option needs `EnableToolFiltering` and an `EmbeddingGenerator`, and it adds one small classifier call to most turns. Without it, a model can still describe an action that it never performed.

## Excessive agency

OWASP lists the risk behind these checks as [LLM06:2025 Excessive Agency](https://genai.owasp.org/llmrisk/llm062025-excessive-agency/): damaging actions that an agent takes because the model's output was unexpected, ambiguous or manipulated. Two of its mitigations are the two checks on this page: run actions in the user's context, with the user's own roles, and ask the user to approve an action before it runs. Another mitigation, checking every access to a single record (complete mediation), stays in your code: see [What stays in your code](https://docs.mentoragent.net/concepts/governance#what-stays-in-your-code).

---

- Previous: [The three agent levels](https://docs.mentoragent.net/concepts/agent-levels.md)
- Next: [Channels: how clients reach the agent](https://docs.mentoragent.net/concepts/channels.md)
