MentorAgent Docs
Menu

ReferencePackages: Novior.MentorAgent, Novior.MentorAgent.Abstractions

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.

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

A call to one of your actions is decided in this order:

  1. the roles of the action, RequiredRoles;
  2. IsAllowedAsync of every policy;
  3. BeforeActionAsync of every policy;
  4. the confirmation: the action's own, or the one that a policy asked for;
  5. 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.

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.

MethodDefaultWhat it decides
IsAllowedAsync(MentorActionRequest, CancellationToken)trueWhether 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)ProceedWhether 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.

SurfaceIsAllowedAsync at OfferBefore a callA Confirm verdict
The widget, the hub and /mentor/chat, in both modesAt every turnIsAllowedAsync, then BeforeActionAsyncThe usual confirmation box
Specialists, team members and the agents of an IMentorAgentSourceNot asked: their roles decide what they are offeredIsAllowedAsync, then BeforeActionAsyncThe usual confirmation box
A task from another agent (A2A)At every turnThe sameRefused, because nobody can confirm
The MCP serverAt every tools/listAt every tools/callRefused, 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.

What a policy is asked about.

PropertyTypeWhat it holds
StageMentorActionStageOffer when MentorAgent decides what to offer, Call when a call is about to run
ToolNamestringThe technical name of the tool, such as close_case
Descriptionstring?The description that the model reads
ArgumentsIReadOnlyDictionary<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
UserClaimsPrincipal?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
SurfaceMentorActionSurfaceWhere the call comes from: InProcess (a Blazor Server circuit or a MAUI app), Hub, Sse, Mcp or A2A
AgentKind, AgentNameMentorAgentKind?, string?The agent that asks: Coordinator, Specialist, TeamMember or AgentSource, with its name when it has one
SessionId, TurnId, CallIdstring?Opaque IDs of the session, of the user turn and of the call. CallId is null at Offer, and TurnId on the MCP server
PageNamestring?The page that the user is on, as it named itself with IMentorPageContext.SetPageName
RequiredRolesIReadOnlyList<string>The roles of the action: any one of them is enough. Empty when it requires none
RequiresConfirmationboolWhether the action asks for a confirmation of its own
ServicesIServiceProviderThe services of the conversation: the circuit, the hub connection, the request or the A2A task. Resolve your scoped services here, such as a DbContext

What BeforeActionAsync answers for one call. Its Kind is a MentorActionVerdictKind, and its Reason is your text.

VerdictEffect
MentorActionVerdict.ProceedNo 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.

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

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

One decision of the gate. It never carries the arguments of the call.

PropertyWhat it holds
OutcomeWhat the gate decided: a MentorActionOutcome
ReasonCodeWhy: one of the MentorActionReasons
ToolNameThe technical name of the tool
DisplayNameThe text shown to the user for the tool
SurfaceInProcess, Hub, Sse, Mcp or A2A
AgentKind, AgentNameThe agent that asked, with its name. null on the MCP server, where the caller names the tool itself
SessionId, TurnId, CallIdOpaque 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
ConfirmationIdThe ID of the confirmation, on the decisions that belong to one
UserWho asked for the action, or null when nobody is signed in
RequiredRolesThe roles of the action
ConfirmedByOn 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
TimestampWhen the gate decided, in UTC
ExceptionTypeOn Failed and PolicyFailed, the full name of the type of the exception, never its message
DetailThe reason that a policy gave, on Blocked, on ConfirmationRequested and on Denied with nobody-to-confirm. null otherwise
OutcomeWhen
AllowedThe roles and every policy let the call through. The confirmation, if any, comes next
DeniedRefused, for example for a missing role, or because a policy does not allow the action
ConfirmationRequestedThe user is asked to confirm
ConfirmedThe user said yes
RejectedThe user said no, or nobody answered within ConfirmationTimeout
StoppedStopped before it finished: Stop, the end of the session or the end of the turn
BlockedA policy stopped the call with Stop. Its reason is in Detail
PolicyFailedA policy threw or did not answer in time, so the call did not run
ExecutedThe action ran
FailedThe action threw

A call goes through the outcomes in one of these orders, the same in Blocking and in Native mode:

  • Allowed, then Executed, Failed or Stopped;
  • Allowed, ConfirmationRequested, then Rejected, Stopped or Confirmed. After Confirmed comes the outcome of the run, or Denied when a role or a permission was taken away while the box was open;
  • Denied, Blocked or PolicyFailed alone.

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.

OutcomeReason codes
Allowedallowed; auto-approved when, in Native mode, a rule of AutoApprovalRules approved the call
Deniedrole-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, Rejectedoperator; policy when a policy asked for the confirmation; confirmation-timeout on Rejected when nobody answered in time
Stoppedstop; session-ended; turn-ended
Executedcompleted; result-withheld when OnToolResult threw
Failedexception
Blockedpolicy
PolicyFailedpolicy-exception; policy-timeout

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.

CaseEscalation.cs
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.";
    }
}
PropertyWhat it holds
ConfirmationIdThe ID of the confirmation, the same as MentorActionDecision.ConfirmationId
ToolNameThe tool that was confirmed
ConfirmedAtWhen the user confirmed, in UTC
ConfirmedByWho confirmed: the caller of POST /mentor/approve, or the signed-in user of the session in a circuit and in a MAUI app

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:

ActionChecks.cs
// 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 propertyWhat it holds
ToolName, DescriptionThe tool, and the description that the model reads
AgentKind, AgentNameCoordinator, Specialist or TeamMember, with the name of the agent
RequiredRolesThe roles of the action
RequiresConfirmationWhether a call asks for a confirmation now, the RequiresApproval predicate included
ReadOnlyWhether the action is declared as only reading, and asks for no confirmation
OnAgentCardWhether the A2A agent card lists it. false when the host does not serve A2A
McpExposureA 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.

TypeWhat it is
MentorActionStartedThe 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
MentorReplyStartedThe argument of the hub event ReplyStarted, sent before each reply of the model: { "aiGenerated": true }. See ShowAiDisclosure
MentorSessionContextPlacementThe values of SessionContextPlacement: Instructions or LastUserMessage
  • Roles, confirmation and governance — Role checks and human-in-the-loop approval for AI agent tools in .NET, enforced in code on every channel and not in the prompt, and what stays in your code.
  • 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.