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.
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).
Due controlli, in quest'ordine
Section titled “Due controlli, in quest'ordine”- Ruolo.
RequiredRoleselenca i ruoli che possono eseguire l'azione: ne basta uno. MentorAgent trova l'utente collegato (vedi sotto) e controlla i ruoli conClaimsPrincipal.IsInRole. Quel metodo legge i claimClaimTypes.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. - Conferma.
RequiresConfirmationchiede 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:
builder.Services.AddScoped<CaseService>();
builder.Services.AddMentorAgent(options =>
{
options.AppName = "Case management";
options.ChatClient = chatClient;
options.ScanAssemblies = [typeof(CaseService).Assembly];
});Come MentorAgent trova l'utente
Section titled “Come MentorAgent trova l'utente”Il controllo dei ruoli deve sapere chi è l'utente. MentorAgent lo trova da dove arriva la richiesta:
- Sull'hub SignalR, su
/mentor/chate su Agent-to-Agent (A2A), l'utente è chi fa la richiesta: l'utente della connessione all'hub, oppureHttpContext.User. MentorAgent lo legge prima di chiedere al tuoAuthenticationStateProvider. Funziona anche in una Blazor Web App, in qualunque ordine diAddRazorComponents()eAddMentorAgentServer(): 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
AuthenticationStateProvideraggiunge claim o ruoli, MentorAgent non li vede sull'hub, su/mentor/chatné su A2A. Aggiungili con unIClaimsTransformation. ASP.NET Core lo applica aHttpContext.User, e quindi all'utente della connessione all'hub. - Se non è collegato nessuno, un'azione con
RequiredRolesnon viene offerta a nessuno, e viene rifiutata se chiamata. Succede anche in un'app .NET MAUI che non ha unAuthenticationStateProvider.
Conferma per default
Section titled “Conferma per default”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:
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");
}ArchiveCasenon dichiara niente, quindi chiede.IsCaseOpenlegge soltanto, quindi non chiede mai. I client Model Context Protocol (MCP) la ricevono conreadOnlyHint, un'indicazione che il tool legge soltanto, e alcuni di loro saltano la propria conferma per un tool del genere. DichiaraReadOnly = truesolo sui metodi che leggono davvero e basta.CloseDuplicateparte 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.
Ogni dichiarazione, su ogni canale
Section titled “Ogni dichiarazione, su ogni canale”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 dichiara | Chiede prima di partire | Server MCP | Server A2A | Filtro dei tool, con un messaggio che non corrisponde a niente |
|---|---|---|---|---|
Solo [Description] | Mai | Pubblicato | Nell'agent card, e parte | Può essere mandato |
Solo [Description], e IsReadOnlyAction restituisce true per il nome del suo tool | Mai | Pubblicato con readOnlyHint | Nell'agent card, e parte | Può essere mandato |
[MentorAction], senza ReadOnly né RequiresConfirmation | Sì, per default | Trattenuto | Fuori dalla card, e rifiutato subito | Mai mandato |
[MentorAction(ReadOnly = true)], oppure una [MentorAction] che non dichiara niente e che IsReadOnlyAction nomina | Mai | Pubblicato con readOnlyHint | Nell'agent card, e parte | Può essere mandato |
[MentorAction(RequiresConfirmation = false)] | Mai | Pubblicato, con readOnlyHint se dichiara anche ReadOnly = true | Nell'agent card, e parte | Può essere mandato |
[MentorAction(RequiresConfirmation = true)] | Sì | Trattenuto | Fuori dalla card, e rifiutato subito | Mai mandato |
Qualunque metodo il cui nome di tool corrisponde a RequiresApproval | Sì | Trattenuto | Fuori dalla card, e rifiutato subito | Mai 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 = trueeRequiresApprovalprevalgono suReadOnly = truee suIsReadOnlyAction. - I ruoli sono una seconda regola.
RequiredRolesaggiunge il controllo dei ruoli a qualunque riga. Il server MCP trattiene un'azione con ruoli, a meno cheMcpCallerPrincipalriconosca 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 = falsespegne il default della terza riga: un'azione del genere parte senza chiedere, come una che dichiaraRequiresConfirmation = 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.
Cosa dice il riquadro
Section titled “Cosa dice il riquadro”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, efalsequando non c'è scritto niente;RequiresConfirmationDeclaredètruequando nell'attributo compareRequiresConfirmation, che siatrueofalse.
Quindi RequiresConfirmation da solo non distingue le due scritture dell'esempio sopra. Con il default per le scritture acceso, le due proprietà valgono:
ArchiveCasenon dichiara niente:RequiresConfirmationèfalseeRequiresConfirmationDeclaredèfalse. Chiede conferma, per default.CloseDuplicatedichiaraRequiresConfirmation = false:falseetrue. Non chiede.- Un'azione che dichiara
RequiresConfirmation = true:trueetrue. Chiede. IsCaseOpendichiara soloReadOnly = true:falseefalse, comeArchiveCase. Non chiede, perché legge soltanto.
Una conferma a cui nessuno risponde
Section titled “Una conferma a cui nessuno risponde”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.
Tool su cui non puoi mettere attributi
Section titled “Tool su cui non puoi mettere attributi”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:
RequiresConfirmationsu unMentorMcpServerprotegge tutti i tool di quel server;- il predicato
RequiresApprovalprotegge i tool per nome, che siano azioni, tool MCP o skill.
// 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.
L'interruttore globale
Section titled “L'interruttore globale”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:
| Nome | Dove lo imposti | Cosa fa |
|---|---|---|
RequiresConfirmation | Su [MentorAction] | Chiede conferma prima che questa azione parta |
RequireConfirmationForWrites | In AddMentorAgent | Fa chiedere conferma per default a una [MentorAction] che non è di sola lettura |
RequireConfirmation | In AddMentorAgent | Accende o spegne tutti i riquadri di conferma |
Anche specialisti e team passano dal gate
Section titled “Anche specialisti e team passano dal gate”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
/a2asull'host remoto e dai i ruoli alle azioni che stanno lì, come in A2A server; - lascia l'agente fuori da
RemoteAgentssugli host i cui utenti non possono usarlo.
Dove non risponde nessuno
Section titled “Dove non risponde nessuno”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:
| Canale | Azione con RequiredRoles | Azione con RequiresConfirmation |
|---|---|---|
| Widget in Blazor Server, WebAssembly o Auto | Offerta ed eseguita solo se l'utente collegato ha uno dei ruoli; altrimenti tolta, e rifiutata se chiamata | Riquadro di conferma prima che parta |
| Widget in .NET MAUI | Rifiutata, a meno che la tua app registri un AuthenticationStateProvider che restituisce l'utente collegato | Riquadro di conferma prima che parta |
| Hub SignalR e Server-Sent Events (SSE), dal tuo front end | Controllata sull'utente della richiesta: offerta solo a chi ha il ruolo; le chiamate anonime vengono rifiutate | Richiesta al client, che risponde su POST /mentor/approve |
| Server MCP | Non offerta, a meno che McpCallerPrincipal identifichi un chiamante che ha il ruolo | Mai pubblicata |
| Server A2A | Non è 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 chiamata | Tolta dall'agent card, e rifiutata subito se il modello la chiama |
Canali descrive ogni canale.
Le tue regole: le action policy
Section titled “Le tue regole: le action policy”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:
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:
builder.Services.AddMentorActionPolicy<CaseTeamPolicy>();
builder.Services.AddMentorActionObserver<ActionAuditLog>();IsAllowedAsyncdecide 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.BeforeActionAsyncdecide su una chiamata, con i suoi argomenti. RispondeProceed,ConfirmoStop.Confirmchiede all'utente anche per un'azione che non chiede conferma.Stopimpedisce 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.
Cosa resta nel tuo codice
Section titled “Cosa resta nel tuo codice”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 registrata
Section titled “Ogni decisione registrata”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.
Audit: chi ha confermato cosa
Section titled “Audit: chi ha confermato cosa”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:
| Fonte | Cosa contiene | Cosa non fa |
|---|---|---|
Un observer, IMentorActionObserver | Ogni decisione: l'azione, chi l'ha chiesta, chi ha confermato, quando, il canale, la sessione, il turno e un codice del motivo | Non salva niente. MentorAgent consegna ogni decisione senza tenerne copia, e una decisione si può perdere (vedi sotto) |
MentorActionConfirmation.Current, dentro la tua azione | Chi ha confermato questa chiamata, e quando | Esiste 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 chiamata | Non nominano l'utente, e durano quanto le tiene il tuo backend OpenTelemetry |
| Log | Alcuni eventi, come una chiamata rifiutata dal controllo del ruolo o una chiamata al server MCP | L'utente compare solo come impronta, caller#…, e il testo dei messaggi di log può cambiare in qualunque versione |
OnToolResult | Il nome del tool e il suo risultato, prima che lo legga il modello | Non sa chi ha chiesto né chi ha confermato. Serve a togliere dati da un risultato |
Gli eventi di IMentorStateService, come OnConfirmationRequired e OnActionStarted | Le conferme e le azioni di una sessione, per il componente che mostra la chat | Esistono 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:
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// 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:
// 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();
}
}
}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,Confirmede l'esito dell'esecuzione:Executed,Failed,Stopped, oppureDeniedquando un ruolo è stato tolto mentre il riquadro era aperto. Una chiamata rifiutata lasciaConfirmationRequestedeRejected. Ogni riga ha lo stessoConfirmationId, così puoi raggruppare le righe di una conferma. - Chi è chi.
Userè chi ha chiesto l'azione.ConfirmedBy, suConfirmed, è chi ha confermato: chi ha chiamatoPOST /mentor/approve, oppure l'utente collegato della sessione in un circuito e in un'app MAUI. L'esempio tiene il claimNameIdentifier, 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.Currentdà 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.
Filtro dei tool e conferma
Section titled “Filtro dei tool e conferma”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.
Le modalità Blocking e Native
Section titled “Le modalità Blocking e Native”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 | |
|---|---|---|
| Meccanismo | MentorAgent aspetta la risposta dentro il suo codice | Il meccanismo di approvazione di Agent Framework (ApprovalRequiredAIFunction) |
| Esecuzioni dell'agente | Una sola per tutto il turno, che aspetta la risposta | Una in più dopo ogni giro di conferme, approvate o rifiutate |
| Perché sceglierla | È più semplice | Funziona 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:
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.
Dire alle persone che parlano con un'AI
Section titled “Dire alle persone che parlano con un'AI”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 conAiDisclosureMessage, oppure spegni l'avviso conShowAiDisclosure. - Una marcatura su ogni risposta del modello.
data-ai-generated="true"sulla risposta nel widget,"aiGenerated": truesui frame di/mentor/chat, l'evento dell'hubReplyStartedprima di ogni risposta, eaiGeneratednei 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.
Excessive agency
Section titled “Excessive agency”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.
Vedi anche
Sezione “Vedi anche”- 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.
