docs(modules): phase 8 as built, and the rig that finally put the sidecar where the design says it lives
Three documents: * `modules/rust/PLAN.md` §22 — the phase as built. Three org-lead decisions (D39-D41), what a player is told and what they are not, the refusals on a phone, and an honest limit the rig found: a rank can be live while every permission it carries resolves nowhere. * `rust-link/INSTALL_RIG.md` — new. The sidecar runs INSIDE the game container now, which is the shape R20 says the egg ships and which retires the firewall wall phases 6, 7 and 7b each stopped at. A container's 127.0.0.1 is genuinely private, so a stock plugin config and a stock sidecar find each other with nothing configured at all. Three things in the launcher are load-bearing and each is written down with the failure it prevents. * `rust-link/PLAYER_WALK.md` — the account walk on a phone, and a correction: 7b's "it needs a firewall rule on a development machine" is no longer true. `android/PLAN.md` gains M15. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMH6bw1jXMgbyF3ZWGEzSM
This commit is contained in:
@@ -986,7 +986,7 @@ Each phase ends with its findings written down, as every workstream here does.
|
||||
| 6 | **Identity** (R1), and the `admin.users.detail` slot (R13). ✅ **Built 2026-09-21 — as built and findings in §19.** Protocol 3: `/link` and `/unlink` in chat, codes in plugin memory, `link.confirm` as the first command the website originates; the site is the author of record and the game holds nothing. Seven org-lead decisions (§19.0), the slot declared in three registries (§19.2), a hole it found in this repo's own OpenAPI generator (§19.3) and **three defects a browser walk found that 122 green tests did not** (§19.4) — including every refusal sentence being invisible, because core's client reads `message` and this module had answered `error` since phase 1. **The site's half is walked; the code-from-the-game half is written down as a walk to run** (§19.6) | 3 + docs | A player links an account in-game; an operator sees the Steam id inside core's own user page |
|
||||
| 7 | **Site-owned permissions** (R2). ✅ **Built 2026-09-21 — as built and findings in §20.** Protocol 4: one verb carrying the whole desired set, diffed by the plugin and drained in bounded steps; groups mirrored as groups; drift reported with adopt and revoke; the `PermissionExists` pre-check as the mechanism; the permission hooks as a live drift *signal* rather than the record. Seven org-lead decisions (§20.0), four defects a browser walk found and a fifth a test did (§20.5). **The site's half is walked end to end, including a restart that emptied the store; the acceptance line below is NOT met** — it needs a second, non-admin Steam account on the rig (§12.5, §20.7) | all 3 + docs | A grant made on the website gates a third-party plugin in-game, survives a wipe, and behaves the same against Oxide's JSON store and Carbon's Protobuf/SQLite one |
|
||||
| 7b | **Mod configuration from the site** (R18). ✅ **Built 2026-09-22 — as built and findings in §21.** Protocol 5: a recursive walk rooted at the framework's own `ConfigDirectory`, a form generated from the values, a raw tier, an explicit reload target, and a write that backs the set up, reloads, watches `OnPluginLoaded` and **restores everything automatically** when it does not arrive. Four org-lead decisions (§21.0), a span-splicing editor so no untouched float is ever rewritten (§21.1), and **four defects a browser found that 179 green tests did not** (§21.5) — the worst being that every save of the bridge's own config was refused while the page said otherwise. **The site's half is walked end to end and the plugin compiles and loads on the live Oxide rig; the acceptance line below needs the sidecar and the game on one host** (§21.4) | all 3 + docs | An admin flips a ZoneManager setting from the website and it takes effect; a deliberately broken config rolls itself back and says why; a nested `<Mod>/x.json` is found and reloads the right plugin |
|
||||
| 8 | **Android leg B** (R10). Identity and permission surfaces | Android-app | A player links from the app |
|
||||
| 8 | **Android leg B** (R10), and the half of R2 a player may see. ✅ **Built 2026-09-22 — as built and findings in §22.** One drawer row under the player group shaped like `module-uo`'s own (the code card first, then what the code got them), gated on `rust` and `PLAYER`; the four refusals kept four pieces of advice; and **one new website route**, `GET /player/rust/permissions`, because phase 7 gave an operator every view of in-game privilege and a player none. Three org-lead decisions (§22.0), the scope arithmetic answered on the server so `inScope` exists once (§22.1), and **the rig moved the sidecar inside the game container** ([`INSTALL_RIG.md`](../../rust-link/INSTALL_RIG.md)), which retired the firewall wall phases 6, 7 and 7b each stopped at | Android-app + Module-Rust + docs | A player links from the app |
|
||||
| 9 | **Teams from first-party clans** (R5). Membership event-driven, leadership read off `LocalClan` at snapshot; **`declareModuleSlot` × 3** for core's `team.notify` / `team.activity` / `team.forum` | Module-Rust + 2 | The clan page is ours, core's contributions land in places we named, and every slot empty still reads correctly |
|
||||
| 10 | **Notifications and engagement** (R7). Streams, triggers with `ceiling` and `subjectKey`, audiences, engagement seeds, announce leg, post hook — **the catalogue is §10**, including the in-game-popup question | Module-Rust + docs | The offline raid alert reaches the player whose base it was, and nobody else |
|
||||
| 11 | **Android leg C** (R10). Inbox and notification preferences for Rust triggers | Android-app | A Rust notification arrives on a phone and can be switched off there |
|
||||
@@ -3206,6 +3206,107 @@ down in [`PLAYER_WALK.md`](../../rust-link/PLAYER_WALK.md).
|
||||
- **Still no module-declared site permission** at MODULE_API 1.10.0, exactly as §20 predicted, so
|
||||
`requireRole('admin')` is again the whole vocabulary. Two phases have now wanted the same member.
|
||||
|
||||
## 22. Phase 8 as built — the player's own half, on two screens, 2026-09-22
|
||||
|
||||
**Walked end to end against a live rig, and the rig is why this phase reads differently from the
|
||||
three before it.** The sidecar now runs inside the game container, so for the first time since phase
|
||||
5 the website talked to a real plugin on a real Oxide server rather than to a stand-in — including
|
||||
the permission push, which landed a group membership in Oxide's own store and reported
|
||||
`unresolved` for a name nothing had registered.
|
||||
|
||||
Two repositories and this document: [`Module-Rust#10`][mr] (the website half, into `edge`) and
|
||||
[`Android-app#48`][aa] (M15, into `edge`). Core needed no change — the third Rust phase running.
|
||||
|
||||
### 22.0 The three decisions this phase needed
|
||||
|
||||
| | Decision |
|
||||
|---|---|
|
||||
| **D39** | **The app's permission half is player-facing and read-only, and the website grows the route for it.** Phase 7's whole surface is `requireRole('admin')`, and the app has no admin user-detail screen to port it into; porting the authoring page to a phone was considered and rejected (it writes into a running game from a device that is easy to mis-tap, and M14 already excluded the Rust admin surface as configuration the app consumes rather than edits). So the phase adds one self-scoped read instead — and renders it on the **website** as well, so the app never has a surface the website lacks. |
|
||||
| **D40** | **It mirrors `module-uo`'s player surface, not a shape of its own.** One drawer row under the player group, with the code card at the top of the screen it leads to — exactly `CharactersScreen`. A tab under one server was rejected because a link is fleet-wide, and a section inside core's own Account screen was rejected because the app has no slot mechanism and the module's data would be hard-wired into a core screen. |
|
||||
| **D41** | **The row hangs on `rust`, matching `module-uo`'s single `shard`.** The module declares a surface word per feature and `identity` is one of them, but D16's rule stands: a capability answers *is the module there*, and core flattens every module's capabilities into one list. Requiring `rust` **and** `identity` was considered — it would refuse the screen on a build predating phase 6 — and rejected as a second meaning for a word that has one. |
|
||||
|
||||
### 22.1 What the player is told, and what they are not
|
||||
|
||||
`GET /player/rust/permissions` is a different shape from the admin read rather than a filtered one,
|
||||
and the three differences are the phase:
|
||||
|
||||
1. **The scope is resolved on the server.** A client handed `scope: "*"` would have to know what the
|
||||
fleet is to say anything, and then `inScope` exists twice. Each entry arrives carrying the
|
||||
servers it reaches, each already marked.
|
||||
2. **`live` is the pushed ledger, never the authored row.** Phase 7 is careful never to record a
|
||||
push that silently did nothing, so *waiting* here means waiting — and the alternative is the site
|
||||
claiming to have given something it has not.
|
||||
3. **Nothing says why it is waiting.** An offline server, a permission no loaded plugin registered
|
||||
and a store that has never seen the account are one state on this screen. Telling them apart is
|
||||
an operator's diagnosis and an inventory of what is installed on a host.
|
||||
|
||||
**An entitlement that reaches nobody still lists**, and both surfaces say so: authored against the
|
||||
website account, it exists before a Steam id does. Hiding it until one turns up is the defect the
|
||||
admin user page shipped in phase 7 (§20.5) — the same mistake, one tier along, caught before it
|
||||
shipped this time because the rule was already written down.
|
||||
|
||||
**The honest limit, named rather than designed around:** a *rank* can be live while every permission
|
||||
it carries resolves nowhere. The rig proved it — `vip` was created in Oxide's store and the
|
||||
membership landed, while `kits.vip` came back `unresolved` because Kits is not installed there. The
|
||||
player is told the rank reached the game, which is true and is what the site gave them; whether a
|
||||
server's plugins understand the names inside it is on the admin screen, where the warning already
|
||||
is.
|
||||
|
||||
### 22.2 The refusals, on a phone
|
||||
|
||||
The app's convention since M1 is that a refusal is chosen by **status** and rendered from a string
|
||||
resource — the website's sentence is never displayed, because the app is localized and the website
|
||||
is not. That convention holds here and it costs something worth writing down: the module
|
||||
distinguishes *three* 503s (a server it could not reach, a fleet that is all down, a site with no
|
||||
servers configured at all) by sentence, and the app has one string for the status. It is written to
|
||||
be true of all three, and it does **not** say "get a new code" — a player told that would go back to
|
||||
the same unreachable server for another one.
|
||||
|
||||
The four that do differ by status keep four different pieces of advice: 400 a spent code, 409 a
|
||||
Steam account another website account holds, 429 the limiter, 503 a server that could not be
|
||||
reached.
|
||||
|
||||
### 22.3 What the walk proved, and on what
|
||||
|
||||
- **The website half** in a browser, against the live rig: the section rendering with zero linked
|
||||
accounts, a rank marked *has it* and a grant marked *waiting* on the same screen, the note that
|
||||
explains why, and the refusal path.
|
||||
- **The app half** on an emulator against the same core: the row absent when signed out and absent
|
||||
on a UO site, present for a signed-in player; both reads; a refused code rendering **beside the
|
||||
button**; the marks; and the release.
|
||||
- **The push itself against a real Oxide store**, which is new. `perm.sync` created the group,
|
||||
applied the membership, and reported `unresolved: ["kits.vip", "zonemanager.admin"]` — the second
|
||||
of those a genuine finding about the rig rather than the code: ZoneManager registers
|
||||
`zonemanager.ignoreflag.*` and friends, and **not** `zonemanager.admin`. A permission name that
|
||||
looks obvious is still a name some plugin has to have registered.
|
||||
|
||||
**The acceptance line needs a person in game.** Everything above used a link row written directly
|
||||
into the rig's database, because a `/link` code reaches a player and nobody else. The three minutes
|
||||
that close it are in [`PLAYER_WALK.md`](../../rust-link/PLAYER_WALK.md).
|
||||
|
||||
### 22.4 Smaller things worth keeping
|
||||
|
||||
- **`MenuAccess.PLAYER` is `isPlayer || isStaff`**, and that is right here: `/player/rust/*` is
|
||||
`requireAuth` with no role above it, and staff play the game too. The app's own gating test now
|
||||
asks *which* module a row on a module path declares, not merely whether it declares one — a
|
||||
second game under `player/` is a new way for that test to have been passing for the wrong reason.
|
||||
- **The website's pill carries the word, not only the dot.** A filled circle beside a hollow one is
|
||||
the whole difference between "you have this in game" and "you do not yet". Found by looking at it.
|
||||
- **The app's row now says when and where an account was linked**, which the website's always did.
|
||||
Which server minted the code is not part of the identity — a link is fleet-wide — but it is where
|
||||
a support conversation starts.
|
||||
- **`/player/rust` has no app deep link**, deliberately: `module-uo`'s player screens have none
|
||||
either, and the app's web-path table is the *public* nav's. Phase 10 is where it will matter, when
|
||||
a notification about an entitlement wants somewhere to land.
|
||||
- **A rig timestamp written by a different connection is five hours out.** The `DATETIME` columns
|
||||
here are naive, and they round-trip correctly only through the connection that wrote them —
|
||||
inserting a stand-in row with `docker exec mariadb` made the site read it as the future. Not a
|
||||
product defect; a rig recipe.
|
||||
|
||||
---
|
||||
|
||||
[aa]: https://gitea.whitlocktech.com/RunicGateway/Android-app
|
||||
|
||||
---
|
||||
|
||||
[rl]: https://gitea.whitlocktech.com/RunicGateway/Rust-Link
|
||||
|
||||
Reference in New Issue
Block a user