docs(modules): the module-rust plan — 18 decisions of record and a 21-phase schedule #249

Merged
whitlocktech merged 10 commits from docs/rust-module-plan into main 2026-09-15 17:41:17 +00:00
Showing only changes of commit 6392f39512 - Show all commits

View File

@@ -385,7 +385,7 @@ under a prefix. A noun from our own domain that equals the module id cannot coll
**Decided 2026-09-15 (org lead).** An admin edits any loaded plugin's configuration from the website
and it reloads automatically. Two tiers:
- **Base:** the site reads `oxide/config/<Plugin>.json` and **generates a form from the values
- **Base:** the site walks `oxide/config/` **recursively** and **generates a form from the values
themselves** — a boolean becomes a toggle, a number a numeric field, a string a text box, an array
a list, a nested object a group. It is derived at read time, so **it works for whatever plugins
happen to be installed**, including ones added or removed after we shipped.
@@ -400,6 +400,43 @@ on demand** ([kit][kit] ch. 3 §2a). Do not build it on the permission mirror.
`OnPluginUnloaded` are real hooks** in the Server category, so *whether a reload actually succeeded
is observable* rather than assumed.
#### Discovery is a recursive walk, and it stops at `oxide/config/`
**Amended 2026-09-15 (org lead).** Configuration is **not** one flat `oxide/config/<Plugin>.json` per
plugin. Plugins nest — `oxide/config/<Mod>/whatever.json`, and deeper — and one plugin may own
several files. So discovery is a **recursive walk** of the config tree, and the UI groups by plugin
rather than assuming one file each.
Four things follow, and the first is a boundary rather than a detail.
**`oxide/data/` is not the settings surface, and must not be walked into.** `DataFileSystem` writes
to `oxide/data/`, and that is **live state, not configuration**. The base set makes the point by
itself: Kits keeps `Kits/kits_data.json` and `Kits/player_data.json`, ZoneManager keeps
`ZoneManager/zone_data.json`, Clans keeps `clan_data.json` (and a legacy `clans_data.json` beside it,
which is also a reminder that these names are not stable). Editing those from a web form edits
**players' kit cooldowns and the live zone definitions**, a running plugin overwrites the change on
its next save, and `oxide.reload` does not make most plugins safely re-read them. It is a different
problem with a different answer and it is deliberately out of scope. If a plugin's *settings* genuinely
live under `data/`, that is a per-plugin exception someone opts into knowingly, never something the
walk discovers on its own.
**The reload target cannot be inferred from the path.** `oxide/config/Foo/bar.json` may belong to
plugin `Foo`, or to something else entirely — the folder name is convention, not contract. So the
reload target is an **explicit field with the folder name as its default guess**, confirmable by the
admin. Infer it silently and the failure is the nastiest kind available here: we reload the wrong
plugin, observe `OnPluginLoaded` for *it*, and report success while the plugin that was actually
edited never re-read anything.
**A relative path from a web form is now a path-traversal surface.** Canonicalise the resolved path,
assert it is genuinely under the config root, reject absolute paths, and reject symlinks that resolve
outside. Before this amendment the feature addressed files by plugin name; now it addresses them by
path, and that is exactly the change that introduces the bug class.
**Bound the walk and the file.** A depth limit, a file-count limit and a per-file size cap — a
pathological tree must not be enumerated and a multi-megabyte JSON must not be loaded into a form.
And because one plugin can own several files, **the backup and rollback operate on the whole set a
save touches**, not one file at a time.
#### The trap that would silently corrupt every float
**JavaScript cannot tell `1` from `1.0`, and Oxide configs deserialize into typed C# classes.**
@@ -644,7 +681,7 @@ Each phase ends with its findings written down, as every workstream here does.
| 5 | **Android leg A** (R10). Capability-driven shell from `GET /api/v1/public/modules`, plus the phase-4 screens | Android-app | The app renders a Rust site it has never seen, and a UO site unchanged |
| 6 | **Identity** (R1), and the `admin.users.detail` slot (R13) | 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). Groups and grants authored on the site; full set pushed on connect, deltas after; drift reported | all 3 + docs | A grant made on the website gates a third-party plugin in-game, and survives a wipe |
| 7b | **Mod configuration from the site** (R18). Generated form from the live config values, raw-JSON advanced tier, versioned read/write, auto-reload watched on `OnPluginLoaded`, **automatic rollback** on a failed load, secret redaction, its own permission and an audit trail | 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 |
| 7b | **Mod configuration from the site** (R18). **Recursive** walk of `oxide/config/` (never `oxide/data/`), generated form from the live values, raw-JSON advanced tier, explicit reload target, versioned read/write, auto-reload watched on `OnPluginLoaded`, **automatic rollback** over the whole file set, path-traversal guards, secret redaction, its own permission and an audit trail | 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 |
| 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 |