Phase 3 closed with core at 158 routes and module-uo releasable; phase 4 is
the first phase that is not about the boundary. It answers how a module gets
onto a box an operator owns, and how it comes off again.
Adds MODULE_SYSTEM.md §2.7.2 recording the measured starting state (the
producer side is finished — module-uo's release.yml already publishes the
tarball, the install manifest and SHA256SUMS, and module.json declares
purge.sql — while core has the installed_modules provenance columns and
nothing that fetches, verifies, unpacks or purges), the four org-lead
decisions, the five slices, the unpack threat model and the acceptance table.
The four decisions:
1. Restart is a button. Install, uninstall and re-enable only take effect at
boot; recording a pending change and telling the operator to restart the
container contradicts §2.4's "no shell access to the box", so an admin
action runs the shutdown and exits, and the supervisor the shipped compose
file already declares brings it back.
2. The install source is a pasted manifest URL, not a catalog — a catalog
would make core's release cadence decide which modules exist. Safety is the
declared sha256 plus an https host allowlist, which also stops the endpoint
being an SSRF primitive.
3. Disable dispatches that one module's onShutdown before flipping the guard.
As built, disable makes a module invisible rather than stopped — module-uo
keeps its sidecar socket open and keeps ingesting into shard_* tables —
which is wrong for the case the button exists for. Enable is not the mirror:
there is no onBoot re-dispatch and the hooks were never promised re-entrant,
so enable flips the row and offers the restart.
4. The declarative Docker set is an environment variable, resolved before the
server starts, idempotent and offline-safe: an already-unpacked module at
the declared version is a no-op.
Amends §2.4 (disable is no longer a pure guard flip) and §2.5 (what "admin
selects the module" and "restart" concretely mean). Records re-entrant
lifecycle hooks as a second candidate for a future MODULE_API major bump,
beside the identity-provider gap the rust dry run found.
Co-Authored-By: Claude <noreply@anthropic.com>