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:
2026-09-22 20:35:46 -05:00
parent 15d64b28fe
commit 11b8965aa7
4 changed files with 346 additions and 3 deletions

View File

@@ -1429,6 +1429,88 @@ push, and Play (M6M8) follow the designed app.
*configuration*, which the app consumes and does not edit. Identity and permissions are legs B
and C (phases 8 and 11), and the map is leg D.
16. **M15 — the player's own Rust account** (post-v1; built 2026-09-22). `module-rust` phase 8, and
R10's leg B: identity (phase 6) and the half of site-owned permissions (phase 7) a player is
allowed to see. Every route existed and answered before a line of Kotlin was written except one,
`GET /player/rust/permissions`, which this phase added on the website for exactly this screen.
**Design of record: [`../modules/rust/PLAN.md`](../modules/rust/PLAN.md)**, §22 for the phase as
built and its three decisions. The contract is normative there; this entry records what the app
does about it.
#### It is `CharactersScreen` for a different game, deliberately
The org lead's instruction was to mirror `module-uo`, and the mirror is exact: **one drawer row
under the player group**, with the code card at the top of the screen it opens — the same shape
UO has had since M4, where the `[link` card sits above the character rosters. Two alternatives
were rejected for reasons the mirror makes obvious: a tab under one Rust server (a link is
**fleet-wide** — one Steam account is one person on every server, while stats are per server and
per wipe), and a section inside core's own Account screen (the app has no slot mechanism, so the
module's data would be hard-wired into a core screen).
| Screen | Route | Reads |
| --- | --- | --- |
| **My Rust account** | `player/rust` | `GET /player/rust/links`, `POST /player/rust/link`, `DELETE …/links/{steamId}`, `GET /player/rust/permissions` |
#### The gate is `rust`, and `PLAYER` means staff too
`module-uo`'s five shard rows all hang on `shard`, and this hangs on `rust` for the same reason
D16 gave: a capability answers *is the module there*, and core flattens every started module's
capabilities into one list, so a surface word like `identity` is not something a row may hang
on. `MenuAccess.PLAYER` is `isPlayer || isStaff`, which is right here — `/player/rust/*` is
`requireAuth` with no role above it, and staff play the game as well.
#### Two reads, and neither blocks the other
The accounts and the entitlements load separately and fail separately. That is not tidiness: an
entitlement is authored against the **website** account, so it exists before a Steam id does, and
the person who has just been given something and has not linked yet is exactly the one who needs
to see both halves at once. The screen says so in as many words when nothing is linked.
**The app does no scope arithmetic.** `*` never reaches a screen: each entry arrives carrying the
servers its scope reaches, each already marked *has it* or *waiting*, because a second
implementation of that rule is a second thing to keep true.
#### The refusals stay four pieces of advice
A refusal is chosen by **status** and rendered from a string resource, the convention every
earlier milestone follows (the app is localized; the website's sentence is not). 400 is a spent
code, 409 is a Steam account another website account holds — released with `/unlink` in game,
never moved silently — 429 is the server's limiter, and 503 is a server that could not be
reached. The last of those may **not** say "get a new code": the code is still good, and a player
told otherwise goes back to the same unreachable server for another one.
The known cost, written down rather than discovered later: the website distinguishes three 503s
by sentence (one server unreachable, the whole fleet down, no servers configured at all) and the
app has one string for the status, written to be true of all three.
#### Verified
The app suite (**657 tests, 0 failures**), `lintDebug`, `assembleDebug`, and an emulator walk
against a core with the module installed and a **live Oxide rig** behind it — the first Rust leg
where the backend was talking to a real game server rather than a stand-in.
Walked directly: the row absent signed-out and absent on a UO site, present for a signed-in
player; both reads; a code the live plugin genuinely refused, with its advice rendering **beside
the button** rather than at the top of a long form; a rank marked *has it* and a grant marked
*waiting* on the same screen, which is what the pushed ledger actually said; and the release.
#### The walk found one thing the suite did not
**The row said who, and not when or where.** The website's row has always read "linked just now
on rust-oxide"; the app's carried the name and the Steam id and stopped. Neither fact is part of
the identity — a link is fleet-wide — but which server minted the code is where a support
conversation starts, and the module's own schema says so in a comment.
- **Excluded**, in the same class as M14's exclusions: the Rust **admin** permission surface.
Authoring grants, groups and drift is `requireRole('admin')` on the website, the app has no
admin user-detail screen to put it in, and it writes into a running game — the phone is where
you read what you hold, not where you decide what somebody else holds.
- **No deep link yet.** `/player/rust` is not in the app's web-path table, deliberately:
`module-uo`'s player screens are not either, and that table is built from the *public* nav.
Phase 10 is when it will matter, because a notification about an entitlement will want
somewhere to land.
### Deferred (not a milestone)
- **Platform Teams in the app** — **deferred 2026-08-17, no app work scheduled.** The website is