MentorAgent Docs
Menu

ConceptPackages: Novior.MentorAgent, Novior.MentorAgent.Server

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.

On this page

Governi un agente AI come governi la tua applicazione .NET: nel tuo codice. Questa pagina spiega come MentorAgent decide se un'azione può partire: il controllo del ruolo, la conferma dell'utente (quella che altri framework chiamano approvazione dei tool, o human-in-the-loop), le regole del tuo codice, e come tenere traccia di ogni decisione. Le regole stanno nell'attributo del metodo, non nel prompt. Un modello si può convincere a ignorare una regola scritta nel prompt, ma non può saltare un controllo che gira nel tuo codice.

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 legge soltanto, quindi dichiara ReadOnly = true: non chiede mai conferma. CloseCase cambia i dati: può eseguirla solo un utente con il ruolo Supervisors, e solo dopo aver confermato. Un'azione con una regola sul ruolo, una conferma o entrambe si dice protetta (in inglese gated). Il Glossary spiega anche withheld (non offerta) e refused (rifiutata).

  1. Ruolo. RequiredRoles elenca i ruoli che possono eseguire l'azione: ne basta uno. MentorAgent trova l'utente collegato (vedi sotto) e controlla i ruoli con ClaimsPrincipal.IsInRole. Quel metodo legge i claim ClaimTypes.Role, a meno che la tua autenticazione non usi un altro tipo di claim per i ruoli. A un utente senza il ruolo l'azione non viene mai offerta: all'inizio di ogni turno MentorAgent toglie dai tool del modello ogni azione che l'utente non può eseguire, sul coordinatore, sugli specialisti e sui membri dei team. Il modello sa che alcune azioni non sono disponibili, ma non sa quali né con quali ruoli. Se il modello chiama comunque un'azione del genere, per esempio ripetendo un turno precedente, la chiamata viene rifiutata prima di ogni altra cosa: non compare nessun riquadro di conferma, e il modello riceve un errore di autorizzazione.
  2. Conferma. RequiresConfirmation chiede all'utente prima che il metodo parta. Chiede anche una [MentorAction] che non è di sola lettura: vedi Conferma per default.
    • Il widget mostra un riquadro di conferma. Un front end tuo, come un'app React, Angular, Vue o mobile, riceve la richiesta e rimanda la risposta.
    • Se l'utente annulla, il metodo non parte. Il modello viene a sapere che l'utente ha rifiutato, che niente è stato controllato o cambiato, e che non deve dare un altro motivo.
    • Dopo la conferma, MentorAgent controlla di nuovo i ruoli: un utente che ha perso il ruolo mentre il riquadro era aperto viene rifiutato.

Se registri delle action policy, MentorAgent le interpella fra i due controlli: dopo i ruoli, prima della conferma.

Le regole arrivano con la classe. MentorAgent trova la classe negli assembly che elenchi in ScanAssemblies, e la chiama attraverso la dependency injection:

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

Il controllo dei ruoli deve sapere chi è l'utente. MentorAgent lo trova da dove arriva la richiesta:

  • Sull'hub SignalR, su /mentor/chat e su Agent-to-Agent (A2A), l'utente è chi fa la richiesta: l'utente della connessione all'hub, oppure HttpContext.User. MentorAgent lo legge prima di chiedere al tuo AuthenticationStateProvider. Funziona anche in una Blazor Web App, in qualunque ordine di AddRazorComponents() e AddMentorAgentServer(): vedi Blazor WebAssembly and Auto.
  • In un circuito Blazor, cioè la connessione che Blazor Server tiene con ogni scheda del browser, l'utente è quello che indica il tuo AuthenticationStateProvider.
  • Se il tuo AuthenticationStateProvider aggiunge claim o ruoli, MentorAgent non li vede sull'hub, su /mentor/chat né su A2A. Aggiungili con un IClaimsTransformation. ASP.NET Core lo applica a HttpContext.User, e quindi all'utente della connessione all'hub.
  • Se non è collegato nessuno, un'azione con RequiredRoles non viene offerta a nessuno, e viene rifiutata se chiamata. Succede anche in un'app .NET MAUI che non ha un AuthenticationStateProvider.

Una [MentorAction] che non dichiara né ReadOnly = true né RequiresConfirmation conta come un'azione che cambia i dati. Chiede conferma, come una dichiarata 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 non dichiara niente, quindi chiede.
  • IsCaseOpen legge soltanto, quindi non chiede mai. I client Model Context Protocol (MCP) la ricevono con readOnlyHint, un'indicazione che il tool legge soltanto, e alcuni di loro saltano la propria conferma per un tool del genere. Dichiara ReadOnly = true solo sui metodi che leggono davvero e basta.
  • CloseDuplicate parte senza chiedere, perché RequiresConfirmation = false è una tua dichiarazione.

Un'azione che chiede conferma non viene offerta ai client MCP e non compare nell'agent card di A2A (la descrizione che leggono i client A2A), perché lì nessuno può rispondere: vedi Ogni dichiarazione, su ogni canale. Dichiara RequiresConfirmation = false su una scrittura che deve restare disponibile su quei canali.

Questo default vale solo per i metodi [MentorAction]: un metodo con il solo [Description] non chiede niente. La prima volta che MentorAgent legge le tue azioni, scrive nel log un avviso per le scritture che partono senza conferma. MentorAgent le legge:

  • all'avvio, quando l'host mappa l'agente A2A o l'endpoint MCP, oppure accende WarmUpAtStartup;
  • alla prima richiesta del tour introduttivo, la breve visita guidata del widget (GET /mentor/tour);
  • altrimenti, al primo messaggio.

L'avviso elenca i metodi [MentorAction] che dichiarano RequiresConfirmation = false, e i metodi con il solo [Description] che non sono dichiarati di sola lettura con IsReadOnlyAction.

Per spegnere il default, imposta RequireConfirmationForWrites a false. Allora chiede solo RequiresConfirmation = true.

Quello che un metodo dichiara decide se chiede conferma e dove viene offerto. Questa tabella è la regola intera in MentorAgent 1.2.0, con le opzioni di default. Le altre pagine rimandano qui invece di ripeterla.

Il metodo dichiaraChiede prima di partireServer MCPServer A2AFiltro dei tool, con un messaggio che non corrisponde a niente
Solo [Description]MaiPubblicatoNell'agent card, e partePuò essere mandato
Solo [Description], e IsReadOnlyAction restituisce true per il nome del suo toolMaiPubblicato con readOnlyHintNell'agent card, e partePuò essere mandato
[MentorAction], senza ReadOnly né RequiresConfirmationSì, per defaultTrattenutoFuori dalla card, e rifiutato subitoMai mandato
[MentorAction(ReadOnly = true)], oppure una [MentorAction] che non dichiara niente e che IsReadOnlyAction nominaMaiPubblicato con readOnlyHintNell'agent card, e partePuò essere mandato
[MentorAction(RequiresConfirmation = false)]MaiPubblicato, con readOnlyHint se dichiara anche ReadOnly = trueNell'agent card, e partePuò essere mandato
[MentorAction(RequiresConfirmation = true)]SìTrattenutoFuori dalla card, e rifiutato subitoMai mandato
Qualunque metodo il cui nome di tool corrisponde a RequiresApprovalSìTrattenutoFuori dalla card, e rifiutato subitoMai mandato
  • Chiedere. Nel widget l'utente vede un riquadro di conferma. Sull'hub e su Server-Sent Events (SSE) riceve la richiesta il tuo front end, che risponde. Anche un'app MAUI mostra il riquadro.
  • Vince la conferma. Un'azione che chiede conferma non è mai di sola lettura: RequiresConfirmation = true e RequiresApproval prevalgono su ReadOnly = true e su IsReadOnlyAction.
  • I ruoli sono una seconda regola. RequiredRoles aggiunge il controllo dei ruoli a qualunque riga. Il server MCP trattiene un'azione con ruoli, a meno che McpCallerPrincipal riconosca un chiamante che ne ha uno; l'agent card la lascia fuori, e il filtro dei tool non la manda mai con un messaggio che non corrisponde a niente. Dove non risponde nessuno mostra i ruoli su ogni canale.
  • RequireConfirmationForWrites = false spegne il default della terza riga: un'azione del genere parte senza chiedere, come una che dichiara RequiresConfirmation = false.
  • RequireConfirmation = false, l'interruttore globale, spegne ogni conferma: le righe che chiedono partono senza chiedere, anche nei task A2A. Il server MCP, l'agent card e il filtro dei tool leggono comunque la dichiarazione, quindi lasciano fuori le stesse azioni.
  • MentorAgent si fida di una dichiarazione di sola lettura. Non controlla che il metodo legga soltanto. Alcuni client MCP saltano la propria conferma per un tool con readOnlyHint: dichiaralo solo sui metodi che leggono davvero e basta.

Il riquadro di conferma mostra la descrizione dell'azione, poi gli argomenti che il modello sta per passare:

  • Gli argomenti seguono l'ordine dei parametri del metodo. Per un tool MCP, tengono l'ordine in cui li ha scritti il modello.
  • Da ogni valore si tolgono i caratteri di controllo e i caratteri di formattazione invisibili, come quelli che cambiano la direzione del testo. Un valore più lungo di 200 caratteri viene tagliato.
  • Un argomento vuoto non compare.

Per scrivere tu il testo, con parole tue o nella lingua dei tuoi utenti, imposta ConfirmationMessage. Restituisce il testo per una chiamata, oppure null per il testo di default. Il titolo del riquadro non cambia.

Il tuo codice può leggere due proprietà di un attributo [MentorAction], per esempio per elencare le tue scritture. Dicono cosa dichiara l'attributo, non se l'azione chiede conferma:

  • RequiresConfirmation è il valore scritto nell'attributo, e false quando non c'è scritto niente;
  • RequiresConfirmationDeclared è true quando nell'attributo compare RequiresConfirmation, che sia true o false.

Quindi RequiresConfirmation da solo non distingue le due scritture dell'esempio sopra. Con il default per le scritture acceso, le due proprietà valgono:

  • ArchiveCase non dichiara niente: RequiresConfirmation è false e RequiresConfirmationDeclared è false. Chiede conferma, per default.
  • CloseDuplicate dichiara RequiresConfirmation = false: false e true. Non chiede.
  • Un'azione che dichiara RequiresConfirmation = true: true e true. Chiede.
  • IsCaseOpen dichiara solo ReadOnly = true: false e false, come ArchiveCase. Non chiede, perché legge soltanto.

Per default un riquadro di conferma aspetta la risposta dell'utente, uno Stop o la chiusura della sessione. Con ConfirmationTimeout, un riquadro a cui nessuno risponde in tempo conta come rifiutato: l'azione non parte, e il modello viene a sapere che nessuno l'ha confermata in tempo, non che l'utente ha rifiutato. Una risposta che arriva dopo viene ignorata. Il limite vale anche per le azioni degli specialisti e dei membri dei team, e per una conferma chiesta da una policy.

I tool che vengono da un server MCP o da una skill non hanno un attributo su cui mettere le regole. Due opzioni fanno confermare l'utente prima che uno di questi tool parta:

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

Il predicato riceve il nome del tool, non il nome del metodo. Per un'azione è il nome del metodo in snake case, senza un Async finale: CloseCaseAsync è close_case. Per un tool MCP è il nome che gli dà il suo server. Se rinomini un metodo, rinomini anche il suo tool, e un predicato scritto per il vecchio nome smette di corrispondere, senza nessun errore.

Proteggi tutto quello che ha effetti collaterali, che tocca dati sensibili o che non si può annullare.

options.RequireConfirmation è true per default. Accende o spegne in un colpo solo i riquadri di conferma di tutte le fonti: attributi, default per le scritture, server MCP e RequiresApproval. Con false nessuna azione chiede conferma, ma i controlli dei ruoli restano. Le sole approvazioni che compaiono ancora sono quelle dei server MCP che il provider del modello chiama direttamente (HostedMcpServers).

Tre nomi si somigliano:

NomeDove lo impostiCosa fa
RequiresConfirmationSu [MentorAction]Chiede conferma prima che questa azione parta
RequireConfirmationForWritesIn AddMentorAgentFa chiedere conferma per default a una [MentorAction] che non è di sola lettura
RequireConfirmationIn AddMentorAgentAccende o spegne tutti i riquadri di conferma

Un metodo si comporta allo stesso modo sia che lo chiami il coordinatore sia che lo chiami uno specialista o un membro di un team. Specialisti e membri dei team chiamano i loro tool da soli, dentro un workflow di Agent Framework, dove il middleware del coordinatore non vede le chiamate. Per questo MentorAgent avvolge ogni loro tool in un wrapper, con cui anche per loro funzionano queste quattro cose:

  • il controllo dei ruoli e la conferma;
  • l'avanzamento mostrato nel widget;
  • la navigazione dopo l'azione (NavigateTo);
  • le metriche.

I tool nominati da un agente definito in YAML vengono avvolti allo stesso modo.

Gli agenti remoti sono diversi. Sono agenti su altri host che il tuo agente chiama via A2A, elencati in RemoteAgents. I loro tool girano sull'altro host, con le regole di quell'host, quindi nessuno dei tuoi ruoli vale per quello che fanno. Qualunque utente raggiunga il tuo agente può raggiungerli. Per limitare un agente remoto, scegli una di queste strade:

  • proteggi l'endpoint /a2a sull'host remoto e dai i ruoli alle azioni che stanno lì, come in A2A server;
  • lascia l'agente fuori da RemoteAgents sugli host i cui utenti non possono usarlo.

Alcuni canali non hanno una persona dall'altra parte che possa confermare. Lì un'azione che chiede conferma non resta in attesa: non viene offerta a chi chiama (è withheld), oppure viene rifiutata quando la si chiama. La tabella mostra cosa succede su ogni canale:

CanaleAzione con RequiredRolesAzione con RequiresConfirmation
Widget in Blazor Server, WebAssembly o AutoOfferta ed eseguita solo se l'utente collegato ha uno dei ruoli; altrimenti tolta, e rifiutata se chiamataRiquadro di conferma prima che parta
Widget in .NET MAUIRifiutata, a meno che la tua app registri un AuthenticationStateProvider che restituisce l'utente collegatoRiquadro di conferma prima che parta
Hub SignalR e Server-Sent Events (SSE), dal tuo front endControllata sull'utente della richiesta: offerta solo a chi ha il ruolo; le chiamate anonime vengono rifiutateRichiesta al client, che risponde su POST /mentor/approve
Server MCPNon offerta, a meno che McpCallerPrincipal identifichi un chiamante che ha il ruoloMai pubblicata
Server A2ANon è nell'agent card (la descrizione che leggono i client A2A). Il turno del task la offre solo a un chiamante autenticato che ha il ruolo. Per ogni altro chiamante è tolta, e rifiutata se chiamataTolta dall'agent card, e rifiutata subito se il modello la chiama

Canali descrive ogni canale.

Un ruolo dice chi può usare un'azione. Alcune regole dipendono dai tuoi dati: un permesso per azienda o per record, una regola sugli argomenti, lo stato di un record. Scrivile come action policy, una classe che implementa IMentorActionPolicy:

CaseTeamPolicy.cs
public class CaseTeamPolicy : IMentorActionPolicy
{
    // Whether the user may use the action: only the support team closes cases
    public ValueTask<bool> IsAllowedAsync(
        MentorActionRequest request, CancellationToken cancellationToken)
        => ValueTask.FromResult(
            request.ToolName != "close_case"
            || request.User?.HasClaim("team", "support") == true);

    // Before each call: a case that is already closed is not closed again
    public async ValueTask<MentorActionVerdict> BeforeActionAsync(
        MentorActionRequest request, CancellationToken cancellationToken)
    {
        if (request.ToolName != "close_case"
            || request.Arguments?.GetValueOrDefault("caseNumber")?.ToString()
                is not { } caseNumber)
            return MentorActionVerdict.Proceed;

        var store = request.Services.GetRequiredService<ICaseStore>();
        var status = await store.GetStatusAsync(caseNumber);
        return status.Contains("closed", StringComparison.OrdinalIgnoreCase)
            ? MentorActionVerdict.Stop($"Case {caseNumber} is already closed.")
            : MentorActionVerdict.Proceed;
    }
}

Registrala in Program.cs, accanto a un observer:

Program.cs
builder.Services.AddMentorActionPolicy<CaseTeamPolicy>();
builder.Services.AddMentorActionObserver<ActionAuditLog>();
  • IsAllowedAsync decide se l'utente può usare l'azione in generale. MentorAgent la interpella prima di offrire le azioni al modello, a ogni turno. Un'azione che la policy non permette viene tolta, come una per cui l'utente non ha il ruolo. MentorAgent la interpella di nuovo prima di ogni chiamata, e dopo la conferma dell'utente.
  • BeforeActionAsync decide su una chiamata, con i suoi argomenti. Risponde Proceed, Confirm o Stop. Confirm chiede all'utente anche per un'azione che non chiede conferma. Stop impedisce alla chiamata di partire, e il modello legge il tuo motivo. Con più policy vince la risposta più severa.
  • Una policy che fallisce ferma la chiamata. Una policy che lancia un'eccezione, o che non risponde entro ActionPolicyTimeout, 5 secondi per default, ferma la chiamata.

Le policy valgono su ogni canale: il widget, l'hub, /mentor/chat, il server MCP e i task A2A, e le chiamate di specialisti e membri dei team. Dove nessuno può confermare, un Confirm rifiuta la chiamata. Tutti i tipi e le proprietà sono in Action policies and observers.

Filtrare i dati che un metodo restituisce resta compito del tuo metodo, come senza un agente: una policy decide se una chiamata parte, non cosa restituisce.

I ruoli, i riquadri di conferma e le tue policy si possono verificare con test automatici, con un modello finto al posto di quello vero: vedi Test without a model.

Ogni decisione del gate, come consentita, negata, confermata, rifiutata, fermata, eseguita o fallita, arriva agli observer che registri con AddMentorActionObserver<T>(). Ogni decisione porta l'azione, l'utente, chi ha confermato, il canale, la sessione e un codice del motivo che resta uguale fra le versioni. Non porta mai gli argomenti della chiamata. Un observer gira fuori dal percorso dell'azione, quindi non ritarda e non ferma niente. Usalo per un registro di audit: vedi IMentorActionObserver.

Dentro un'azione, MentorActionConfirmation.Current dice chi ha confermato questa chiamata, e quando. Così un registro delle tue scritture può annotare chi ha approvato ognuna: vedi MentorActionConfirmation.

Le tracce portano gli stessi fatti: ogni span mentor.tool.* ha l'esito, il codice del motivo, la conferma e il canale. Vedi EnableObservability.

In un settore regolamentato può servirti mostrare chi ha chiesto un'azione, chi l'ha confermata e quando. MentorAgent dà questi fatti alla tua applicazione quando il gate decide, ma non li conserva: non salva né utenti né orari. Il registro lo costruisce la tua applicazione.

Cosa sa di una conferma ogni fonte:

FonteCosa contieneCosa non fa
Un observer, IMentorActionObserverOgni decisione: l'azione, chi l'ha chiesta, chi ha confermato, quando, il canale, la sessione, il turno e un codice del motivoNon salva niente. MentorAgent consegna ogni decisione senza tenerne copia, e una decisione si può perdere (vedi sotto)
MentorActionConfirmation.Current, dentro la tua azioneChi ha confermato questa chiamata, e quandoEsiste solo per una chiamata che parte. Una conferma rifiutata o fermata non arriva mai al tuo metodo
Tracce: gli span mentor.tool.*L'esito, il codice del motivo, la conferma e il canale di ogni chiamataNon nominano l'utente, e durano quanto le tiene il tuo backend OpenTelemetry
LogAlcuni eventi, come una chiamata rifiutata dal controllo del ruolo o una chiamata al server MCPL'utente compare solo come impronta, caller#…, e il testo dei messaggi di log può cambiare in qualunque versione
OnToolResultIl nome del tool e il suo risultato, prima che lo legga il modelloNon sa chi ha chiesto né chi ha confermato. Serve a togliere dati da un risultato
Gli eventi di IMentorStateService, come OnConfirmationRequired e OnActionStartedLe conferme e le azioni di una sessione, per il componente che mostra la chatEsistono solo dove gira il turno, e solo per quella sessione

Un registro costruito sulle decisioni del gate

Section titled “Un registro costruito sulle decisioni del gate”

Questo observer tiene ogni decisione che appartiene a una conferma, una riga per decisione, in un archivio tuo:

ConfirmationLedger.cs
using System.Security.Claims;
using System.Text.Json;
using Novior.MentorAgent.Abstractions.Interfaces;   // IMentorActionObserver
using Novior.MentorAgent.Abstractions.Models;       // MentorActionDecision
using Novior.MentorAgent.Extensions;                // AddMentorActionObserver
ConfirmationLedger.cs
// One line of the record: a decision that belongs to a confirmation
public sealed record ConfirmationRecord(
    DateTimeOffset At, string Tool, string Outcome, string Reason,
    string? AskedBy, string? ConfirmedBy, Guid ConfirmationId,
    string Surface, string? SessionId, string? TurnId);

public interface IConfirmationStore
{
    Task AppendAsync(
        ConfirmationRecord record, CancellationToken cancellationToken);
}

public sealed class ConfirmationLedger(IConfirmationStore store)
    : IMentorActionObserver
{
    public async ValueTask OnDecisionAsync(
        MentorActionDecision decision, CancellationToken cancellationToken)
    {
        // Asked, confirmed, rejected or stopped, and the run that followed
        if (decision.ConfirmationId is not { } confirmationId)
            return;

        await store.AppendAsync(new ConfirmationRecord(
            decision.Timestamp, decision.ToolName,
            decision.Outcome.ToString(), decision.ReasonCode,
            UserId(decision.User), UserId(decision.ConfirmedBy),
            confirmationId, decision.Surface.ToString(),
            decision.SessionId, decision.TurnId), cancellationToken);
    }

    static string? UserId(ClaimsPrincipal? user) =>
        user?.FindFirst(ClaimTypes.NameIdentifier)?.Value
        ?? user?.Identity?.Name;
}

L'archivio dell'esempio aggiunge a un file una riga JSON per decisione:

JsonLinesConfirmationStore.cs
// Appends one JSON line per record. Decisions can arrive at the same time.
public sealed class JsonLinesConfirmationStore(string path) : IConfirmationStore
{
    readonly SemaphoreSlim _gate = new(1, 1);

    public async Task AppendAsync(
        ConfirmationRecord record, CancellationToken cancellationToken)
    {
        var line = JsonSerializer.Serialize(record) + Environment.NewLine;
        await _gate.WaitAsync(cancellationToken);
        try
        {
            await File.AppendAllTextAsync(path, line, cancellationToken);
        }
        finally
        {
            _gate.Release();
        }
    }
}
Program.cs
builder.Services.AddSingleton<IConfirmationStore>(
    new JsonLinesConfirmationStore(
        builder.Configuration["Audit:Path"] ?? "confirmations.jsonl"));
builder.Services.AddMentorActionObserver<ConfirmationLedger>();
  • Cosa lascia una conferma. Una chiamata confermata lascia ConfirmationRequested, Confirmed e l'esito dell'esecuzione: Executed, Failed, Stopped, oppure Denied quando un ruolo è stato tolto mentre il riquadro era aperto. Una chiamata rifiutata lascia ConfirmationRequested e Rejected. Ogni riga ha lo stesso ConfirmationId, così puoi raggruppare le righe di una conferma.
  • Chi è chi. User è chi ha chiesto l'azione. ConfirmedBy, su Confirmed, è chi ha confermato: chi ha chiamato POST /mentor/approve, oppure l'utente collegato della sessione in un circuito e in un'app MAUI. L'esempio tiene il claim NameIdentifier, oppure il nome.
  • Niente argomenti. Una decisione non porta mai gli argomenti della chiamata, quindi il registro non dice quale pratica è stata rimborsata. Registra gli argomenti nell'azione, come sotto.
  • Il registro può perdere una decisione. Nessuna azione aspetta un observer. Ogni observer ha una coda di 10.000 decisioni, e quando la coda è piena le sue decisioni vengono scartate, con un avviso nel log. Una chiamata che dura più di 5 secondi non viene più attesa. Tieni veloce l'archivio, e tieni d'occhio quell'avviso.
  • Un registro che deve essere completo si scrive nell'azione stessa, nella stessa transazione della modifica, con gli argomenti: MentorActionConfirmation.Current dà chi ha confermato e quando. L'observer aggiunge quello che non arriva mai al tuo metodo: le conferme rifiutate, le chiamate fermate e quelle respinte.
  • Dove tenerlo. Per l'esempio basta un file. In produzione usa un archivio a cui la tua applicazione può solo aggiungere, come una tabella di database senza il diritto di modificare o cancellare, e conservalo per il tempo che chiedono le tue regole.
  • Cosa non contiene. Le azioni dell'interfaccia che una pagina registra, su Path A, partono prima del gate. Un hosted tool gira presso il provider del modello: per un server MCP gestito dal provider che chiede l'approvazione, la conferma è la sua ultima decisione. Sul server MCP, un nome di tool che niente definisce non produce nessuna decisione.

Il limite, in breve. MentorAgent 1.2.0 non conserva chi ha confermato cosa. Dà le decisioni ai tuoi observer e a MentorActionConfirmation.Current. Salvarle, e tenerle complete, spetta alla tua applicazione.

Le prossime tre sezioni parlano di opzioni spente per default: il filtro dei tool, la modalità Native e RequireActionForChangeRequests.

EnableToolFiltering manda al modello solo i tool che corrispondono al messaggio. La corrispondenza è per significato, e può sbagliare: una domanda sui tuoi dati può corrispondere peggio di un saluto. Per questo valgono due regole, entro ToolFilterMaxTools:

  • Quando nessuna azione corrisponde abbastanza, MentorAgent manda comunque le tre azioni più vicine che non chiedono né conferma né ruoli. Un'azione senza conferma può quindi arrivare al modello con qualunque messaggio.
  • Quando tutte le azioni scelte chiedono conferma, MentorAgent manda anche fino a tre delle azioni più vicine che non la chiedono. Un'azione che chiede conferma quindi non viaggia mai da sola.

Il filtro usa quello che dichiari, anche quando l'opzione globale RequireConfirmation è false. L'ultima colonna di Ogni dichiarazione, su ogni canale dice quali azioni possono essere mandate quando un messaggio non corrisponde a niente: una scrittura che dichiara RequiresConfirmation = false sì, e anche un metodo con il solo [Description]. Usa [MentorAction] per ogni metodo che cambia i dati.

Questa sezione ti serve solo se usi i workflow o gli host di Agent Framework, oppure se vuoi AutoApprovalRules. HitlMode sceglie come viaggia una conferma dentro il server. Il nome viene da human in the loop (HITL): una persona conferma un'azione prima che parta. Agent Framework chiama una conferma approval. Il riquadro di conferma, il protocollo del client e le chiamate al modello sono gli stessi nelle due modalità.

Blocking (default)Native
MeccanismoMentorAgent aspetta la risposta dentro il suo codiceIl meccanismo di approvazione di Agent Framework (ApprovalRequiredAIFunction)
Esecuzioni dell'agenteUna sola per tutto il turno, che aspetta la rispostaUna in più dopo ogni giro di conferme, approvate o rifiutate
Perché sceglierlaÈ più sempliceFunziona con i workflow e gli host di Agent Framework

Per il modello una conferma rifiutata è uguale nelle due modalità. Anche Stop è sicuro in entrambe.

  • Con Native, uno Stop premuto mentre un riquadro aspetta lascia aperta quella richiesta. Il messaggio successivo la chiude come non eseguita, nella stessa chiamata al modello della nuova domanda. Poi la conversazione continua.
  • Uno snapshot salvato dopo uno Stop del genere, o dopo un'esecuzione fallita, porta con sé le richieste rimaste aperte e le loro risposte. Il primo turno dopo il ripristino dello snapshot le chiude.
  • Se fallisce l'esecuzione che portava la risposta dell'utente, MentorAgent segna come sconosciuto l'esito dell'azione e non la esegue mai una seconda volta.

In modalità Native, AutoApprovalRules può approvare una chiamata prima che compaia un riquadro. Le regole girano in ordine, e la prima che restituisce true approva la chiamata:

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)),
];

Una regola accende anche il "non chiedere più" di Agent Framework: dopo che l'utente ha approvato una chiamata, le chiamate che corrispondono smettono di chiedere per il resto della sessione. Una regola può saltare la domanda di conferma, ma non può mai saltare il controllo dei ruoli. MentorAgent controlla RequiredRoles nel suo wrapper dei tool, e lì le regole di approvazione non girano. Una chiamata che RequireActionForChangeRequests rende obbligatoria non viene mai approvata da una regola: il riquadro compare sempre. Le chiamate che il modello sceglie di fare da solo si possono invece approvare con una regola.

I tool di specialisti e membri dei team usano sempre il flusso bloccante, anche con HitlMode = Native. Quindi AutoApprovalRules non li approva mai, e il "non chiedere più" non se li ricorda. Se una regola deve approvare un tool senza mostrare un riquadro, fanne un'azione del coordinatore, non un tool di uno specialista o di un membro di un team.

Quando il modello dichiara una modifica che non ha fatto

Section titled “Quando il modello dichiara una modifica che non ha fatto”

I controlli di questa pagina scattano quando il modello chiama un'azione. Un modello però può anche rispondere "Magazzino aggiornato" senza chiamare niente. Allora non compare nessun riquadro, e non cambia niente.

RequireActionForChangeRequests chiude questo buco, ed è spenta per default. Un modello classificatore legge ogni messaggio. Prendi un messaggio che chiede una delle tue azioni con conferma, che l'utente può eseguire, e che dà tutti i dati che servono all'azione. Allora la prima chiamata al modello deve chiamare quell'azione, e il riquadro arriva prima di qualunque frase. Una richiesta a cui manca un dato non viene forzata, e l'agente può chiederlo.

L'opzione richiede EnableToolFiltering e un EmbeddingGenerator, e aggiunge una piccola chiamata al classificatore alla maggior parte dei turni. Senza l'opzione, un modello può ancora descrivere un'azione che non ha mai eseguito.

L'articolo 50 dell'AI Act europeo chiede che le persone sappiano quando parlano con un sistema di AI, e che i contenuti generati dall'AI siano marcati in modo leggibile da una macchina. MentorAgent ti dà due strumenti. Se l'articolo vale per la tua applicazione, e quali parole mostrare, lo decidi tu.

  • Un avviso. Il widget mostra sotto la sua intestazione "You are talking to an AI assistant: its replies are generated automatically.", nella lingua di Language, per tutta la conversazione. Scrivi il tuo testo con AiDisclosureMessage, oppure spegni l'avviso con ShowAiDisclosure.
  • Una marcatura su ogni risposta del modello. data-ai-generated="true" sulla risposta nel widget, "aiGenerated": true sui frame di /mentor/chat, l'evento dell'hub ReplyStarted prima di ogni risposta, e aiGenerated nei metadati di una risposta A2A. La marcatura resta anche quando l'avviso è spento. Un front end tuo lo legge per mostrare il suo avviso: vedi Any front end.

I risultati del server MCP, le risposte vocali e le immagini e i file degli hosted tool non portano la marcatura.

OWASP chiama il rischio dietro questi controlli LLM06:2025 Excessive Agency (external site): azioni dannose che un agente compie perché l'output del modello era inatteso, ambiguo o manipolato. Due delle mitigazioni che propone sono i due controlli di questa pagina: eseguire le azioni nel contesto dell'utente, con i suoi ruoli, e chiedere all'utente di approvare un'azione prima che parta. Un'altra mitigazione, il controllo di ogni accesso a un singolo record (complete mediation), si fa nel tuo codice, nei tuoi metodi o in una action policy: vedi Cosa resta nel tuo codice.

  • Attributes — The parameters of MentorAction, MentorAgent, MentorTeam, TeamMember, MentorPage and MentorSkill, with their defaults and rules.
  • 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.
  • 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.