webui: Console pinned copy, update step and service hardening (#1571)

Row 77, round 3 (round 2 rebased onto row 95). Rocko built it. This
covers row 75's N1-N5 and Vikunja 146 and 147. `install --copy ID`
refuses unless every ExecStart path of the bus unit, read from
`systemctl --user show` so drop-ins count, lies inside that copy. It
also refuses when the unit isn't loaded, no unit is loaded, ExecStart
has no path, or systemctl fails. COPY_PATHS gains packages/tasks/src and
packages/tasks/fields.json. Every copy directory is 0555. TOOLS.md and
the webui README give the removal command and the rollback floor.

Candidate manifest d9880f4c92d1d976027562a03cc5dd07512954e86f49f1da0ebb34e642a955f5
(10 paths, 8 applied by the patch, 2 packet files already committed),
base 2e26fbe1, packet agents/rocko/work/queue-77/r2b/. Approvals on round
3: Darkwing 27979 and Filbert 27985 (Filbert round 2: 27898). Sage's
gate rerun 22:32:51Z-22:35:12Z on 459c4fc6 plus the patch (row 92
included), manifest 10/10: webui 101, control-board 124, and every
scripts/test-*.sh exited 0 (test-release 4, Docker cases skipped at a
dead socket).

Live at the next Console install and restart (decision 109). Mos makes
that call.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-12 17:35:26 -05:00
co-authored by Claude Opus 5.5
parent cca5f23eaf
commit 0d10cf0798
8 changed files with 921 additions and 94 deletions
+65 -12
View File
@@ -241,8 +241,9 @@ Exit codes: `0` ok · `1` failed or outcome unknown · `2` invalid input ·
## Console service (`scripts/webui-service.sh`)
```bash
scripts/webui-service.sh render [FLAGS] # print the unit
scripts/webui-service.sh install [--dir DIR] [--no-reload] [FLAGS]
scripts/webui-service.sh copy COMMIT [--copies DIR] # make a pinned copy
scripts/webui-service.sh render [--copy ID [--copies DIR]] [FLAGS] # print the unit
scripts/webui-service.sh install --copy ID [--copies DIR] [--dir DIR] [--no-reload] [FLAGS]
scripts/webui-service.sh uninstall [--dir DIR] [--no-reload]
scripts/webui-service.sh status [--dir DIR]
# FLAGS: [--port N] [--business ID] [--board URL]
@@ -257,19 +258,71 @@ preview set and is refused otherwise (exit 1, nothing written); ExecStart
then ends with `--send`, and the Console needs the system config at start
(`--send needs the bus configured`, exit 2). Without `--send` the unit is
read-only. The template is
`packages/webui/systemd/mosaic-webui.service.in`. `install` writes a temp
file, renames it and runs `systemctl --user daemon-reload`; `uninstall`
refuses while the unit is active; `status` prints the unit state, whether
the installed unit sends, and `GET /healthz` on the installed port. Each
value is held to a plain
character set the unit can carry (no space, `%`, `$`, quote, backslash,
`&` or `|`) and refused otherwise. The unit sets no `MOSAIC_*`, `CLAUDE*`,
`CODEX*` or `PI_*` variable. Serve's exit 2 (usage, config, or a port
already in use) is not restarted.
`packages/webui/systemd/mosaic-webui.service.in`.
The unit runs a pinned copy, not the working checkout (row 77, #1571).
`copy COMMIT` resolves COMMIT to its full id, refuses one not reachable
from `refs/heads/refactor`, and writes `git archive` of the paths the
Console loads into `DIR/<id>/`, by default
`${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-console/<id>/`. It writes a
temp directory beside the target, makes every file and directory read-only
(so a writer that replaces by rename can't change it either), then renames
it, so the copy's path is always whole. A copy that exists is left alone
(`exists:`). `install --copy ID` and `render --copy ID` point
`WorkingDirectory` and ExecStart at the copy and pass serve
`--checkout <this checkout>`, where the queue read still runs; `RELEASE`,
the roles, the config check and the bus child come from the copy. The bus
child is the human transport, `packages/bus/src/human-cli.mjs`, and the bus
checks that path against its own, so the bus must run from the same copy
(below). `install` refuses without `--copy`, or when the copy is missing.
`render` without `--copy` prints the unit for the checkout, as before.
`install --copy ID` also refuses unless the bus runs from the same copy
(round 2, #225). The bus accepts a human proof only from its own
`packages/bus/src/human-cli.mjs`, so a Console on a copy whose bus runs
from the checkout would fail every bus call with `human-required`. With
`--business ID`, install reads `[email protected]`; without it, every
loaded `mosaic-bus@` unit. It reads the effective ExecStart from
`systemctl --user show -p LoadState -p ExecStart`, so a drop-in counts,
and every ExecStart path must lie inside the copy. It refuses, naming both
paths, when one doesn't; when the unit is not installed or none is loaded;
when ExecStart has no path; and when the systemctl call fails. The check
runs before the unit state read and changes nothing.
`install` writes a temp file, renames it and runs
`systemctl --user daemon-reload`. `install` and `uninstall` refuse while the
unit is in any state but inactive or failed (`systemctl --user list-units`),
and say Jason or Mos stops it first; `--no-reload` skips only the
daemon-reload, never that check. If the state read itself fails they refuse
with one line naming the call, and change nothing. `copy` and `install`
refuse outside the canonical checkout (a `.git` directory, not a worktree's
`.git` file); `--allow-worktree` overrides that for the tests only. `status`
prints the unit state, the copy id it runs, whether `refactor` points at
that commit or another, whether the installed unit sends, and
`GET /healthz` on the installed port. It changes nothing.
Each value is held to a plain character set the unit can carry (no space,
`%`, `$`, quote, backslash, `&` or `|`), and the preview values must start
with a letter or digit; anything else is refused. With `--auth-header
X-authentik-uid`, in any letter case, `--allow-user` must be the Authentik
uid, `^[0-9a-f]{64}$` (lead decision 101). The unit sets no `MOSAIC_*`,
`CLAUDE*`, `CODEX*` or `PI_*` variable. Serve's exit 2 (usage, config, or a
port already in use) is not restarted.
The update step: `copy` the commit, stop the unit, `install --copy ID`
with the same flags, start it. Rollback is the same steps with the earlier
id, which must be at or after row 77's landing: an older `cli.mjs` refuses
serve's `--checkout` ("unknown option"). The queue view isn't pinned: it
reads the checkout, so it shows the queue as it is now. Copies are kept
until someone removes them, with `chmod -R u+w DIR && rm -rf DIR` since
they are read-only; nothing prunes them. The step can't be used yet: the
bus still runs from the checkout (`copy` doesn't include
`scripts/mosaic`), so `install --copy ID` refuses until a later row puts
the bus on the copy.
There is no start, stop, enable or restart verb. The Console reads the bus
through the human transport, so whoever starts it holds the human's
capability. Jason starts and restarts it
capability. Jason starts, stops and restarts it
(`systemctl --user enable --now mosaic-webui`), and so may Mos under lead
decision 102, which accepts that sends from a Console Mos started enter the
trail as Jason (`human-cli`). Any other agent starting it, even through