7.3 KiB
Console runs from a pinned copy (2026-10-12)
Status: written by Sage, lead. It follows docs/plans/BRIEF-TEMPLATE.md
and replaces row 77's brief in
docs/plans/2026-10-11_console-service-followups.md. Mos asked for one
row that ends in one reinstall (sage-0089): Vikunja #146 (a pinned copy
with a deliberate update step), #147 (the uid from lead decision 101) and
#155 (row 77's brief). Lead decision 112 records the choices below.
Console pinned copy, update step and service hardening
Problem
The Console unit runs node packages/webui/src/cli.mjs serve straight
from the canonical checkout: scripts/webui-service.sh:35 sets REPO from
the script's own location, and the template puts it in WorkingDirectory
and ExecStart (packages/webui/systemd/mosaic-webui.service.in:19,24).
Every landing that touches the code the Console loads is a live change
the next time Node reads a file or the unit restarts (lead decision 109).
The Console needs to run from a copy fixed at one landed commit, changed
only by a step someone takes on purpose.
Two things in the code shape the fix:
- The queue view runs
packages/webui/src/queue-read.mjsas a child with its cwd at REPO (packages/webui/src/reads.mjs:62-66). The queue store refuses code outside the canonical root (packages/queue/src/store.mjs:85) and refuses linked worktrees (store.mjs:70). So a copy can't read the queue by itself, and a worktree can't serve as the copy. - The rest of the Console loads relative to its own files: the bus
child (
packages/webui/src/bus.mjs:31), the config check (packages/cli/src/config.mjs:15,22), the roles directory (config.mjs:36) andRELEASE(reads.mjs:198). Those can all come from the copy.
Row 75's review left five gaps (Filbert, comment 27430 on #1569), N1 to N5:
- N1:
installdoesn't refuse while the unit is active, so the unit's nextRestart=on-failureruns the new ExecStart without anyone starting it. - N2: an install from a worktree points the unit at the worktree, and no doc says to install from the canonical checkout.
- N3:
uninstall --no-reloadskips the active check while the docs say uninstall always refuses while active. - N4: if
systemctl --user list-unitsfails, the script exits 1 with no message. - N5: the
--allow-uservalue has no check that it starts with a letter or digit.
Decision 101 switches the unit from X-authentik-username to
X-authentik-uid once the uid is known, and only for a value matching
^[0-9a-f]{64}$. Today nothing in the script holds that rule. #147's
title says "a dedicated uid"; decision 101 means the Authentik header, not
a separate Unix user (decision 112).
Owner and reviewer
Owner: Rocko. Reviewers: Darkwing and Filbert. Rocko has no Gitea token; Sage runs the review request as jarvis.
Files owned
scripts/webui-service.shpackages/webui/systemd/mosaic-webui.service.inpackages/webui/src/cli.mjs, the new--checkoutflag and passing it toconsoleReadspackages/webui/src/reads.mjs, only so the queue reader takes its own rootpackages/webui/tests/service.test.mjsand new tests inpackages/webui/tests/packages/webui/README.md, the service sectiondocs/TOOLS.md, the script's entry onlyagents/rocko/work/queue-77/
What ships
The copy:
- A new verb,
scripts/webui-service.sh copy COMMIT [--copies DIR]. It runs only from the canonical checkout (the repository root has a.gitdirectory, not a worktree's.gitfile). It refuses a COMMIT that isn't reachable fromrefs/heads/refactor. It resolves COMMIT to the full 40-character id. - It writes
git archiveof the paths the Console loads intoDIR/<full id>/, by default${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-console/<full id>/. The path list lives in the script with a comment. It writes into a temp directory beside the target and renames it, then removes write permission from every file in the copy. A copy that already exists is left as it is, and the verb printsexists:and its path. - A test makes a copy from a fixture repository, starts
cli.mjs servefrom the copy with a stand-in socket, and gets/healthz200. So a path missing from the list fails the suite.
The unit:
installandrendertake--copy ID, which names an existing copy by its full id.installrefuses without it. The template's@REPO@becomes the copy's path, soWorkingDirectoryandExecStartpoint at the copy.servetakes--checkout PATH, the canonical checkout, and the unit passes the checkout the script runs from. The queue read runs the checkout'squeue-read.mjswith its cwd there, as today.RELEASE, the roles directory, the config check and the bus child come from the copy. Without--checkout, serve behaves as it does now. A test shows each of the two roots in use.statusprints the copy id the unit runs, read from itsWorkingDirectory, and whetherrefactorin the checkout points at another commit. It never changes anything.
The update step, documented in TOOLS.md and the README: copy, then
stop, install --copy ID with the same flags, then start. Rollback is
the same steps with the earlier id; copies are kept until someone removes
them. The stop and start stay Jason's or Mos's (decisions 94, 102 and
103).
N1 to N5:
- N1:
installrefuses while the unit is active, activating or deactivating, and its message says Jason or Mos stops it first. A test uses a stand-insystemctl. - N2:
installandcopyrefuse when the script isn't in the canonical checkout.--allow-worktreeoverrides the refusal for tests only, and the help text says so. - N3:
uninstallchecks for an active unit even with--no-reload, or the docs say exactly when it doesn't. Rocko picks one and a test shows it. - N4: a failed state read prints one line to stderr naming the failed
systemctlcall. - N5:
--allow-userand the other values must start with a letter or digit. Tests cover the two leading-character checks that row 75's mutants left untested.
The uid rule from decision 101:
- When
--auth-headerisX-authentik-uidin any letter case,--allow-usermust match^[0-9a-f]{64}$, andrenderandinstallrefuse anything else with a message naming the rule. Tests cover a good value, 63 and 65 characters, upper-case hex and a username.
The template's header comment (lines 6-9) still says only Jason starts the unit; it gets decision 102's wording, Jason or Mos (Vikunja #159 item 1).
Out of scope
Starting, stopping, enabling or restarting the unit. Reading the uid
itself: ops-01 reads it without writing anything (decision 112), and the
reinstall that uses it is Mos's. Pruning old copies. A health-gated
activate and rollback like scripts/release.sh, which is D1 for the
Console as a whole. Any change to packages/queue. The bus service
script.
Gate
Darkwing and Filbert approve on #1571, each naming the candidate
manifest. The webui node suite and every scripts/test-*.sh pass on
Sage's gate rerun. Before the landing Sage notifies Mos (decision 109);
after it, Sage sends Mos the commit, scripts/webui-service.sh status
and /healthz. The live unit changes only at Mos's one reinstall:
copy at the landed commit, stop, install --copy ID with the uid
header and value, start, then status, /healthz and the anonymous 302.