Capabilities¶
Lookup reference for the capability ACL: the capability id grammar, the closed capability list, full/own/none grading, the seeded role bundles, and a summary of which routes demand which capability. The identity model behind it — tokens, Margonem-ID binding, identity linking, governance data — is owned by Roles & permissions; the authoritative per-route table is API reference §7.
Id grammar¶
A capability is <resource>.<action> with an optional .own ownership suffix:
<resource>.allgrants every action on the resource.admin.allgrants everything — it is the machine token's implicit set and the Namiestnik/Koordynator bundle.X.ownnever silently satisfiesX(see Grading).
Capability list¶
| Capability | Grants |
|---|---|
entity.read |
Read any entity, name resolution, location/map reads, job polling — the broad shared-lore read |
entity.write |
Create/modify/soft-delete entities, maps, and traversal links |
player.read |
Read Gracz records and their Postacie (/people, /characters reads) |
player.write / player.write.own |
Gracz/Postać CRUD on the dedicated /people + /characters routes (.own = the caller's own subjects) |
player.declare.own |
Reserved for the bot's /deklaracja self-service declaration; seeded in the Gracz bundle, no route consumes it yet |
session.read |
Session reads, integrity, participation graph, narrator profile |
session.write |
Author/edit/open/close sessions, rebuild the graph, refresh the hash store |
session.distribute |
Run entity-driven session distribution |
pu.award |
Run the monthly PU batch (POST /workflows/award-pu) |
pu.read / pu.read.own |
PU timeline/history/new-character preview and election eligibility (.own scopes the timeline, new-character preview, and eligibility to own Postacie / own Gracz; history is served unscoped) |
workflow.settle |
Run the settlement workflow (POST /workflows/settle) — see Settlement model |
workflow.map_checkup |
Run the map-checkup sweep (POST /workflows/map-checkup) |
currency.read / currency.read.own |
Holdings, reports, denominations (.own filters to own holdings; reconciliation needs full) |
currency.write |
Create/adjust holdings; apply session @Transfer directives |
economy.read |
Economy snapshot, timeline, materialization |
governance.read |
Read the narrator Uprawnienia table |
governance.write |
Edit narrator Uprawnienia rows |
location.override |
Reindex the repository after hand edits to nerthus.entities.md — Council-level (see Locations model) |
identity.read |
GET /auth/whoami, GET /identity/{id} |
identity.link |
Link a Discord account ⇄ Margonem ID |
capability.read |
GET /capabilities (the caller's own effective set) |
capability.admin |
Grant/revoke per-Person capabilities |
token.manage |
Mint/list/revoke named tokens |
log.read |
Log streams, audit reads, entity history, Discord delivery log, log parsing |
log.fetch |
Game-log fetch and per-session refresh |
discord.send |
Deliver a webhook message |
events.subscribe |
Open the GET /events SSE stream (Bearer in the header) |
sync.read |
Read the daemon's repo-sync state (GET /sync) |
sync.run |
Run a repo-sync tick immediately (POST /sync) |
admin.mode / admin.shutdown / admin.index / admin.migrate |
Control-plane: mode flip, shutdown, name-index rebuild, repository import (POST /import) |
admin.all |
Everything (machine token, Namiestnik/Koordynator) |
No seeded role bundle carries workflow.settle, workflow.map_checkup,
sync.read, or sync.run — settlement spans distribution, currency, and PU at
once, and sync is repo-level, so these resolve only through admin.all or a
per-Person grant. .own grading is meaningless for repo-level sync — the
handlers treat an own-only sync grant as no grant and refuse with 403. The
daemon's own scheduled sync tick runs in-process and needs no token — design in
Sync.
The machine token implicitly satisfies every capability (admin.all); it exists
to authenticate local processes, never to sandbox the operator's shell.
Capabilities scope end users reaching the daemon through the Discord/Margonem
bot — token mechanics in
Roles & permissions.
Grading (full / own / none)¶
When the dispatcher checks a route's required capability, the caller's grant is graded:
| Grade | Condition | Effect |
|---|---|---|
| full | Holds the plain capability, a matching .all, or admin.all |
Unrestricted handler run |
| own | Holds only the .own variant |
Request marked OwnOnly — the handler scopes it to the caller's own subjects or refuses |
| none | Neither | 403 naming the missing capability |
X.own never silently satisfies X. The caller's own subjects are their
Gracz entity (by name or Margonem ID) and every Postać whose @należy_do is that
Gracz.
Handler scoping for an OwnOnly caller, route by route:
| Route (OwnOnly caller) | Scoping |
|---|---|
GET /pu/timeline |
Filtered to own Postacie; an explicit ?character= must be an own subject (403 otherwise) |
GET /pu/new-character-count |
player is forced to the caller's own Gracz |
GET /pu/history |
Served unscoped — each row carries only a batch date and a counted-session count, facts the committed nerthus.ledger.md echo already publishes to every repo clone |
GET /elections/eligibility |
gracz is forced to the caller's own Gracz (the /wybory self-check) |
GET /currency, GET /currency/report |
Filtered to holdings owned by the caller's Gracz/Postacie |
GET /currency/{name} |
403 unless the holding is the caller's own |
GET /currency/reconciliation |
Always 403 — reconciliation is a repo-wide integrity report |
PATCH/DELETE /people/{name}, /characters/{name} |
403 unless the subject is the caller's own |
POST /people |
Always 403 — a new Gracz has no ownership to scope to |
POST /characters |
@należy_do must equal the caller's own Gracz |
So a Gracz holding pu.read.own reads their PU timeline but not another
player's, and player.write.own lets them edit their own Postać but not create a
second Gracz.
Role bundles¶
Roles are named bundles of capabilities — data, not hardcoded tiers. They load
from committed .nerthus/data-tables/roles.json; the daemon seeds that file only
when it is absent, so a Council edit survives every boot and re-init (file
location and seed rule: Data tables,
Configuration). The seeded defaults are also the fallback
when the file is missing:
| Role | Bundle (as seeded) |
|---|---|
| Gracz | entity.read, player.read, player.write.own, session.read, pu.read.own, currency.read.own, player.declare.own, events.subscribe, identity.read, capability.read |
| Narrator | entity.read, player.read, session.read, session.write, pu.award, pu.read, governance.read, currency.read, log.read, log.fetch, discord.send, events.subscribe |
| Radny | entity.read, entity.write, player.read, player.write, session.read, session.write, pu.award, pu.read, governance.read, governance.write, token.manage, capability.admin, identity.link, log.read, currency.read, currency.write, location.override, discord.send, events.subscribe |
| Namiestnik / Koordynator | admin.all |
Namiestnik and Koordynator are two distinct roles with identical seeded bundles —
roles.json keys them separately, so the Council can diverge them later. The
bundles are independent lists, not strict supersets: the narrator bundle
carries full pu.read/currency.read instead of the .own variants and omits
identity.read; the radny bundle omits log.fetch.
A Person's effective set = union of their role bundles, plus per-Person grants,
minus per-Person revokes (Grant-NerthusCapability /
Revoke-NerthusCapability). Session tokens store only the role; capabilities are
recomputed on every request, so a grant or revoke takes effect on live tokens
immediately. Every externally-minted session token carries the gracz role — a
Narrator's or Radny's extra powers attach to their Margonem ID as per-Person
grants, or ride a named token minted with -Roles narrator.
Routes → capabilities (summary)¶
The full closed table — every route with its exact capability and Write flag — is API reference §7. The shape of the mapping:
| Route area | Read | Write / act |
|---|---|---|
/entities, /resolve, /locations, /maps, /jobs |
entity.read |
entity.write |
/locations/reindex |
— | location.override |
/people, /characters |
player.read |
player.write (.own graded) |
/sessions, /workflows/open-session, /workflows/close-session |
session.read |
session.write |
/workflows/distribute-session |
— | session.distribute |
/pu/*, /elections/eligibility |
pu.read (.own graded) |
pu.award (the monthly batch) |
/currency/*, /workflows/apply-transfers, /audit/ledger |
currency.read (.own graded) |
currency.write |
/economy/* |
economy.read |
— |
/workflows/settle |
— | workflow.settle |
/workflows/map-checkup |
— | workflow.map_checkup |
/logs/{stream}, /logs/parse, /audit/changes, /audit/notifications, /entities/{name}/history, /discord/deliveries |
log.read |
— |
/logs/fetch, /sessions/{header}/logs/refresh |
— | log.fetch |
/discord/send |
— | discord.send |
/governance/permissions |
governance.read |
governance.write |
/auth/whoami, /identity/{id} |
identity.read |
identity.link (POST /auth/link) |
/capabilities |
capability.read |
capability.admin (grant/revoke) |
/tokens |
token.manage |
token.manage |
/sync |
sync.read |
sync.run |
/events |
events.subscribe |
— |
/admin/mode, /admin/shutdown, /name-index/rebuild, /import |
— | admin.mode / admin.shutdown / admin.index / admin.migrate |
Six routes are public (no bearer, no capability): GET /health, GET /schema,
GET /routes, GET /schema/version, POST /auth/margonem,
POST /auth/discord.
Note
A capability never bypasses the write gate. Even admin.all is refused when
the daemon is in read-only mode or on schema drift — status codes and error
ids in API reference.
Election eligibility (GET /elections/eligibility) requires pu.read because
eligibility is PU-derived data; a Gracz self-checks via pu.read.own. The
formula and window live in PU model.