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.
On this page
This page explains how MentorAgent decides whether an action 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.
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
Section titled “Two checks, in this order”- Role.
RequiredRoleslists 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 withClaimsPrincipal.IsInRole. That method reads theClaimTypes.Roleclaims, 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. - Confirmation.
RequiresConfirmationasks the user before the method runs. A[MentorAction]that is not read-only also asks: see 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.
The rules come with the class. MentorAgent finds the class in the assemblies you list in ScanAssemblies, and calls it through dependency injection:
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/chatand on Agent-to-Agent (A2A), the user is the caller of the request: the user of the hub connection, orHttpContext.User. MentorAgent reads it before it asks yourAuthenticationStateProvider. This works in a Blazor Web App too, whatever the order ofAddRazorComponents()andAddMentorAgentServer(): see Blazor WebAssembly and Auto. - In a Blazor circuit, the connection that Blazor Server keeps with each browser tab, the user is the one that your
AuthenticationStateProviderreports. - If your own provider adds claims or roles, they are not seen on the hub, on
/mentor/chator on A2A. Add them with anIClaimsTransformation: ASP.NET Core applies it toHttpContext.User, and so to the user of the hub connection. - If nobody is signed in, an action with
RequiredRolesis refused for everyone. This is also the case in a .NET MAUI app that has noAuthenticationStateProvider.
Confirmation by default
Section titled “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:
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");
}ArchiveCasedeclares nothing, so it asks.IsCaseOpenonly reads, so it never asks. Model Context Protocol (MCP) clients receive it withreadOnlyHint, and some of them skip their own confirmation for such a tool. DeclareReadOnly = trueonly on methods that really only read.CloseDuplicateruns without asking, becauseRequiresConfirmation = falseis 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; - 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.
To turn the default off, set RequireConfirmationForWrites to false. Then only RequiresConfirmation = true asks.
Tool filtering and confirmation
Section titled “Tool filtering and confirmation”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 = truesays 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
Section titled “Specialists and teams are gated too”A method behaves the same whether the coordinator calls it or a specialist or team member 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); - 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. 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
/a2aendpoint on the remote host and give roles to the actions there, as in A2A server; - leave the agent out of
RemoteAgentson hosts whose users may not use it.
Where nobody can answer
Section titled “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 describes each channel.
What stays in your code
Section titled “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
Section titled “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:
RequiresConfirmationon aMentorMcpServergates every tool of that server;- the
RequiresApprovalpredicate gates tools by name, for actions, MCP tools and skills alike.
// 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
Section titled “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).
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
Section titled “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.
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:
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 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
Section titled “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 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
Section titled “Excessive agency”OWASP lists the risk behind these checks as LLM06:2025 Excessive Agency (external site): 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.
See also
Section titled “See also”- Attributes — The parameters of MentorAction, MentorAgent, MentorTeam, TeamMember, MentorPage and MentorSkill, with their defaults and rules.
- Configuration options — Every MentorOptions property with its type, default and effect, grouped by area, plus the options of the WebAssembly client and the YAML loader.
- Secure the endpoints — Protect the SignalR hub, Server-Sent Events, /mcp and /a2a endpoints with authentication and CORS before you expose them.
