UgrĂĄs a tartalomhoz
← Back to the journal

Role-based tool set — same chat, different tools

Same chat, four roles, four tool catalogues. The scope guard, the super_admin leak bug, and the fix.

Role-based tool set

The Nortinia AI Assistant is a single chat surface, but the tool catalogue underneath is role-shaped. A viewer does not see the same actions as an admin, and a super_admin does not automatically inherit every tenant-bound tool. This article describes the MCP scope guard and the bug we had to fix along the way.

The four roles

viewer

Reads only. getInvoice, searchCustomer, listOrders, getAuditEvents — but no create, update, delete. A small icon next to the chat input signals that the user is in read-only mode. If a viewer tries to issue a mutation, the assistant politely says the role does not allow it and offers to notify an editor.

editor

Mutations behind preview-and-confirm. Every write tool runs in two steps: the assistant first shows a diff-like summary, and only after explicit user confirmation does the action run. Editors see their own actions in the audit log; other people's actions are hidden.

admin

RBAC actions (role swap, permission grant), full audit trail, tenant config edits, integration management. Above admin level the userManagement.* and audit.full tool families become available. Preview-and-confirm is stricter here: every destructive action (delete, role downgrade) requires a second confirmation with a written reason.

super_admin

Cross-tenant actions: a Netorigo-internal role that can look across customer tenants. This is where tenant.*, billing.crossTenant, featureFlag.global tool families appear. The role logs a double audit trail (tenant + Netorigo-internal), and every cross-tenant tool call fires a separate notify event to the affected tenant admin.

The scope guard

Every MCP tool declares a requiredScopes list, for example:

refund.initiate → ['payments:write', 'role:editor']
user.changeRole → ['users:write', 'role:admin']
tenant.delete → ['tenant:write', 'role:super_admin']

The chat agent pulls the role token from the user's JWT at session start, and the tools/list response only includes tools whose scopes are satisfied by that token. In other words: the viewer never sees refund.initiate listed — they cannot request it, they cannot even see it exists.

The bug we fixed

In May 2026 we noticed the guard was incorrectly accepting a role:* wildcard for super_admin. An early version of the implementation worked like this: if a user had a role:super_admin token, every tool was included in the tools/list response — including tools whose explicit requirement was role:admin and which had not been re-declared with role:super_admin.

In practice this was a week-long leak: a super_admin user could see editor-level tools through chat that were not meant for the role. They could not actually run mutations through them (the guard re-checks at tool-call time), but the catalogue leaked.

The fix is two-sided: tools/list filtering now does strict scope matching (not wildcard-permissive), and the tool-call guard double-checks. The super_admin user must hold an explicit role:admin scope as well to use admin-level tools — a deliberate UX tradeoff in favour of security.

What this means for development

Two things are mandatory when defining a new tool:

  1. Exact scope list. Not role:*, but a concrete role. If several roles can use a tool, list them: ['role:editor', 'role:admin'].
  2. Per-scope unit test. Every new tool ships a tool.spec.ts that exercises every role combination.

This slows development, but in six months we have found one bug (the one above) and even that one within a day of release.

Why not a static tool list per role

We tried. The first version statically encoded viewer_tools.json, editor_tools.json, etc. It was unmaintainable. A new tool meant four file edits. A tool rename meant four file edits. Now the tool catalogue is the source, and scope filtering runs at runtime. One source of truth.

What comes next

The next iteration introduces a granular scope system. Today role:editor enables every editor-level tool. Soon we will be able to constrain by sub-module: role:editor + module:finance permits writes only inside finance, not logistics. This matters for larger customers where finance and warehouse teams share one Netorigo instance.

Let's talk about your project

Tell us what you are building — we will figure out how to help.

Role-based tool set — same chat, different tools — Nortinia Journal | Nortinia