R2, the game half. The website is the author of record for groups and grants;
this plugin makes the framework's own permission store match what it is sent.
**One verb, and the plugin does the diffing (D32).** `perm.sync` carries the
whole set the site authors for this server. The plugin compares it against the
live store and writes only what differs, then answers with a report: what it
applied, what was already right, the names it could not resolve, the memberships
waiting on a first connection, and every holder the site did not author.
`perm.catalogue` is the other half — a live read of what the loaded plugins have
registered, which is the option source the authoring form is built from.
**The first command whose work is unbounded, so it is drained rather than run.**
A community with two thousand linked players sends thousands of operations in one
frame, and applying them in the tick the frame arrives is a freeze an operator
will blame on the game. A sync compiles into a list of single-store operations
and drains two hundred at a time on a timer. That is §5's rule — nothing the far
side sends may cost the main thread an unbounded amount of work — applied to the
inbound half for the first time.
**The pre-check §12.2 rule 1 exists for.** `GrantUserPermission` returns void,
throws nothing and logs nothing for a name no plugin registered, so every grant
naming an unloaded plugin's permission would disappear without a trace. The plugin
checks `PermissionExists` first and reports the name as unresolved; it does NOT
register the name itself (D33), because that fabricates a permission the operator
never installed.
**`UserHasPermission` cannot answer the question the diff asks.** It is true for a
member of a group that carries the permission, so a direct grant would look
already applied the moment a group covered it — and a revoke already done.
`GetPermissionUsers` reads the user's own set, which is what a direct grant is.
Six permission hooks join the read path, all void, all in `ExpectedHooks`. They
are not the drift record — only the site's desired set can say whether a change is
foreign — they are a reason to reconcile sooner, and they are suppressed while
this plugin is writing, or every sync would announce itself as drift.
`rg.perms` reports the last report from the console, which is the fastest way to
tell "no plugin registered that name" from "the store has never seen that player".
Protocol bumped to 4 here and in overlay.toml, in the same change as the emitters.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMH6bw1jXMgbyF3ZWGEzSM