Skip to content

Governance

Nerthus.Core (until cutover). This page describes the frozen system that runs today and is deleted at cutover. Replaced by: not yet written.

Governance is the narrator permissions table — Uprawnienia — one row per Narrator, keyed by Margonem ID and stored in private state (never the lore repository). Each row records what a Narrator is allowed to run: ranga, magia, polityka, walki_pvp, and the like. This page covers reading the table and upserting one row. The write example targets Anward, the Narrator of the fixture session.

Reads need governance.read, writes need governance.write; the admin token carries both. Every example runs against a live daemon — see how the reference is tested.

Routes

Method Path Cmdlet Cap Write
GET /governance/permissions Get-NerthusNarratorPermission governance.read
PATCH /governance/permissions/{nick} Set-NerthusNarratorPermission governance.write
GET /config/schedules governance.read
PATCH /config/schedules/{workload} governance.write

Cap is the required capability ( = public, no token); Write () marks routes that pass the write gate; a in Cmdlet is reached directly, no wrapper. Paths are relative to /v1/api; the cross-cutting contract — middleware, envelopes, status codes — is on the API reference index.

This page is the narrator permissions table and nothing else. The /governance/* prefix also carries the fact ledger — who held which function when, which rule was in force — on /governance/refs, /governance/{ref} and /governance/{ref}/log, which are public and are covered by the governance ledger. The two share a prefix and no concept. Route order keeps them apart: permissions is a literal declared before /{ref}, and it is not a ref name.

The {nick} path segment also accepts the Margonem id; a successful edit fires the AfterGovernanceChange hook (governance:changed on the event stream). The Uprawnienia model: Permissions.

Read the permissions table

Schedule ownership

Three recurring workloads can run from two places. schedules.<workload>.owner in the committed config.json names one owner each, and both halves of the deployment obey it: the primary's tick through Get-NerthusScheduleOwner, the CI drivers through Get-NerthusScheduleOwnerFromRepo before any daemon exists. These two routes are the only surface over that block.

GET /v1/api/config/schedules answers { workloads, source }. Each workload carries its owner, its allowed set, its default, and a source of committed or default. source is read from the committed file alone — the merged config lays the document over the built-in defaults, so every workload reads as decided there whether or not anybody decided it, and a console must not render a default nobody chose as a decision somebody made.

PATCH /v1/api/config/schedules/{workload} takes { "owner": "off" }. An owner outside the workload's allowed set is 422 naming the set — fleet on settlement is refused rather than defaulted, because settlement has no tick and fleet there would be silence dressed up as a choice. An unknown workload is 404. ?dryRun=true answers 200 with wouldSet and persists nothing.

Two things the block does not say and the answer does. off does not stop the pipeline — the GitLab schedule still fires and the driver exits as a no-op; what stands down is the driver. And the edit is committed lore, so the response carries effectiveAfterConverge: true: this host obeys at once, the rest of the fleet at converge speed.

GET /v1/api/governance/permissions returns the { count, items } envelope — every Uprawnienia row. Narrow to one Narrator with ?id= (Margonem ID or nick). A freshly imported repository has no rows yet.

GET /v1/api/governance/permissions
Authorization: Bearer <token>
await fetch("https://evocation.nerthus.pl/v1/api/governance/permissions", {
  headers: { "Authorization": `Bearer ${token}` },
}).then((r) => r.json());

Response 200:

{ "count": 0, "items": [] }

Preview a permission edit without writing

PATCH /v1/api/governance/permissions/{nick} upserts the matched row (by Margonem ID or nick) and stamps zmieniono. With ?dryRun=true it returns 200 { wouldUpdate, fields } and persists nothing — proven by the follow-up ?id=Anward read still finding no row.

PATCH /v1/api/governance/permissions/Anward?dryRun=true
Content-Type: application/json
Authorization: Bearer <token>

{ "ranga": "Narrator", "magia": "do 3/5" }
await fetch("https://evocation.nerthus.pl/v1/api/governance/permissions/Anward?dryRun=true", {
  method: "PATCH",
  headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
  body: JSON.stringify({ ranga: "Narrator", magia: "do 3/5" }),
});

Response 200:

{ "wouldUpdate": "Anward", "fields": { "ranga": "Narrator", "magia": "do 3/5" } }