What the token identifies
Not a person. The bearer is one long-lived token for the whole box, so every call arrives as the same principal. Consequences, stated plainly:
- The server cannot tell whether a call came from hazemarian's turn or totti786's.
- Therefore it cannot scope by user, and the
chats.*tools — which would have leaked across users — were removed rather than scoped by a rule it cannot enforce. - Audit lines read
actor: mcp:bot, which is true but useless: it records that the machine acted, not who asked.
The identity block prepended to each turn (Caller: <login> (role <r>, user id <n>)) improves
the agent's answers and is not a control. A prompt can be argued with; a token cannot.
The member-only rule
Because there is no human present to approve anything, dangerous writes are refused by construction:
| Action | member |
admin / master / bot |
|---|---|---|
users.create |
allowed, role must be member |
refused — asking for a privileged role fails, it is never silently downgraded |
users.set_role |
allowed (to member) |
refused if the target is privileged or the requested role is privileged |
users.delete |
allowed | refused — masters and admins are removed by hand |
The refusal-not-downgrade rule exists because the first implementation accepted
role: master and created a member. A caller that asks for an administrator and receives a
member believes it created an administrator; that is worse than an error.
Audit
Every mutation writes one line to logs.jsonl:
mcp users.create <login> role=member actor: mcp:bot
Where this is going
One token per bot, and the token resolves to a user — see
one bot per user. At that point the principal is a person, the
role gate becomes per-user instead of a fixed rule, and the audit line becomes
actor: user:<login>.
Rendered from nx-master/mcp/auth.md · OKF bundle · regenerate with npm run render