docs(website): phase 4 decisions 5-6, slices 1-2, and a rule main currently gets wrong #142

Merged
whitlocktech merged 3 commits from docs/module-phase4-slices into main 2026-08-12 09:11:29 +00:00

3 Commits

Author SHA1 Message Date
f753dfcff8 docs(website): record slice 2, and the three defects a browser found
The Modules screen as built (website#143), and the three things standing it
up against a live server exposed. Two of them are older than this phase and
neither was reachable from a test.

- A fresh install over a row the previous boot left `startup_failed` rendered
  the old failure - "module directory not present on the volume", one second
  after the files were written there - and, because that branch is not
  pending, suppressed the restart banner the install had just told the
  operator to use. Fixed by a derivation rather than a special case: the
  loader scans once at require time, so a module on the volume with no live
  record arrived after that scan and anything the row says predates it.

- The boot refresh had been nulling every install's provenance. source and
  sha256 exist for this screen and never survived a restart, because
  lifecycle.boot() re-records with neither and the upsert assigned both
  unconditionally. It could not have been found before Phase 4 wrote the first
  non-null value those columns ever had - and the model's test fake
  reproduced the defect faithfully, assigning exactly like the SQL.

- The restart killed the server outright on Windows. process.kill(pid,
  'SIGTERM') reaches the graceful handler on Linux and is unconditional
  termination where POSIX signals do not exist, so a Windows host got no
  module onShutdown, no pool close and no log flush. process.emit('SIGTERM')
  reaches the same listener everywhere. The test had stubbed process.kill and
  asserted the call - precisely the call whose meaning differs by platform.

That last one generalises: deployment is Linux containers and would never
have shown it. A smoke that only runs where the code ships cannot find a class
of defect that only bites the people developing it.

Also documents the COALESCE in BACKEND_DESIGN's installed_modules section.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 03:50:46 -05:00
88b6257333 docs(website): record slice 1, and correct two rules it disproved
The install service and admin API as built (website#142), plus the two
things §2.7.2 got wrong before there was any code to check them against.

The top-level directory rule was checking against nothing real. module-uo's
release workflow packs module-uo-<version>/, not uo/, so "reject a top-level
directory whose name is not the manifest's id" would have refused every
bundle that exists. That level is stripped instead - its name belongs to
whoever published the bundle, the directory it lands in has to be the id the
loader scans for - and what replaces the check is stronger: the UNPACKED
module.json must agree with the install manifest about id and version.

And tar has to be pinned forward rather than merely depended on. Installing
it gets 6.x, which npm audit reports as critical, and the advisory list reads
as this feature's own threat model: hardlink traversal via drive-relative
linkpath, symlink poisoning, hardlink escape through a symlink chain, PAX
size override on GNU long-name headers, decompression DoS. Refusing symlink
and hardlink entry types outright is what takes the extractor off most of
that list rather than depending on the library to contain them.

Also records the measurement behind the two-pass unpack, which was assumed in
the plan and is now known: node-tar DOES reject an escaping member, but late.
An archive whose fourth member escapes throws and leaves the first three on
disk - and the loader only asks whether module.json is present, so a
half-unpacked bundle is a module as far as the next boot is concerned.

Refreshes api-route-inventory.json (158 -> 166 public) and the route count in
BACKEND_DESIGN, and documents module_source_hosts beside the other seeded
settings keys - bootstrapped from the environment, owned by the database.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 03:12:25 -05:00
6bca9adf49 docs(website): two more phase 4 decisions, one of them a correction
Both came out of writing slice 1 against §2.5 rather than from planning, and
one of them means §2.5 was wrong rather than merely vague.

5. Purge is offered inside the uninstall flow, because it cannot be offered
   after it. §2.5 promised a disabled row an operator could purge later — but
   purge.sql is a file INSIDE the module directory that uninstall deletes, so
   "later" would have left a Purge button with nothing to run. The destructive
   choice now sits at the one moment the file is still there: an opt-in
   checkbox in the uninstall dialog, beside the standalone Purge action on a
   module that is still installed. "Never bundled into uninstall" becomes
   "never implied by uninstall". The cost is stated rather than hidden — an
   operator who does not tick it keeps their tables, and reinstalling is how
   they get the ability to drop them back.

6. The host allowlist bootstraps from MODULE_SOURCE_HOSTS and then lives in
   the database, admin-editable, with the variable no longer overriding it —
   the shape core already uses for seeded settings. The argument for keeping
   it in the environment does not survive contact with what this screen is: an
   admin who can install a module already has code execution in the process,
   so an admin who can name a host has gained nothing they did not have.

Also renames the subsection to six decisions and updates acceptance criterion
2 to name the checkbox.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 02:43:26 -05:00