diff --git a/modules/rust/PLAN.md b/modules/rust/PLAN.md index f33f02b..d20b79f 100644 --- a/modules/rust/PLAN.md +++ b/modules/rust/PLAN.md @@ -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/.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/.json` per +plugin. Plugins nest — `oxide/config//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 `/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 |