Skip to content

The moderator roster

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

Who holds mc or smc, since when, and whether they can read a record at all.

The roster is not public, unlike the regulation corpus beside it. Which rules apply to everybody is a public fact; who enforces them is not, and the file carries published encryption recipients as well as names.

Routes

Method Path Cmdlet Cap Write
GET /moderators moderation.read

The one row carried a cmdlet name that named nothing. Get-NerthusModerator is defined in no .ps1 in Nerthus.Core and exported by no manifest. The cell is empty now, following the regulations page's precedent for a browser-only family, and ci/assert-cmdlet-names.py in this repository fails the build if it comes back.

Replica-safe and poolable, with one caveat that no other replica row has: the corpus is a second working tree. Two hosts agree only when both clones sit on the same commit, so the answer carries corpus and a stale clone is a question a caller must be able to ask. The read itself is still a pure function of the bytes.

What the answer says about itself

{
  "count": 8,
  "corpus": "present",     // null = this host has no moderators clone at all
  "items": [
    { "name": "Vinnie", "margonemid": "8420046", "status": "Aktywny",
      "roles": ["smc", "mc"], "has_key": true }
  ],
  "findings": []
}

corpus: null and an empty items are different statements, and collapsing them is the failure this field exists to prevent. A host with no moderators clone answers every moderation question with an empty roster — 04-daemon.md calls that present and unreachable, and it is preferred to a host that will not boot — but an empty roster read as "nobody moderates" would let a caller conclude that no sanction has an author.

has_key, not the key. Whether somebody can be encrypted to is a roster question; the published age recipient itself is a keyring question and is answered by the keyring routes.

Keys on a block

A moderator block carries up to three credential tags, each parsed through the contributor grammar rather than a second one — a person who appears in both rosters must have one key accepted in both:

  • @klucz_szyfr — the published age recipient a new epoch is wrapped to.
  • @klucz_podpis — an ssh-ed25519 public signing key, with an optional (od:do) range. A record signature verifies against the ring's published signing keys (keyrings), so a member without one still reads and has every record they submit refused.
  • @klucz_api — a sha256:<64 hex> hash with an optional (od:do) range. The key authenticates its holder as an API principal, and for a moderator who is in no other roster it is the only way in: the margonem exchange refuses them with no_gracz_for_margonem_id.

Findings

kind severity when
ModeratorWithoutMargonemId Error a block with no @margonemid
ModeratorWithoutRole Error no @rola, so no bundle can be resolved
ModeratorUnknownRole Error @rola is neither mc nor smc
InvertedRange Error a range ending before it begins
RawSigningSecret Error @klucz_podpis holds a PRIVATE KEY — the pair is burned
MalformedSigningKey Error @klucz_podpis is not an ssh-ed25519 public key with an optional (od:do) range
RawApiKeyValue Error a raw nak_ value committed instead of its sha256: hash — the key is burned
MalformedApiKey Error @klucz_api is not sha256:<64 hex> with an optional (od:do) range
ModeratorWithoutKey Warning active, no @klucz_szyfr — cannot read any record body
ModeratorNotAContributor Warning the id resolves to nobody in nerthus.contributors.md

ModeratorWithoutMargonemId is an Error here where the contributor store tolerates a missing id, and the asymmetry is the point: a sanction has to be attributable to a person who can be asked about it. The block is still returned and still resolvable by name, because every row of the imported ledger names its moderator by display name and nothing else — an unattributable record is bad, an authorless one is worse.

ModeratorNotAContributor is a Warning deliberately. A chat moderator is not required to be a player, and treating that as a fault would refuse a legitimate appointment.

Every failed credential parse is an Error, because a dropped credential is a silent lockout that reads as a typo. RawApiKeyValue and MalformedApiKey also travel into the contributor model's findings, so a burned or unreadable key surfaces where the contributor store is read.

Tenure

@rola carries the estate's standard (VALID_FROM:VALID_TO) range. Standing down closes the range; it never deletes the line, because every record that person issued would otherwise lose its author — which is exactly what a contested sanction asks about.

Two consequences worth stating:

  • A role with no range holds at any time. No appointment date is attested for any current moderator, so demanding one would make every record they have ever written a finding. The authority check reports why it answered yes, so in-tenure and no-tenure-recorded stay distinguishable.
  • Two tenures need not meet. An office can genuinely stand vacant, so a gap between one moderator's VALID_TO and the next's VALID_FROM is not a finding — unlike the regulation chain, where a gap means a day with no rule in force. A record dated inside a roster gap is RecordOutsideTenure against everybody, which is the correct answer.