Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ jobs:
- run: ./gradlew build
- uses: actions/upload-artifact@v4
with:
name: Usher
name: Sheriff
path: build/libs/*.jar
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ jobs:
want="${GITHUB_REF_NAME#v}"
have="$(./gradlew -q properties | sed -n 's/^version: //p')"
[ "$want" = "$have" ] || { echo "Tag $GITHUB_REF_NAME doesn't match version $have in build.gradle.kts"; exit 1; }
head -1 RELEASE_NOTES.md | grep -qx "# Usher $want" || { echo "RELEASE_NOTES.md is for another version. Its first line must be: # Usher $want"; exit 1; }
head -1 RELEASE_NOTES.md | grep -qx "# Sheriff $want" || { echo "RELEASE_NOTES.md is for another version. Its first line must be: # Sheriff $want"; exit 1; }
- run: ./gradlew build
- name: Attach the jar to a GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: gh release create "$GITHUB_REF_NAME" build/libs/Usher-*.jar --title "Usher $GITHUB_REF_NAME" --notes-file RELEASE_NOTES.md --verify-tag
run: gh release create "$GITHUB_REF_NAME" build/libs/Sheriff-*.jar --title "Sheriff $GITHUB_REF_NAME" --notes-file RELEASE_NOTES.md --verify-tag
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Usher
# Sheriff

Registered in the owner's ops process (`~/Code/ops`). Area: `minecraft-plugins` (lead: Minecraft: Plugins Lead). Added 2026-11-09 from the ops starter kit; the repo itself dates from 2026-11-07 (as Stationmaster).
Registered in the owner's ops process (`~/Code/ops`). Area: `minecraft-plugins` (lead: Minecraft: Plugins Lead). Added 2026-11-09 from the ops starter kit; the repo itself dates from 2026-11-07.

**Before doing anything here, run `~/Code/ops/scripts/repo-context`.** It shows who leads this repo, whether that session is active, what's pending, and which briefs apply. If it says the repo has no active owner, tell the director ("Helm") before starting work.

Expand Down
59 changes: 25 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Usher
# Sheriff

A Paper plugin for whitelisted servers. Admins approve a new player and choose their profiles, and Usher sets their whitelist entry and LuckPerms groups. Nobody types LuckPerms commands.
A Paper plugin for whitelisted servers. Admins approve a new player and choose their profiles, and Sheriff sets their whitelist entry and LuckPerms groups. Nobody types LuckPerms commands.

**Status:** pre-release. It passes its bot tests on throwaway Paper 26.1.2, 26.2 and 26.3 servers with LuckPerms and Multiverse. It hasn't been used on a real server or seen in a real game client yet. See [Testing](#testing).

- Paper 26.1 or newer, on Java 25. There's no support for 1.21.x.
- Needs LuckPerms. Without it, Usher still loads but refuses every change and says why.
- Needs LuckPerms. Without it, Sheriff still loads but refuses every change and says why.
- Works with Multiverse and Multiverse-Inventories, but doesn't need them.

## What it does
Expand All @@ -15,44 +15,44 @@ A profile is a LuckPerms group, such as "Builders", plus the worlds that group o
- Adds or removes groups through LuckPerms' API. The change applies at once and survives restarts.
- Saves the groups first, then adds the person to the whitelist. If LuckPerms fails, they aren't whitelisted.
- Sends anyone standing in a world they just lost to the lobby. It's an ordinary teleport, so Multiverse-Inventories keeps their things with the world they left.
- Logs who changed what and when, in `plugins/Usher/changes.log`.
- Logs who changed what and when, in `plugins/Sheriff/changes.log`.

Approving someone with no profile gives them whatever LuckPerms' default group allows.

There are three ways to do it:

- **In the game:** `/approve <name>` opens a chest menu. Click profiles on or off, then press Approve.
- **From Leaf Juice or a script:** through the server console. See [Console commands](#console-commands).
- **From Discord or another plugin:** Usher fires Paper events that a bridge plugin can listen to. See [Events](#events).
- **From Discord or another plugin:** Sheriff fires Paper events that a bridge plugin can listen to. See [Events](#events).

### Safety rules

- Only `usher.approve` can approve or change profiles. Players without it don't see the commands at all.
- Only `sheriff.approve` can approve or change profiles. Players without it don't see the commands at all.
- Nobody can approve themselves or change their own profiles, even with the permission. The console can.
- `restricted-groups` are never added or removed. A profile is refused, with a warning in the log, if its group:
- is on that list, in any capitals,
- inherits a group on that list,
- grants every permission (`*`), or
- grants `usher.approve` or LuckPerms editing.
- grants `sheriff.approve` or LuckPerms editing.
These checks run at startup and again on every change, so they still hold if a group changes after a menu opens.
- Adding a profile that puts someone in two chat circles at once (see `circle` in the config) asks for a yes first. A profile with a `confirm` question, such as an adults-only world, does too. A yes to one question doesn't cover another.
- Changes run one at a time. Two admins editing the same person both get their change. If both add the same profile, it's added once.
- Nothing is changed until Approve or Save is pressed. Closing the menu cancels. Every click in the menu is cancelled, so no item can be taken out.
- The menu says whether the account has played here, has tried to join, or has never tried. An account that has never tried gets a warning, so a mistyped name that belongs to a stranger is easy to spot.

Children play on the servers this was built for. [docs/kids-first.md](docs/kids-first.md) says how Usher protects them and what it leaves to the people running it. Owner decisions are recorded in [docs/decisions.md](docs/decisions.md).
Children play on the servers this was built for. [docs/kids-first.md](docs/kids-first.md) says how Sheriff protects them and what it leaves to the people running it. Owner decisions are recorded in [docs/decisions.md](docs/decisions.md).

### The menu

Each profile shows its state in words with a symbol: ✔ On, + Will be added, − Will be removed, ○ Off, or ↳ Comes from another group. On and added are aqua, removed is gold. So the menu reads the same without colour, and it avoids the red and green that colour-blind players most often can't tell apart. Results in chat start with ✔, and errors with ✘. The menu shows up to 14 main profiles and 14 extras. If there are more, it says so, and the rest still work from the console.

## Install

1. Put `Usher-<version>.jar` in `plugins/`, next to LuckPerms.
2. Start the server once, then edit `plugins/Usher/config.yml`. The profiles in it are examples.
1. Put `Sheriff-<version>.jar` in `plugins/`, next to LuckPerms.
2. Start the server once, then edit `plugins/Sheriff/config.yml`. The profiles in it are examples.
3. Create each profile's group in LuckPerms, and give it its worlds, for example `multiverse.access.<world>`.
4. Run `usher reload` in the console.
5. Grant `usher.approve` to the people who approve, and nobody else.
4. Run `sheriff reload` in the console.
5. Grant `sheriff.approve` to the people who approve, and nobody else.

The lobby must be open to everyone, since people are sent there. If Multiverse's `enforce-access` is on, the default group needs `multiverse.access.<lobby>`.

Expand All @@ -62,34 +62,25 @@ The lobby must be open to everyone, since people are sent there. If Multiverse's
| --- | --- |
| `lobby-world` | Where people go when a world closes to them. Blank means the main world. |
| `name-lookup` | How typed names become accounts. `auto` asks Mojang in online mode. `mojang` always asks, which is right behind a proxy. `offline` never asks. |
| `restricted-groups` | Groups Usher never touches. |
| `restricted-groups` | Groups Sheriff never touches. |
| `profiles.<id>.group` | The LuckPerms group. It must exist. |
| `profiles.<id>.worlds` | Worlds this profile opens. `*` matches anything, so `explore*` covers `explore_nether`. |
| `profiles.<id>.circle` | The chat circle it joins, for the two-circles warning. |
| `profiles.<id>.confirm` | A question that needs a yes before it's added. |
| `profiles.<id>.toggle` | `true` puts it in the menu's lower rows, as an extra. |
| `profiles.<id>.granted` | What the player sees when it's added. |

A config that can't be read at startup makes Usher refuse every change until it's fixed. A broken file on `reload` keeps the settings already in use.
A config that can't be read at startup makes Sheriff refuse every change until it's fixed. A broken file on `reload` keeps the settings already in use.

### Names

`/approve` and the console take a name that has never joined, in any capitals, and find the real account. Usher checks, in this order:
`/approve` and the console take a name that has never joined, in any capitals, and find the real account. Sheriff checks, in this order:

1. Players the server already knows.
2. People the whitelist recently turned away.
3. Mojang's public profile lookup, in online or `mojang` mode.

A typo fails with "No Minecraft account is called …". If Mojang can't be reached, Usher says that instead of guessing, and nothing changes. In `offline` mode, capitals matter for names the server hasn't seen, because offline accounts are built from the exact name.

## Upgrading from Stationmaster

Usher was called Stationmaster up to 0.2.1. It was renamed in 0.3.0; nothing else changed in how it works.

- Remove `Stationmaster-0.2.1.jar` and add `Usher-0.3.0.jar`, with the server stopped.
- On its first start, Usher copies `plugins/Stationmaster/` to `plugins/Usher/` if `plugins/Usher/` doesn't exist yet: your config, `changes.log` and `join-attempts.json` come across unchanged. The old folder is left as it was; delete it once you're happy. If `plugins/Usher/` already exists, nothing is copied.
- The old names went in 0.4.0, as 0.3.x said: the permission `stationmaster.approve` and the console command `stationmaster` (also `smaster`) no longer work. Grant `usher.approve` instead: `lp group <group> permission set usher.approve true`.
- The API package is `com.extantape.usher.api` (`UsherProblemEvent` was `StationmasterProblemEvent`).
A typo fails with "No Minecraft account is called …". If Mojang can't be reached, Sheriff says that instead of guessing, and nothing changes. In `offline` mode, capitals matter for names the server hasn't seen, because offline accounts are built from the exact name.

## Commands

Expand All @@ -98,10 +89,10 @@ Usher was called Stationmaster up to 0.2.1. It was renamed in 0.3.0; nothing els
| `/approve <name>` | approvers | Opens the menu to whitelist someone and choose their profiles. |
| `/profile <name>` | approvers, console | Shows someone's profiles, with a button to change them. |
| `/profile <name> edit` | approvers | Opens the menu to change their profiles. |
| `/usher reload` | approvers, console | Re-reads `config.yml`. |
| `usher <command> …` | console | The commands below, for Leaf Juice, scripts and people. |
| `/sheriff reload` | approvers, console | Re-reads `config.yml`. |
| `sheriff <command> …` | console | The commands below, for Leaf Juice, scripts and people. |

Approvers need `usher.approve`. It's `false` by default, operators included.
Approvers need `sheriff.approve`. It's `false` by default, operators included.

## Console commands

Expand Down Expand Up @@ -131,19 +122,19 @@ How `apply` works:
Example:

```
> usher apply 069a79f4-44e9-4726-a5be-fca90e38aaf5 Notch builders none true leaf-juice --json r42
> sheriff apply 069a79f4-44e9-4726-a5be-fca90e38aaf5 Notch builders none true leaf-juice --json r42
ea-result r42 {"id":"r42","ok":true,"status":"ok","code":"applied","message":"Notch is approved and can join now, with Builders.", ...}
```

## Events

Usher sends nothing over the network itself, apart from name lookups to Mojang. To post to Discord, listen to these events from another plugin. All fire on the main thread. They carry names and profiles, which can show who is a child, so post them only to a channel that just the admins can read.
Sheriff sends nothing over the network itself, apart from name lookups to Mojang. To post to Discord, listen to these events from another plugin. All fire on the main thread. They carry names and profiles, which can show who is a child, so post them only to a channel that just the admins can read.

| Event | When |
| --- | --- |
| `com.extantape.usher.api.ProfilesChangedEvent` | Someone was approved or their profiles changed. It has who, by whom, how, what was added and removed, and any warnings said yes to. |
| `com.extantape.usher.api.JoinAttemptEvent` | Someone not on the whitelist tried to join. At most once every ten minutes per account. |
| `com.extantape.usher.api.UsherProblemEvent` | LuckPerms missing or failing, a config problem, a refused restricted group, self-approval, or a change cut off by a crash. |
| `com.extantape.sheriff.api.ProfilesChangedEvent` | Someone was approved or their profiles changed. It has who, by whom, how, what was added and removed, and any warnings said yes to. |
| `com.extantape.sheriff.api.JoinAttemptEvent` | Someone not on the whitelist tried to join. At most once every ten minutes per account. |
| `com.extantape.sheriff.api.SheriffProblemEvent` | LuckPerms missing or failing, a config problem, a refused restricted group, self-approval, or a change cut off by a crash. |

## If the server stops mid-change

Expand All @@ -153,7 +144,7 @@ Each change writes a `started` line to `changes.log` before LuckPerms is touched

`./gradlew build` runs the unit tests: config parsing, the safety rules, the warnings and the change log's crash handling.

The bot tests in `test/` build fresh Paper servers in a temp folder, on 127.0.0.1 port 25615. Each has LuckPerms, Multiverse-Core, Multiverse-Inventories, made-up worlds and groups, and mineflayer bots as players. Every result that says "blocked" has a check beside it showing the same thing works when it should. See the top of `test/harness.mjs` for setup, then run `node test/acceptance.mjs`. It runs one server at a time and deletes it when the run ends. Set `USHER_TEST_KEEP=1` to keep it for a look. LuckPerms uses YAML storage unless `USHER_TEST_STORAGE=h2`.
The bot tests in `test/` build fresh Paper servers in a temp folder, on 127.0.0.1 port 25615. Each has LuckPerms, Multiverse-Core, Multiverse-Inventories, made-up worlds and groups, and mineflayer bots as players. Every result that says "blocked" has a check beside it showing the same thing works when it should. See the top of `test/harness.mjs` for setup, then run `node test/acceptance.mjs`. It runs one server at a time and deletes it when the run ends. Set `SHERIFF_TEST_KEEP=1` to keep it for a look. LuckPerms uses YAML storage unless `SHERIFF_TEST_STORAGE=h2`.

**Passed on Paper 26.1.2, 26.2 and 26.3 (2026-11-07), 100 checks each:**

Expand Down
23 changes: 10 additions & 13 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
# Usher 0.4.1
# Sheriff 0.5.0

A fix to how `attempts` pages, so it matches the shared command format. Nothing else changes.
A new name (D194): the sheriff decides who's welcome in town. Nothing else changes in how it works.

This is an early 0.x release. It has passed bot tests on throwaway servers, but it hasn't been used on a real server or seen in a real game client.
## Installing over the previous version

## Fixed
With the server stopped:
1. Rename its plugin folder to `plugins/Sheriff`.
2. Rename its permission in LuckPerms to `sheriff.approve`.
3. Use `/sheriff` for the console commands.
4. Swap in `Sheriff-0.5.0.jar`.

- **`usher attempts --json` answers in the spec's list shape:** `items`, and `next` as a string cursor, or `null` on the last page. 0.4.0 sent the list under `attempts`, with a numeric `next`, which Leaf Juice found. The cursor is still a position, written as a string. One that Usher didn't hand out is refused with `bad-argument`.
- `describe` gives the cursor's type as `string`.
- Signalbox 0.3.0, whose `Answer.page` makes this shape the default.

## Upgrading from 0.4.0

Swap the jar with the server stopped. Scripts that read `attempts` from the answer read `items` instead.
Install WelcomeWagon 0.6.0 at the same time: Sheriff hooks its invite events by their new names. Leaf Juice needs its matching update to find Sheriff.

## Tested

- 51 unit tests pass, including the cursor rules.
- The bot suite passes 121/121 on throwaway Paper 26.1.2, 26.2 and 26.3, including a check of the list's shape.
Unit tests pass. The rename was checked on a throwaway server.
12 changes: 6 additions & 6 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins {
}

group = "com.extantape"
version = "0.4.1"
version = "0.5.0"

java {
toolchain.languageVersion.set(JavaLanguageVersion.of(25))
Expand Down Expand Up @@ -48,17 +48,17 @@ tasks.test {
}

// Signalbox goes inside the jar, relocated, so it can never clash with another plugin's copy. The shaded jar is the
// only one built, so the release workflow's Usher-*.jar finds exactly one.
// only one built, so the release workflow's Sheriff-*.jar finds exactly one.
tasks.shadowJar {
archiveBaseName.set("Usher")
archiveBaseName.set("Sheriff")
archiveClassifier.set("")
from("LICENSE")
relocate("com.extantape.signalbox", "com.extantape.usher.libs.signalbox")
relocate("com.extantape.signalbox", "com.extantape.sheriff.libs.signalbox")
}
tasks.jar { enabled = false }
tasks.assemble { dependsOn(tasks.shadowJar) }

// Test only: a tiny plugin that logs Usher's events, for the bot tests in test/. Never shipped.
// Test only: a tiny plugin that logs Sheriff's events, for the bot tests in test/. Never shipped.
val spy: SourceSet = sourceSets.create("spy") {
java.srcDir("test/spy/java")
resources.srcDir("test/spy/resources")
Expand All @@ -67,6 +67,6 @@ val spy: SourceSet = sourceSets.create("spy") {
dependencies { "spyCompileOnly"(paperApi) }
tasks.register<Jar>("spyJar") {
from(spy.output)
archiveFileName.set("UsherSpy.jar")
archiveFileName.set("SheriffSpy.jar")
destinationDirectory.set(layout.buildDirectory.dir("test-spy"))
}
4 changes: 4 additions & 0 deletions docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ Decisions the owner made for Stationmaster, with dates. Follow them without aski
- **Invites (2026-11-09, owner):** when a Latchkey invite is redeemed, the username is whitelisted at once and Usher, if installed, applies the profile the invite names, through its usual safety rules. To be built with Latchkey 0.2.0 (crossing `latchkey-usher`).
- **Profiles on invites (2026-11-09, owner):** a Latchkey invite carries a list of Usher profiles. Usher runs its safety checks when the invite is created, so a bad combination is refused before anyone arrives, and applies the profiles when it's redeemed. An invite with no profiles gives lobby-only access until an admin adds more. Usher's commands get a machine-readable JSON form and `/usher describe`, through the shared command library. Waystation and HouseRules need nothing new: their platforms and circles are already permissions a profile can carry.
- **Console format and the old names (2026-11-10, owner through Helm):** 0.4.0 moves the console commands to the shared Signalbox format (`--json <id>`, `ea-result`, `describe`) and removes `stationmaster`, `smaster` and `stationmaster.approve`, as 0.3.x promised. Leaf Juice's asks (crossing plugins-leafjuice, ledger 10f277027b09): `none` is reserved and can't be a profile name, and `describe` marks optional arguments (Signalbox 0.2.0).

## 2026-11-14

- **Name (owner, D194):** the plugin is renamed Sheriff. The owner chose "Sheriff (Recommended)", and asked that no old names be kept: no aliases, no old permissions and no folder copy. Released as 0.5.0.
Loading
Loading