MentorAgent Docs
Menu

TutorialTime to complete: 15 minPackage: Novior.MentorAgent

2 · Require roles and confirmation

Tutorial step 2: restrict actions to roles and ask the user to confirm before the agent changes data, then try both cases.

View as Markdown

In this tutorial, you'll:

  • Restrict the actions that change data with RequiredRoles
  • Keep a confirmation on every change with RequiresConfirmation
  • Sign in as a user with the role and as one without, and compare
  • Tell what the model receives when a call is refused
On this page

Right now anyone who reaches the widget can add a note. In this step you decide who may change a case. The app gets two roles for its support staff: Agents may add notes, and Supervisors may also close cases. Here Agents is a role of people, not an AI agent. Every change still asks for confirmation.

Before you start: finish 1 · Turn service methods into actions.

Replace the content of CaseService.cs. The reads stay as they are; AddNote gets roles, and a new action closes a case:

CaseService.cs
using System.ComponentModel;
using Novior.MentorAgent.Attributes;

public class CaseService(CaseStore store)
{
    [MentorAction(
        Description = "Gets a support case by its number, with its status, priority and notes",
        ReadOnly = true)]
    public SupportCase? GetCase(
        [Description("The case number, for example C-1")] string caseNumber)
        => store.Find(caseNumber);

    [MentorAction(
        Description = "Lists support cases, optionally only those with a given status",
        ReadOnly = true)]
    public IReadOnlyList<SupportCase> ListCases(
        [Description("Open, Waiting for customer or Closed. Leave empty for every case")] string? status = null)
        => store.List(status);

    [MentorAction(
        Description = "Adds a note to a support case",
        RequiresConfirmation = true,
        RequiredRoles = ["Agents", "Supervisors"])]
    public string AddNote(string caseNumber, string text)
    {
        store.AddNote(caseNumber, text);
        return $"Note added to case {caseNumber}.";
    }

    [MentorAction(
        Description = "Closes a support case with a resolution",
        RequiresConfirmation = true,
        RequiredRoles = ["Supervisors"])]
    public string CloseCase(string caseNumber, string resolution)
    {
        store.Close(caseNumber, resolution);
        return $"Case {caseNumber} closed.";
    }
}
  • RequiredRoles lists the roles that may run the action. Holding any one of them is enough: AddNote is for agents and supervisors, CloseCase only for supervisors.
  • RequiresConfirmation = true keeps the confirmation box on both. The role decides who may act. The confirmation asks the user, every time, whether to act. A [MentorAction] that is not read-only asks even without it. Writing RequiresConfirmation = true puts the rule in your code.
  • CloseCase becomes the tool close_case.
  • The reads have no rules, so anyone who reaches the widget can call them. Protect reads the same way when the data is not for everyone.

MentorAgent checks roles with ClaimsPrincipal.IsInRole (the ClaimTypes.Role claims, by default) on the signed-in user. It gets that user from the AuthenticationStateProvider that Blazor already has. The Cases app has no sign-in yet. In a real app, users sign in with ASP.NET Core Identity, Microsoft Entra ID or another scheme. For this tutorial, a cookie and a test endpoint are enough.

  1. At the top of Program.cs, add the using directives:

    Program.cs
    using System.Security.Claims;
    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Authentication.Cookies;
  2. Before builder.Build(), register cookie authentication:

    Program.cs
    builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddCookie();
  3. After var app = builder.Build();, add the test endpoints:

    Program.cs
    if (app.Environment.IsDevelopment())
    {
        // For this tutorial only: sign in as a test user, with or without a role
        app.MapGet("/dev/sign-in", async (HttpContext http, string name, string? role) =>
        {
            List<Claim> claims = [new(ClaimTypes.Name, name)];
            if (!string.IsNullOrEmpty(role))
                claims.Add(new(ClaimTypes.Role, role));
    
            await http.SignInAsync(new ClaimsPrincipal(
                new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme)));
            return Results.Redirect("/");
        });
    
        app.MapGet("/dev/sign-out", async (HttpContext http) =>
        {
            await http.SignOutAsync();
            return Results.Redirect("/");
        });
    }
  • /dev/sign-in?name=Alex&role=Agents signs you in as Alex, with the role Agents. Leave out role to sign in with no role, and open /dev/sign-out to sign out.
  • ASP.NET Core adds the authentication middleware by itself when authentication is registered, so the rest of the pipeline stays as it is.
  • Each endpoint redirects to the home page. The page loads again, and the widget works for the new user.

Run the app and try the same requests as three different users. To sign in as a user, add a path such as /dev/sign-in?name=Alex&role=Agents to the address of the app in the browser.

  1. Type "Close case C-2, it is a duplicate of C-1". No confirmation box appears. The agent tells you that you need to sign in, and the case stays open.
  1. Open /dev/sign-in?name=Alex&role=Agents.
  2. Type "Add a note to C-2: same issue as C-1". The confirmation box appears. Click Confirm, and the note is added.
  3. Type "Close case C-2, it is a duplicate of C-1". No box appears: the agent tells you that closing a case needs the role Supervisors.
  1. Open /dev/sign-in?name=Sam&role=Supervisors.
  2. Ask again to close C-2. The confirmation box shows the case number and the resolution.
  3. Click Cancel. The method does not run, and the agent tells you in one sentence that it did not proceed.
  4. Ask once more and click Confirm. Case C-2 is closed.
  • The same tools for everyone. close_case is in the tool list even when Alex is signed in. The check runs in your app when the tool is called, not in the model.
  • Who is asking. For a signed-in user, the model also receives the name and the roles. For a user who is not signed in, it receives nothing about the user.
  • A refusal instead of a result. When the role is missing, the method does not run and no box appears. The model receives an access-denied error instead. The error names the required roles, or says that the user must sign in, and the model passes it on to the user.
  • A cancellation. When the user clicks Cancel, the method does not run. The model is told that the user declined and that nothing was checked or changed. It is asked not to try again, not to give another reason, and not to describe the state of any record.

The order never changes: first the role, then the confirmation, then your method. MentorAgent reads the roles once more after the user confirms, so a user who lost the role while the box was open is refused.

The coordinator, the main agent that answers you, does everything itself. In 3 · Hand off to a specialist agent you move the case actions to a specialist, and the rules move with them.

Two checks, in this order explains both checks, and Where nobody can answer what changes on the other channels.

  • Restrict the actions that change data with RequiredRoles
  • Keep a confirmation on every change with RequiresConfirmation
  • Sign in as a user with the role and as one without, and compare
  • Tell what the model receives when a call is refused
  • Add MentorAgent to an existing app — Add MentorAgent to a Blazor Web App you already have: install the package, register the services, mark your methods and place the widget.
  • Attributes — The parameters of MentorAction, MentorAgent, MentorTeam, TeamMember, MentorPage and MentorSkill, with their defaults and rules.