MentorAgent Docs
Menu

ConceptPackages: Novior.MentorAgent, Novior.MentorAgent.Server

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.

View as Markdown
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.

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.

  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. 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:

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.
  • 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.

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:

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;
  • 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.

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.

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 /a2a endpoint on the remote host and give roles to the actions there, as in A2A server;
  • leave the agent out of RemoteAgents on hosts whose users may not use it.

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:

ChannelAction with RequiredRolesAction with RequiresConfirmation
Widget in Blazor Server, WebAssembly or AutoRuns if the signed-in user holds one of the roles, otherwise refusedConfirmation box before it runs
Widget in .NET MAUIRefused, unless your app registers an AuthenticationStateProvider that returns the signed-in userConfirmation box before it runs
SignalR hub and SSE, from your own front endChecked on the user of the request; anonymous callers are refusedRequest to the client, answered on POST /mentor/approve
MCP serverWithheld, unless McpCallerPrincipal identifies a caller that holds the roleNever published
A2A serverLeft 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 refusedLeft out of the agent card, and refused at once if the model calls it

Channels describes each channel.

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 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:

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.

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:

NameWhere you set itWhat it does
RequiresConfirmationOn [MentorAction]Asks for confirmation before this action runs
RequireConfirmationForWritesIn AddMentorAgentMakes a [MentorAction] that is not read-only ask for confirmation by default
RequireConfirmationIn AddMentorAgentTurns all confirmation boxes on or off

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
MechanismMentorAgent's middleware waits for the answerAgent Framework's ApprovalRequiredAIFunction and approval request and response contents
Agent runsOne for the whole turn: the run waits for the answerOne more after each round of confirmations, approved or declined
Why choose itFewer moving partsInteroperability 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:

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 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.

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.

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.

  • 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.