Committed by sage for rocko (no credential). Candidate manifest 7dc571db, packet manifest 0bc9a870, base 43d2328f.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
116 lines
6.8 KiB
Markdown
116 lines
6.8 KiB
Markdown
# Row 75 (#1569): Console as a user service, candidate packet, round 1
|
||
|
||
Author: Rocko. Reviewer: Filbert. Brief: `docs/plans/2026-10-11_dogfood-cutover.md`,
|
||
section "Console as a user service" (blob 1b124173 at 57f36a96; the rev 501
|
||
re-pin to blob f5d56554 changes only row 76's section). Lead decisions 94 and
|
||
95. Base: `43d2328f` (`base.txt`). At 2026-10-11T22:32Z the checkout HEAD
|
||
`2d04426c` has no change to any candidate path since the base, and
|
||
`build.patch` applies to it cleanly. The packet is uncommitted. I made no
|
||
commits, pushes or Gitea calls, read no token or private binding, and
|
||
started no unit.
|
||
|
||
## Files (`files.txt`, `candidate-manifest.sha256`)
|
||
|
||
| File | What |
|
||
|---|---|
|
||
| `scripts/webui-service.sh` (new, 0755) | `render`, `install [--dir DIR] [--no-reload]`, `uninstall [--dir DIR] [--no-reload]`, `status [--dir DIR]` on the `bus-service.sh` pattern; exit 0 ok, 1 failed, 4 usage |
|
||
| `packages/webui/systemd/mosaic-webui.service.in` (new) | the unit template |
|
||
| `packages/webui/tests/service.test.mjs` (new) | 10 tests |
|
||
| `docs/TOOLS.md` | one new section, "Console service (`scripts/webui-service.sh`)", before the Discord connector section; nothing else touched |
|
||
|
||
No `src/` file and no README. Row 74 owns those.
|
||
|
||
## What it does
|
||
|
||
- The serve flags are `--port N` (default 7332, 1..65535), `--business ID`,
|
||
`--board URL` and the preview set `--public-host NAME --auth-header
|
||
HEADER --allow-user ID`, all three or none.
|
||
- Each value has its own plain character set:
|
||
- business: serve's ID pattern, `[a-z0-9][a-z0-9._-]{0,63}`.
|
||
- board: `http://` followed by `[A-Za-z0-9.:/_-]`. Serve checks the rest itself: a loopback origin with no path.
|
||
- public host: `[A-Za-z0-9.-]` with a leading alphanumeric, so no port and no scheme.
|
||
- header: `[A-Za-z0-9-]`.
|
||
- user: `[A-Za-z0-9._@+-]`.
|
||
- None of the sets allows a space, newline, `%`, `$`, quote, backslash, `&` or `|`. That means neither systemd nor the `sed` render can read a value as anything other than the value.
|
||
- Every refusal exits 1 and writes nothing. A refused `install` doesn't create the unit directory.
|
||
- `ExecStart=@NODE@ @REPO@/packages/webui/src/cli.mjs serve @FLAGS@`. NODE is
|
||
`command -v node` at render time, kept as given (here `/usr/bin/node`).
|
||
systemd doesn't look up an ExecStart command in `Environment=PATH`, so a bare
|
||
`node` would not be found the way the brief's wording suggests. The REPO
|
||
and node paths are held to `[A-Za-z0-9/._+-]`.
|
||
- `WorkingDirectory` and `PATH` are filled in as the bus unit fills them:
|
||
`/usr/local/bin:/usr/bin:/bin`, with node's directory prepended if it isn't
|
||
already there. The other settings are `Restart=on-failure`,
|
||
`RestartPreventExitStatus=2`, `KillSignal=SIGTERM`, `TimeoutStopSec=30`,
|
||
`NoNewPrivileges=yes` and `WantedBy=default.target`. There is no other
|
||
`Environment=` line and no `MOSAIC_*`, `CLAUDE*`, `CODEX*` or `PI_*` variable.
|
||
- `install` prints the `systemctl --user enable --now mosaic-webui` line for
|
||
the human to run. The script has no start, stop, enable or restart verb; the
|
||
header and TOOLS.md say why (lead decisions 62 and 94).
|
||
- `uninstall` refuses while `list-units` shows the unit active, activating or
|
||
deactivating, unless `--no-reload` is given, as in bus-service.
|
||
- `status` prints is-enabled, is-active and `show`, then reads `--port` from the
|
||
installed unit's ExecStart and prints `GET http://127.0.0.1:<port>/healthz`:
|
||
the status code and body, or `unreachable (<code>)`. It exits 0 either way,
|
||
as bus-service status does.
|
||
|
||
## Points for the reviewer
|
||
|
||
1. **Exit 2 isn't retried.** `serve` exits 2 for usage, config and listen errors,
|
||
EADDRINUSE included (`serve.mjs` lines 149–150 reject, `cli.mjs` sets
|
||
exitCode 2). With `RestartPreventExitStatus=2`, a port conflict stops the
|
||
unit rather than looping. I chose that on purpose, and the template comment
|
||
says so. Serve has no separate config exit code: a missing system config
|
||
still serves, and the bus routes answer not-configured.
|
||
2. **The preview flags need row 74.** Until row 74 lands, a unit rendered with
|
||
the preview set makes `serve` refuse with exit 2 (`unknown option`) and not
|
||
restart. Rendering and installing are unaffected.
|
||
3. **Serve runs under systemd.** It runs as written. The user manager's
|
||
environment (`systemctl --user show-environment`) carries none of the
|
||
human.mjs markers, and the ancestry walk accepts at ppid 1. That is the gap
|
||
lead decisions 62 and 94 name, and it's why only Jason starts the unit. I did
|
||
not start it to prove this; `systemd-analyze --user verify` on the rendered
|
||
unit exits 0.
|
||
4. **An empty value counts as unset.** `--board ""` and `--business ""` are
|
||
treated as unset, the way bus-service treats an empty value. An empty member
|
||
of the preview set counts as missing, so a set with one empty member is
|
||
refused as partial.
|
||
|
||
## Tests (`packages/webui/tests/service.test.mjs`)
|
||
|
||
The brief asks for six things:
|
||
|
||
- render with and without the preview flags;
|
||
- a partial set refused, in all three two-of-three shapes;
|
||
- space, `%`, newline, `$`, quote, backslash, `|` and `&` refused for each value flag, plus bad ports, business ids, boards, hosts and headers;
|
||
- the rendered unit names none of the `MARKERS` that the test parses from `packages/bus/src/human.mjs`, and its only Environment key is PATH;
|
||
- install into `--dir` with `--no-reload` writes the file whole and identical to `render`, at mode 0644, with no temp file left and no systemctl call; a second install reports unchanged; a refused install leaves the installed unit alone;
|
||
- uninstall refuses for active, activating and deactivating through a stand-in `systemctl` on PATH, then removes the unit once it is inactive.
|
||
|
||
I added three tests beyond the brief:
|
||
|
||
- install without `--no-reload` calls only `daemon-reload`;
|
||
- status against an in-process `/healthz` server on port 0, then with the server down, then with no unit installed;
|
||
- usage exits 4, including the `start`, `stop`, `enable` and `restart` verbs.
|
||
|
||
No test reaches the real systemd or spawns `human-cli.mjs`. `XDG_CONFIG_HOME` and `HOME` point into a temp directory.
|
||
|
||
## Gate (`out/gate/`, sequential, scratch worktree at 43d2328f with the patch, `node_modules` linked)
|
||
|
||
| Suite | Result |
|
||
|---|---|
|
||
| webui `node --test 'packages/webui/tests/*.test.mjs'` | 50/50 (service: 10/10) |
|
||
| test-auth | 15 passed, 0 failed |
|
||
| test-conductor | 17 passed, 0 failed |
|
||
| test-config | 24 passed, 0 failed |
|
||
| test-discord | 66 passed, 0 failed (node 178 pass) |
|
||
| test-extension-package | 18 passed, 0 failed |
|
||
| test-foundation | 44 passed, 0 failed |
|
||
| test-queue | 27 passed, 0 failed |
|
||
| test-release | 14 passed, 0 failed |
|
||
| test-task | 98 passed, 0 failed (Docker) |
|
||
|
||
After the gate I removed the `rocko-r75_default` Docker network.
|
||
`out/gate/start.txt` and `out/gate/end.txt` hold the times. shellcheck isn't
|
||
installed on dragon-lin; `bash -n` passes.
|