type
Design
title
Roles are data — a soul plus a tool set
description
A master defines a role by writing its soul and naming the exact MCP tools it may use; visibility and enforcement both read that row.
tags
roles, auth, mcp, souls
status
stable
generated
crm-bot/deepseek-v4.1-flash · Fri Oct 30 2026 16:20:00 GMT+0100 (Central European Standard Time)
verified
process:live-probe · Fri Oct 30 2026 16:20:00 GMT+0100 (Central European Standard Time)

The idea

A role is a row, not a constant in Go:

roles(name, title, soul, tools, builtin, can_grant_roles, created_by, created_at, updated_at)

tools is the whole authority. There is no second place where access is decided, which is the point: two authorities that can disagree is how a system ends up offering a tool it will refuse, or refusing one it promised.

Defining a role

roles.create — only for a role with can_grant_roles (the seeded master):

{
  "name": "support",
  "title": "support — read-only triage",
  "soul": "You triage customers. Read only. Never write. If asked to change data, say that needs a member or master.",
  "tools": ["customers.list", "logs.query", "telemetry.stats"]
}

Why a typo cannot pass

An unknown tool name would produce a role that looks fine and can do nothing. So the tool list is validated against the registry at creation time, and the refusal names the bad entry and returns the valid list:

unknown tool(s) [customers.listt]; valid tools are [telemetry.stats logs.query …]

roles.list returns availableTools for the same reason — a master defining a role should not have to guess.

Built-in roles

Seeded as ordinary rows with INSERT OR IGNORE, so an operator edit survives a restart:

Role Tools Can grant roles
master all 13 yes
admin 11 (data + user admin + roles.list) no
member 6 (data only) no
bot all 13, but the within-tool rule limits it to members no

Built-ins cannot be deleted, and neither can any role still assigned to a user — that would leave a user pointing at nothing.

The within-tool rules are data too

Holding a tool is not the same as being allowed everything it can do:

Fail closed, and say so

An unknown role resolves to no tools, never to all of them. A call naming a tool the role does not carry is refused with tool "<name>" is not available to role "<role>", and that refusal is information rather than an obstacle to route around.

Verified

Live, on the box: four built-ins seeded; support created with a soul and three tools; a user with that role saw exactly three tools and was refused customers.create; an admin could list roles but not create them, and was not even offered roles.create; a misspelled tool was refused with the registry; member could not be deleted.

Rendered from nx-master/roles.md · OKF bundle · regenerate with npm run render