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"]
}
- The soul becomes that bot's
SOUL.mdat provisioning. It is voice and boundaries — advisory. It states what the role is for and what it must refuse. - The tools are the authority — enforced. Both
tools/listandtools/callread them, so what the model is offered and what it may call are the same set.
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:
- a role with
can_grant_rolesmay grant any existing role exceptbot; - any other role holding
users.*may only create, re-role or delete members; - nobody may grant
botover MCP — it is the machine principal.
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