diff --git a/modules/rust/PLAN_REDESIGNS.md b/modules/rust/PLAN_REDESIGNS.md index fbdd948..36eef37 100644 --- a/modules/rust/PLAN_REDESIGNS.md +++ b/modules/rust/PLAN_REDESIGNS.md @@ -222,6 +222,56 @@ rig), and a change to a shared group (split off, the other rig unchanged). Then and Grant all / Revoke all. The plugin buttons must be by owner: `nokits` is under ZoneManager. The large-store measurement is in §1.2. +### 1.9 As built, and as walked (2026-09-28) + +Rust-Plugins `feat/perm-inventory`, Rust-Link `feat/perm-inventory`, Module-Rust `feat/perm-manager`. The +wire is [`PROTOCOL.md`](../../rust-link/PROTOCOL.md) §19.9, and the admin API is the module's swagger +fragment (`/admin/rust/permissions…`). + +**What the build settled that the plan left open:** + +- **The data model.** Groups are rows with ids: `rust_permgroups`, plus `_servers` (with `included = 0` + to take one server out of an all-servers group), `_permissions`, `_members`, `_steam_members` and + `_chat`. The chat style belongs to a group row now, because one server's `vip` may be styled + differently. Steam-account grants are `rust_perm_steam_grants`, and D190's exceptions are + `rust_perm_exceptions`. `rust_servers.perm_policy`, `rust_perm_sync.imported_at`, + `rust_perm_catalogue.owner` and `rust_perm_drift.direction` are new columns. The old group tables + are copied once at boot (a `rust_settings` marker makes it once) and are then left unread. +- **Every sync is read → reconcile → push.** The reconcile step runs under one fleet-wide lock, so two + servers never split one shared group at once. Each desired row remembers the authored rows that + produced it (its *sources*). That is how a removal knows whether to delete a row, add an exception, or + split a group. +- **Two things are never judged.** A permission the server does not register right now (an unloaded + plugin is not a revocation), and a pair an event lease holds. +- **A removal at the first import is pushed back**, not deleted. D198 imports what is present; a + snapshot taken the moment the site first looked is not taken as a revocation. +- **An event's grant removed in the game** is pushed back under auto-adopt, with a notice. The event + owns it, and its revert withdraws it. +- **Under `adopt`**, a removal or a changed group is *held*: it is not pushed back, retired or recorded + until a person answers. "Put back" works by forgetting the ledger row, and the next sync pushes it. +- **A group the site already authored keeps the site's title** at import. `walkvoice`, made by the site + before the rebuild, overwrote the game's empty title with "Walk voice" on Oxide. +- **The announcement voice names a group by id.** A setting saved as a name before the rebuild still + resolves, to the first styled group of that name. + +**Walked** on `rust-oxide` and `rust-carbon`, against the walk core on `rustp16` (backed up first): + +| Case | Result | +|---|---| +| Migration of the old groups | 2 copied (`site` → Oxide only; `walkvoice` → every server) | +| First import (D198) on an existing install | Oxide: `default`, `admin` made its own, 2 hand grants → Steam grants, the site's own grants recorded as landed; Carbon: `default`, `admin`, `moderator` with titles kept byte for byte | +| Auto-adopt: `oxide.grant` + `oxide.revoke` | adopted within one 30 s tick; the revoked Steam grant deleted | +| Fleet grant (`*`) revoked in the game on Oxide (D190) | an exception for `rust-oxide`; still pushed and landed on Carbon | +| `oxide.unload Kits` | nothing deleted; the report lists both `kits.*` as unresolved | +| `c.grant group walkvoice kits.admin` on Carbon (D190) | split: Carbon got its own `walkvoice` with the change and the style; the shared one excludes Carbon; a notice waits | +| Policy `adopt` | one addition and one removal waited; the removal was held, not pushed back; adopt and accept answered them | +| Policy `revoke` | an in-game grant undone at the next sync (`revokes: 1`) | +| Server view | plugins by owner (`nokits` under ZoneManager), groups with where they are, players named, the exception listed | + +**Not walked yet:** a group with a parent; sharing and unsharing from the screen, and its conflict answer; +Grant all / Revoke all; the screen itself in a browser (every endpoint behind it was called); and the +large-store measurement. + --- ## 2. The event step editor and the kit weekend (§4.2, §4.3; U-3–U-6, F11; D164) diff --git a/rust-link/PROTOCOL.md b/rust-link/PROTOCOL.md index 04b971c..932524f 100644 --- a/rust-link/PROTOCOL.md +++ b/rust-link/PROTOCOL.md @@ -2182,3 +2182,66 @@ zone** — that needs somebody in the game. servers is unreachable; a dead server that minted nothing no longer makes a wrong code look good. - **NPC attackers** (F2, D185) are named by the killfeed: a family (`scientistnpc_*` → Scientist, `bradleyapc` → Bradley APC) or the prefab without its variant digits (`wolf2` → Wolf). No wire change. + +### 19.9 `perm.inventory` — the whole permission store (the permission manager) + +The site owns every permission and group on a server, not just the ones it authored +(`docs/modules/rust/PLAN_REDESIGNS.md` §1, D160). So it has to read all of them. It does that with one +request/reply verb, through one new sidecar route: + + POST /permissions/inventory ─── perm.inventory ───► {snapshotId?, page?} + ◄── perm.inventory ──── one page + ◄── perm.error ──────── busy · stale · too-large + +**Page 0 without a `snapshotId` starts a fresh read.** The reply names the snapshot, and each later page +is asked for by `snapshotId` and `page`: + +```json +{"kind":"perm.inventory","type":"reply","snapshotId":"62fe7e6cfac5","page":0,"pages":1,"more":false, + "permissions":[{"name":"kits.admin","owner":"Kits"},{"name":"adminmodule.greet"}], + "groups":[{"name":"default","title":"Default ","rank":0,"parent":"","permissions":[]}], + "users":[{"steamId":"76561198038695917","name":"whitlocktech","permissions":["kits.admin"],"groups":["site"]}], + "leased":[{"kind":"group-permission","subject":"default","object":"kits.vip"}], + "stats":{"permissions":85,"groups":4,"users":2,"buildMs":148}} +``` + +- **`permissions`** is every registered name, with **`owner`**, the plugin that registered it. The + owner comes from `permission.PermissionExists(name, plugin)` asked of each loaded plugin. That is + public API on both frameworks (Oxide takes a `Plugin`, Carbon a `BaseHookable`), never a guess from + the name's prefix. A name no plugin owns has no `owner`; on Carbon those are its built-in modules' + names (`adminmodule.*`). +- **`groups`** have their title **verbatim** (Carbon's own end in a space), rank, parent, and the + permissions the group itself carries, not what it inherits. +- **`users`** is every Steam id holding anything, with its direct permissions and its groups, **except + `default`**, which every connected player is in by framework rule. `name` is the store's last + recorded name, when it has one. +- **`leased`** is the group permissions an event lease holds (§14). The site does not take them for hand + edits. +- **Page 0 alone carries `permissions`, `groups`, `leased` and `stats`**, and every page carries + `users`. A page is at most 700 KiB of rows, inside the 1 MiB line cap (§3.1). + +**Cost.** Neither framework can list its users, so holders come from `GetPermissionUsers` for every +permission and `GetUsersInGroup` for every group. Each of those walks every user. The read is therefore +built **25 sources per tick**, on the sync drain's 50 ms timer, and replied to when complete. It is taken +**once** and paged from memory for two minutes, so a grant made between two pages cannot tear it. A build +past **8 s** is abandoned and answered `too-large` — the sidecar's ten-second wait has already lost it. +A second read while one is building is `busy`; a page of a snapshot that is gone is `stale`, and the +site starts again from page 0 (once). `rg.inventory` runs the same build from the server console and +logs its summary instead of replying. + +**`perm.sync`**, in the same bump: + +- **Groups carry `parent`.** It is applied with `SetGroupParent` after every group exists and before any + retirement. A parent that will not set (missing, or a loop) is reported in `notLanded` as + `group:parent:name`. +- **An existing group's title and rank are written when they differ.** Until now a title was only set + when the sync created the group. **An absent `title` or `rank` means "leave it"**, and an empty title + is a title. The site omits them while a person decides about an in-game change (the `adopt` policy). + The report's `applied` gains **`groupsUpdated`**. +- **`managed` and the report's `foreign` are gone.** The foreign scan looked only at names the site + claimed. The site now reads the whole store and decides what an in-game change becomes itself + (PLAN_REDESIGNS §1.5, §1.6). + +Walked on both rigs, 2026-09-27/28. Oxide gave 85 permissions, every one owned (`RustCore` owns +`oxide.*`), in 148 ms over four ticks. Carbon gave 104, the 30 `adminmodule.*` unowned, in 204 ms. On +both, **`zonemanager.ignoreflag.nokits` belongs to ZoneManager**.