Questa pagina non è ancora tradotta in italiano: qui la trovi in inglese.
Action policies and observers
The types that let your code decide on each action, record every decision of the gate, know who confirmed a call, and list the actions.
On this page
The tool gate is the part of MentorAgent that decides whether a call to one of your actions may run: the roles, the confirmation, then the action. This page lists the types that let your code take part in it. An action policy decides, in your code, whether a user may use an action and whether a call may run. An observer receives every decision of the gate. MentorActionConfirmation tells an action who confirmed it. IMentorActionCatalog lists your actions as MentorAgent sees them.
Roles, confirmation and governance explains when to use them, with an example of a policy. The types are in the Novior.MentorAgent.Abstractions.Interfaces and Novior.MentorAgent.Abstractions.Models namespaces. The registration methods are in Novior.MentorAgent.Extensions. All of them are new in 1.2.0.
The order of a call
Section titled “The order of a call”A call to one of your actions is decided in this order:
- the roles of the action,
RequiredRoles; IsAllowedAsyncof every policy;BeforeActionAsyncof every policy;- the confirmation: the action's own, or the one that a policy asked for;
- the action.
A call refused or stopped before step 4 shows no confirmation box. After the user confirms, MentorAgent asks the roles and IsAllowedAsync again, so a permission taken away while the box was open stops the action.
IMentorActionPolicy
Section titled “IMentorActionPolicy”A class of yours that decides on the application's actions. Register it with AddMentorActionPolicy<T>(), or an instance with AddMentorActionPolicy(policy). A policy is a singleton. Every policy registered is asked, in the order of registration. Both methods have a default body, so a policy writes only the one it needs.
| Method | Default | What it decides |
|---|---|---|
IsAllowedAsync(MentorActionRequest, CancellationToken) | true | Whether this user may use this action at all. Asked with Stage = Offer when MentorAgent decides what to offer: at every turn, for each action of the coordinator, and at every tools/list of the MCP server. An action that it does not allow is left out, like one the user has no role for. Asked again with Stage = Call before every call, and after the user confirms. A call that it does not allow is refused. With several policies, the first false decides |
BeforeActionAsync(MentorActionRequest, CancellationToken) | Proceed | Whether this call may run, with its arguments: a MentorActionVerdict. Every policy is asked, and the strictest answer wins: Stop, then Confirm, then Proceed |
Where policies are asked.
| Surface | IsAllowedAsync at Offer | Before a call | A Confirm verdict |
|---|---|---|---|
The widget, the hub and /mentor/chat, in both modes | At every turn | IsAllowedAsync, then BeforeActionAsync | The usual confirmation box |
Specialists, team members and the agents of an IMentorAgentSource | Not asked: their roles decide what they are offered | IsAllowedAsync, then BeforeActionAsync | The usual confirmation box |
| A task from another agent (A2A) | At every turn | The same | Refused, because nobody can confirm |
| The MCP server | At every tools/list | At every tools/call | Refused, because nobody can confirm |
Policies decide on your actions and on the tools of the MCP servers that you consume. They never decide on MentorAgent's own tools, such as navigate_to, remember, load_skill, route_to_specialist, a team's tool or invoke_ui_action, so not on the UI actions of a page either. The A2A agent card is not filtered: it is one card for every caller.
A policy that fails stops the call. Each call to a policy may take ActionPolicyTimeout, 5 seconds by default. A policy that throws, returns null or does not answer in time stops the call, and the model reads that the check could not be completed. When MentorAgent decides what to offer, such a policy leaves the action out. The log names the type of the exception, never its message.
IsAllowedAsync is asked for every action offered, by every policy, on every turn: keep it fast, for example with a cache per user.
MentorActionRequest
Section titled “MentorActionRequest”What a policy is asked about.
| Property | Type | What it holds |
|---|---|---|
Stage | MentorActionStage | Offer when MentorAgent decides what to offer, Call when a call is about to run |
ToolName | string | The technical name of the tool, such as close_case |
Description | string? | The description that the model reads |
Arguments | IReadOnlyDictionary<string, object?>? | At Call, the arguments by parameter name, as the model or the MCP client sent them, usually a JsonElement each. Nothing has checked them: read them as you would read the body of a request. null at Offer |
User | ClaimsPrincipal? | The signed-in user, or null when nobody is signed in. On the MCP server, the caller that McpCallerPrincipal returns, or else the user of the request |
Surface | MentorActionSurface | Where the call comes from: InProcess (a Blazor Server circuit or a MAUI app), Hub, Sse, Mcp or A2A |
AgentKind, AgentName | MentorAgentKind?, string? | The agent that asks: Coordinator, Specialist, TeamMember or AgentSource, with its name when it has one |
SessionId, TurnId, CallId | string? | Opaque IDs of the session, of the user turn and of the call. CallId is null at Offer, and TurnId on the MCP server |
PageName | string? | The page that the user is on, as it named itself with IMentorPageContext.SetPageName |
RequiredRoles | IReadOnlyList<string> | The roles of the action: any one of them is enough. Empty when it requires none |
RequiresConfirmation | bool | Whether the action asks for a confirmation of its own |
Services | IServiceProvider | The services of the conversation: the circuit, the hub connection, the request or the A2A task. Resolve your scoped services here, such as a DbContext |
MentorActionVerdict
Section titled “MentorActionVerdict”What BeforeActionAsync answers for one call. Its Kind is a MentorActionVerdictKind, and its Reason is your text.
| Verdict | Effect |
|---|---|
MentorActionVerdict.Proceed | No objection. It never removes a confirmation that the action asks for |
MentorActionVerdict.Confirm(reason) | Asks the user to confirm first, even for an action that asks for no confirmation. Where nobody can confirm, on the MCP server or in a task from another agent, the call is refused |
MentorActionVerdict.Stop(reason) | The call does not run. The model reads that a rule stopped it, followed by your reason, never that the user declined |
The reason reaches the model and the observers. Never put a secret in it.
IMentorActionObserver
Section titled “IMentorActionObserver”Receives every decision of the gate, for every session and every surface of the process, and for every agent that calls a tool. Use it for an audit trail: who asked for which action, and why the gate decided as it did. Register it with AddMentorActionObserver<T>(), or an instance with AddMentorActionObserver(observer).
public class ActionAuditLog(ILogger<ActionAuditLog> logger) : IMentorActionObserver
{
public ValueTask OnDecisionAsync(
MentorActionDecision decision, CancellationToken cancellationToken)
{
logger.LogInformation(
"{Tool}: {Outcome} ({Reason}) for {User}, confirmed by {ConfirmedBy}",
decision.ToolName, decision.Outcome, decision.ReasonCode,
decision.User?.Identity?.Name ?? "anonymous",
decision.ConfirmedBy?.Identity?.Name ?? "nobody");
return ValueTask.CompletedTask;
}
}- One method.
OnDecisionAsync(MentorActionDecision, CancellationToken)receives one decision. - Never in the way. An observer never runs on the path of the action. Each observer has a queue of its own and receives the decisions in the order they were taken. A slow observer, or one that throws, delays and stops nothing, and its failure is logged.
- Five seconds a call. A call that has not finished in 5 seconds is no longer awaited, and its token is cancelled. The next decision can then arrive while that call still runs, so an observer must be thread-safe.
- A singleton. It is resolved once, from the root services of the application. An observer that needs scoped services creates a scope with
IServiceScopeFactory. - No record of its own. MentorAgent keeps no copy of the decisions. A record of who confirmed what, built on an observer, and what it can miss: see Audit: who confirmed what.
MentorActionDecision
Section titled “MentorActionDecision”One decision of the gate. It never carries the arguments of the call.
| Property | What it holds |
|---|---|
Outcome | What the gate decided: a MentorActionOutcome |
ReasonCode | Why: one of the MentorActionReasons |
ToolName | The technical name of the tool |
DisplayName | The text shown to the user for the tool |
Surface | InProcess, Hub, Sse, Mcp or A2A |
AgentKind, AgentName | The agent that asked, with its name. null on the MCP server, where the caller names the tool itself |
SessionId, TurnId, CallId | Opaque IDs of the session, of the user turn and of the call. On the MCP server, the session is mcp# and a fingerprint, never the session ID itself |
ConfirmationId | The ID of the confirmation, on the decisions that belong to one |
User | Who asked for the action, or null when nobody is signed in |
RequiredRoles | The roles of the action |
ConfirmedBy | On Confirmed, who confirmed: the caller of POST /mentor/approve, or the signed-in user of the session in a circuit and in a MAUI app |
Timestamp | When the gate decided, in UTC |
ExceptionType | On Failed and PolicyFailed, the full name of the type of the exception, never its message |
Detail | The reason that a policy gave, on Blocked, on ConfirmationRequested and on Denied with nobody-to-confirm. null otherwise |
MentorActionOutcome
Section titled “MentorActionOutcome”| Outcome | When |
|---|---|
Allowed | The roles and every policy let the call through. The confirmation, if any, comes next |
Denied | Refused, for example for a missing role, or because a policy does not allow the action |
ConfirmationRequested | The user is asked to confirm |
Confirmed | The user said yes |
Rejected | The user said no, or nobody answered within ConfirmationTimeout |
Stopped | Stopped before it finished: Stop, the end of the session or the end of the turn |
Blocked | A policy stopped the call with Stop. Its reason is in Detail |
PolicyFailed | A policy threw or did not answer in time, so the call did not run |
Executed | The action ran |
Failed | The action threw |
A call goes through the outcomes in one of these orders, the same in Blocking and in Native mode:
Allowed, thenExecuted,FailedorStopped;Allowed,ConfirmationRequested, thenRejected,StoppedorConfirmed. AfterConfirmedcomes the outcome of the run, orDeniedwhen a role or a permission was taken away while the box was open;Denied,BlockedorPolicyFailedalone.
MentorActionReasons
Section titled “MentorActionReasons”The reason codes, as string constants. They stay the same across versions: a new code comes only in a minor version, and the release notes of that version say so.
| Outcome | Reason codes |
|---|---|
Allowed | allowed; auto-approved when, in Native mode, a rule of AutoApprovalRules approved the call |
Denied | role-missing; login-required (a role is required and nobody is signed in); auth-not-configured (no AuthenticationStateProvider); auth-check-failed (reading the user threw); not-authorized (a policy does not allow the action); nobody-to-confirm; on the MCP server, also withheld and rate-limited |
ConfirmationRequested, Confirmed, Rejected | operator; policy when a policy asked for the confirmation; confirmation-timeout on Rejected when nobody answered in time |
Stopped | stop; session-ended; turn-ended |
Executed | completed; result-withheld when OnToolResult threw |
Failed | exception |
Blocked | policy |
PolicyFailed | policy-exception; policy-timeout |
MentorActionConfirmation
Section titled “MentorActionConfirmation”Inside the method of an action, MentorActionConfirmation.Current says who confirmed this call, and when. It works in Blocking and in Native mode, and it is null for a call that nobody confirmed. So a ledger of your writes can record who approved each one.
public class CaseEscalation(ILogger<CaseEscalation> logger)
{
[MentorAction(
Description = "Escalates a support case to the second line",
RequiresConfirmation = true)]
public string EscalateCase(string caseNumber)
{
// Who confirmed this call, and when: null when nobody confirmed it
var confirmation = MentorActionConfirmation.Current;
logger.LogInformation(
"Case {Case} escalated, confirmed by {User} at {Time}",
caseNumber,
confirmation?.ConfirmedBy?.Identity?.Name,
confirmation?.ConfirmedAt);
return $"Case {caseNumber} escalated.";
}
}| Property | What it holds |
|---|---|
ConfirmationId | The ID of the confirmation, the same as MentorActionDecision.ConfirmationId |
ToolName | The tool that was confirmed |
ConfirmedAt | When the user confirmed, in UTC |
ConfirmedBy | Who confirmed: the caller of POST /mentor/approve, or the signed-in user of the session in a circuit and in a MAUI app |
IMentorActionCatalog
Section titled “IMentorActionCatalog”Registered by AddMentorAgent(). Its GetActions() lists every [MentorAction] of the coordinator, of the specialists and of the team members, one MentorActionInfo each, decided again at every call. Use it for an administration page, or for a test that fails when an action is published by mistake:
// The writes that the MCP server publishes to every caller, for example
// to fail a test when one appears
public static IEnumerable<string> WritesPublishedToEveryone(
IServiceProvider services) =>
services.GetRequiredService<IMentorActionCatalog>().GetActions()
.Where(action => !action.ReadOnly
&& action.McpExposure == MentorMcpExposure.Published)
.Select(action => action.ToolName);MentorActionInfo property | What it holds |
|---|---|
ToolName, Description | The tool, and the description that the model reads |
AgentKind, AgentName | Coordinator, Specialist or TeamMember, with the name of the agent |
RequiredRoles | The roles of the action |
RequiresConfirmation | Whether a call asks for a confirmation now, the RequiresApproval predicate included |
ReadOnly | Whether the action is declared as only reading, and asks for no confirmation |
OnAgentCard | Whether the A2A agent card lists it. false when the host does not serve A2A |
McpExposure | A MentorMcpExposure: Published to every caller; PublishedToRoles, to the callers that McpCallerPrincipal identifies with one of its roles; Withheld, because it asks for a confirmation or needs a role with no McpCallerPrincipal; NotServed, because the host does not map the MCP server, the action belongs to a specialist or a team, or its class is not registered |
The catalog says what MentorAgent's own rules decide. A policy can still leave an action out for one user. The tools of the MCP servers that you consume, and those of an agent source, are not in it.
Other new types
Section titled “Other new types”| Type | What it is |
|---|---|
MentorActionStarted | The argument of IMentorStateService.OnActionStarted: the technical name of the tool next to the text shown to the user. OnActionExecuting carries only the text shown. Raised where the turn runs, not by the WebAssembly client |
MentorReplyStarted | The argument of the hub event ReplyStarted, sent before each reply of the model: { "aiGenerated": true }. See ShowAiDisclosure |
MentorSessionContextPlacement | The values of SessionContextPlacement: Instructions or LastUserMessage |
Vedi anche
Sezione “Vedi anche”- Ruoli, conferma e governance — Ruoli e approvazione human-in-the-loop per i tool di un agente AI in .NET: controlli nel codice, non nel prompt, su ogni canale, e cosa resta a te.
- 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.
- Attributes — The parameters of MentorAction, MentorAgent, MentorTeam, TeamMember, MentorPage and MentorSkill, with their defaults and rules.
