docs(modules): phase 7 as built — the permission mirror, and the set arithmetic behind it

Protocol 4 (`PROTOCOL.md` §10), phase 7 as built (`PLAN.md` §20), what an operator
needs to know about it (`INTEGRATION.md`), and the in-game leg as a walk to run
(`PLAYER_WALK.md`).

**The spec.** One verb carrying the whole desired set, diffed by the plugin
against the live store; a report whose two interesting fields are the ways a push
looks like it worked and did not (`unresolved`, `pending`); drift as a report
rather than an action; and the permission hooks as a live SIGNAL rather than the
record — a hook that stops firing costs latency, not correctness.

**The finding the design turns on, written where it belongs.** A name in the store
that is not in the desired set is either something the site retired or something a
human granted, and those have opposite correct answers. The store records who
granted a permission nowhere, so only the website can tell them apart — which is
why it keeps a ledger of what it pushed, and why revoking a hand edit needed a
table of its own.

**§10.5 is a rule generalising.** "A wedged sidecar must never stall the game"
becomes "nothing the far side sends may cost the main thread unbounded work",
because `perm.sync` is the first command whose work is not bounded by its own
shape. Three bounds, each on the side that can say something useful when it is hit.

**§20.7 says plainly what is not proven**: the acceptance line needs a second,
non-admin Steam account on the rig, and nothing in the plugin has been compiled.
The walk doc carries the seven steps, including the two things to confirm on
Carbon rather than assume.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMH6bw1jXMgbyF3ZWGEzSM
This commit is contained in:
2026-09-21 22:59:24 -05:00
parent 4fe8864939
commit bba2ab04e0
4 changed files with 441 additions and 6 deletions

View File

@@ -127,3 +127,50 @@ unreachable right now"* instead, which is the same rule with nothing left to be
for it (check the chat log and the sidecar's `/events?kind=account.link.requested` — the frame
carries the steam id, the name and a TTL, and **no code**), a Steam account belongs to one website
account at a time, and every refusal is a sentence that tells the player what to do next.
---
## The permission walk (protocol 4, phase 7)
Added 2026-09-21. The website half was walked end to end against a stand-in plugin — the authoring
screen, the report, drift and its two answers, and a restart that emptied the store and was fully
re-pushed. **What is left is the sentence the phase exists for: a grant made on the website gates a
third-party plugin in the game.**
It cannot be walked from a console, and it cannot be walked on the owner's account:
- **A console session bypasses every gate.** The standard idiom is
`return !player || permission.UserHasPermission(...)`, and an RCON command has no `BasePlayer` —
so the console is unconditionally allowed ([PLAN.md §12.5](../modules/rust/PLAN.md)).
- **An admin account bypasses most plugins' gates too**, and not uniformly: Popup Notifications
(`player.IsAdmin ||`) and Zone Manager (`authLevel > 0 ||`) are hard bypasses. Kits is the
exception — its `IsAdmin` is the `kits.admin` **permission** and `AdminIgnoreRestrictions`
defaults to `false` — so a kit's `RequiredPermission` does apply to a server owner.
So this walk wants a **second, non-admin Steam account** connected to the rig. Kits alone can be
walked on the owner's account; steps 4 and 5 cannot.
| # | Do this | You should see |
|---|---|---|
| 1 | **Link the second account** (the identity walk above), then on the website open Admin → Rust permissions and grant it a kit's `RequiredPermission` — pick the kit from `GetKitNames`, or read one out of `oxide/config/Kits.json` | The grant appears with the account beside it. Within a minute the server row reads **in sync** — or press *Sync now* and watch it happen |
| 2 | **In game on that account, open the kit menu** | The kit is no longer locked. Before the grant it shows as locked; that difference is the whole phase |
| 3 | **At the server console, `oxide.show user <steamid>`** | The permission is there, granted by this plugin rather than by hand |
| 4 | **At the console, `oxide.grant user <steamid> zonemanager.admin`** (a permission the site manages but did not grant) | Within seconds the website's screen shows it under *Changed in game*. **Revoke** it there, and it is gone from `oxide.show user` on the next sync. **Adopt** a different one instead and it stays, now listed as the site's own |
| 5 | **Put the second account in a group on the website, then wipe or restart the server** (a wipe script that clears `oxide/data/` is the interesting case) | After the server is back: the group exists again, the membership is back, and the grant is back — without anybody touching the website. This is R2's central promise and the one thing a stand-in cannot prove |
| 6 | **Grant a permission whose plugin you have just unloaded** | The site reports it **unresolved** against that server and keeps the grant. Load the plugin again: it lands on the next sync, with nothing typed |
| 7 | **Add a website account that has never connected to this server to a group** | The site reports the membership as *waiting on their first connection*. Have them connect: it lands. A **direct grant** to the same account, by contrast, is in `oxide.show user` immediately |
**Run it on both frameworks.** From phase 3, done means done on Oxide and on Carbon (R19/R21), and
this phase has two specific things to confirm there rather than assume:
- **`GetPermissionUsers` / `GetUsersInGroup` entry format.** Both answer `id(name)`, and the spacing
differs between the calls and between the frameworks. The plugin takes everything before the first
bracket. If that parse is wrong, **every holder is reported as foreign** — which is visible
immediately: the drift list fills with grants the site itself made.
- **`GetGroupPermissions(name, false)`** is called with both arguments. If Carbon's signature has no
second parameter, the plugin does not compile there at all — the one place in protocol 4 where
R19's byte-identical-plugin claim is at risk.
**What counts as a pass:** a non-admin player's access in game changes because of something typed on
the website and nothing else; a hand edit is reported rather than undone; and a wipe costs the
operator nothing.