From 8a7b099c2d203d9780cbaf94177b7a9ee8b00d08 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Tue, 11 Aug 2026 00:50:17 -0500 Subject: [PATCH] =?UTF-8?q?docs(website):=20Phase=202=20PR=209=20=E2=80=94?= =?UTF-8?q?=20the=20modules=20mount,=20and=20Phase=202=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records the last Phase 2 PR and closes the phase. Amends §2.5: the mount is a bind mount of ./modules, not the named volume the section reached for by analogy with uploads. Hand-placing a module directory is a supported install in that same section, and a named volume routes it through `docker cp` — the least discoverable mechanism Docker offers, for the one install path an operator without the admin panel has. Two things the build settled that the plan had not considered, both silent failures rather than errors: the directory has to be tracked, because Docker recreates a missing bind-mount source as root-owned and the container is uid 1000; and .dockerignore has to exclude it, because COPY . . would otherwise bake a builder's checked-out module into every image — and Docker seeds a fresh named volume from image contents, so it could have surfaced on a deployment that never installed it. MODULE_API §4.1 gains the concrete Compose values and states outright that a missing modules directory is not an error, which the loader has always done and the contract never said. Website side: RunicGateway/website#136. Co-Authored-By: Claude --- website/MODULE_API.md | 8 +++++- website/MODULE_SYSTEM.md | 60 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/website/MODULE_API.md b/website/MODULE_API.md index 72aa5a9..68c1d24 100644 --- a/website/MODULE_API.md +++ b/website/MODULE_API.md @@ -703,7 +703,13 @@ half needs rethinking before Phase 2 builds on it. `app.js` scans `modules/*/module.json` with `fs.readdirSync` at require time and mounts what it finds (`MODULE_SYSTEM.md` §1.12). The database is not consulted. `MODULES_DIR` defaults to -`/modules` and is overridable by env for tests and for the Docker volume mount. +`/modules` and is overridable by env for tests and for the Docker mount. Under Compose it is +set to `/app/modules`, where `./modules` is bind-mounted read-write (`MODULE_SYSTEM.md` §2.5); the +image itself carries that directory empty and owned by the container user, and `.dockerignore` +excludes any local one so a module can never be baked in. + +**A missing modules directory is not an error.** The scan catches and returns, because "no modules +installed" is the normal state of bare core and the loader must not make the mount mandatory to boot. **The trigger is one explicit call, and there is no lazy self-scan** (§7.6). `app.js` calls diff --git a/website/MODULE_SYSTEM.md b/website/MODULE_SYSTEM.md index 82be038..5440d57 100644 --- a/website/MODULE_SYSTEM.md +++ b/website/MODULE_SYSTEM.md @@ -428,8 +428,12 @@ leaves the row `disabled`, which the reset never touches. ### 2.5 Install, uninstall, purge -Modules live on a **mounted volume**, not in the image — the same treatment `uploads` already gets in -`docker-compose.yml`. That is what makes the WordPress model work against a pull-only image. +Modules live on a **mounted volume**, not in the image. That is what makes the WordPress model work +against a pull-only image. *(Settled in Phase 2 PR 9: it is a **bind mount** of `./modules`, not the +named volume this sentence originally reached for by analogy with `uploads` — hand-placing a module +directory is a supported install below, and a named volume would route it through `docker cp`. The +image's copy is excluded by `.dockerignore`, so a module in a builder's working tree can never ship +inside an image; see `modules/README.md` in the website repo.)* **Install:** admin selects the module → bundle downloaded from the module repo's release and verified against its `sha256` → unpacked into `modules//` on the volume → `installed_modules` row written → @@ -511,14 +515,14 @@ too (API §7.2). 7. Client `src/modules/registry.js`, the `window.__rg` shared-dependency global, the chunk's static mount and the `htmlShell` script injection — empty registry, no visible change. 8. `MOD_PATHS` → `roles`-derived (§1.4); the generic feature-provider seam (§1.5). -9. `docker-compose.yml` gains the `modules` volume. +9. `docker-compose.yml` gains the `modules` mount. Exit criterion: no **existing** URL moves and every existing test passes. If Phase 2 changes one URL, it is wrong. PR 6 is the single deliberate exception in the phase and it *adds*: `routes.manifest.json` gains exactly one line, `GET /api/v1/public/modules`, and nothing else in the file moves. Every other PR in Phase 2 produces a zero-line diff. -**Progress: PRs 1-7 done.** +**Progress: complete — all nine PRs landed.** - **PR 1** — `installed_modules` and the state machine, with the stored shape and the boot rules settled in §2.4 above. @@ -678,6 +682,54 @@ fixed enum because the leg set is whatever has been registered. admin can relabel a module row in Admin → Navigation and have it persist and apply — the whole point of merging before the override layer. Zero CSP reports, zero console errors. +- **PR 9** — the mount itself, which closes the phase: `docker-compose.yml` gains `./modules` at + `/app/modules`, the `Dockerfile` creates that directory node-owned, `.dockerignore` keeps any local + one out of the image and `modules/README.md` documents the directory for whoever opens it. + + It is a **bind mount, not the named volume** §2.5 assumed by analogy with `uploads`. Placing a + module directory by hand is a supported install, and a named volume makes that a `docker cp` into a + running container — the one install path an operator without the admin panel has, routed through + the least discoverable mechanism Docker offers. A bind mount makes it `tar -xf … -C ./modules`, and + makes the installed set something an operator can *see*. §2.5 is amended above; nothing else about + install, uninstall or purge changes. + + Two consequences worth stating, because both are silent failures rather than errors. The directory + is **tracked** — via its README, the same shape `brand/` already uses — because Docker recreates a + missing bind-mount source as `root:root`, and the container runs as uid 1000: delete `modules/` from + a checkout and the next install fails on a permission error that names no cause. And the mount is + **read-write**, since §2.5's install unpacks into it from inside the container; deferring that to + Phase 4 would have bought nothing, as a mount-mode change is a redeploy either way. + + `.dockerignore` matters more than it looks. `COPY . .` would otherwise bake whatever module the + builder had checked out into every image — and because Docker seeds a *fresh* named volume from + the image's contents, that module could have appeared on a production deployment that never + installed it. The exclusion is what makes "modules live on a mount, never in the image" true rather + than merely intended. + + Verified against a **running container**, which is the only thing that can check any of the above — + a compose file that parses proves nothing about ownership, and nothing about what the image + contains. The image carries an empty, node-owned `/app/modules` despite a module sitting in the + build context. A module on the mount loads, mounts, runs `onBoot` and reaches `started`; + `/api/v1/public/modules` lists it; its chunk serves from the entry's directory alone, with the + module's own server source and `module.json` both `404`. The [§7.7](MODULE_API.md#77-the-client-half-has-to-be-verified-in-a-browser--the-timing-bug-no-test-could-see) + browser smoke was re-run against the containerised stack rather than a working tree: in Chrome, the + page renders on first paint inside core's `PublicLayout`, drawing React and the UI kit off + `window.__rg`, with its nav row interleaved into core's public nav — under the enforced + `script-src 'self'`, with zero CSP reports and no console errors. Removing the directory by hand and + restarting reconciles the row to `startup_failed`/`require` exactly as §2.4 says, and leaves core + healthy with no script injected and `{"modules":[]}` published. + + **933 server tests** and **160 client tests** pass, both unchanged — this PR ships no application + code. `routes.manifest.json` stays at 230 routes and the OpenAPI spec regenerates byte-identical. + The one source change is a comment: `scripts/routeManifest.js` enumerated the filesystem-conditional + mounts it excludes and had never been told about `/modules`. Its filter is an allowlist, so the + behaviour was always right and only the explanation was stale. + +**Phase 2 is complete.** Core can discover, validate, mount, migrate, boot, publish, serve and +navigate a module it does not contain, on a deployment that builds nothing — and it does all of that +while no existing URL has moved. The exit criterion held: `routes.manifest.json` went 229 → 230 across +the whole phase, and the one added line is PR 6's deliberate `GET /api/v1/public/modules`. + **Phase 3 — Extract `module-uo`.** Moves out of `website/`: the 8 model directories and their 25 tables; the nine UO `utils/` files plus `newsGump.js`; the 13 router/controller files; `scripts/importSpawnAtlas.js` and `db/spawnAtlas.art.json`; `usersShard.controller.js` **minus