Committed by sage for rocko (no credential). Candidate manifest 7dc571db, packet manifest 0bc9a870, base 43d2328f.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
6.8 KiB
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 URLand 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._@+-].
- business: serve's ID pattern,
- None of the sets allows a space, newline,
%,$, quote, backslash,&or|. That means neither systemd nor thesedrender can read a value as anything other than the value. - Every refusal exits 1 and writes nothing. A refused
installdoesn't create the unit directory. ExecStart=@NODE@ @REPO@/packages/webui/src/cli.mjs serve @FLAGS@. NODE iscommand -v nodeat render time, kept as given (here/usr/bin/node). systemd doesn't look up an ExecStart command inEnvironment=PATH, so a barenodewould not be found the way the brief's wording suggests. The REPO and node paths are held to[A-Za-z0-9/._+-].WorkingDirectoryandPATHare 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 areRestart=on-failure,RestartPreventExitStatus=2,KillSignal=SIGTERM,TimeoutStopSec=30,NoNewPrivileges=yesandWantedBy=default.target. There is no otherEnvironment=line and noMOSAIC_*,CLAUDE*,CODEX*orPI_*variable.installprints thesystemctl --user enable --now mosaic-webuiline 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).uninstallrefuses whilelist-unitsshows the unit active, activating or deactivating, unless--no-reloadis given, as in bus-service.statusprints is-enabled, is-active andshow, then reads--portfrom the installed unit's ExecStart and printsGET http://127.0.0.1:<port>/healthz: the status code and body, orunreachable (<code>). It exits 0 either way, as bus-service status does.
Points for the reviewer
- Exit 2 isn't retried.
serveexits 2 for usage, config and listen errors, EADDRINUSE included (serve.mjslines 149–150 reject,cli.mjssets exitCode 2). WithRestartPreventExitStatus=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. - The preview flags need row 74. Until row 74 lands, a unit rendered with
the preview set makes
serverefuse with exit 2 (unknown option) and not restart. Rendering and installing are unaffected. - 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 verifyon the rendered unit exits 0. - 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
MARKERSthat the test parses frompackages/bus/src/human.mjs, and its only Environment key is PATH; - install into
--dirwith--no-reloadwrites the file whole and identical torender, 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
systemctlon PATH, then removes the unit once it is inactive.
I added three tests beyond the brief:
- install without
--no-reloadcalls onlydaemon-reload; - status against an in-process
/healthzserver on port 0, then with the server down, then with no unit installed; - usage exits 4, including the
start,stop,enableandrestartverbs.
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.