diff --git a/docs/plans/2026-09-26_lead-decisions.md b/docs/plans/2026-09-26_lead-decisions.md index e64b7575c..cdbfc629d 100644 --- a/docs/plans/2026-09-26_lead-decisions.md +++ b/docs/plans/2026-09-26_lead-decisions.md @@ -2462,3 +2462,55 @@ which stay with him. Each item names who decided it and what happened. into Vikunja would be a separate change and goes to Jason first. - If the pm slot fails for Sage, Mos files the task (sage-0081). Agent key scopes are #151, owned by ops-01. +111. **Q111 and Q112: work advances without dates (2026-10-12).** Jason, + relayed by Mos in sage-0089: "Q111: A, but time is irrelevant. We + move through tasks with a "next..." philosophy. Anything that can be + done without me should be done without me. Automatic advancement + without time restriction." Mos read it back and Jason confirmed in + Q112: "precisely" (sage-0090). + - Dates don't gate work. The next task starts as soon as its + prerequisites are done. A due date is a latest-by marker, not a + start date. Holds that named a date, such as decision 94's "after + the cutover", end when their condition is met. + - Anything that doesn't need Jason goes ahead without him and is + reported after. The hard-rule waits (push and merge to next or + main, deployment, spend, secrets, policy and role changes, starting + the Console) and strategy calls still wait for him. + - The Q97 order stands: S7, then M1-M2, then D1-D2 with I1, then + I2-I6. I1 containment still comes before Mos runs in the stack + (#164). + - Sage passes this to Darkwing, Dewey, Filbert, Researcher and Rocko + as lead. Each seat goes straight to its next row when one is done. +112. **One Console row for the pinned copy, the uid rule and row 77 + (2026-10-12).** Mos asked for one row and one reinstall (sage-0089) + covering Vikunja #146, #147 and #155. Row 77 (#1571, Rocko) takes a + new brief, `docs/plans/2026-10-12_console-pinned-copy.md`, with + Darkwing added beside Filbert as reviewer because it now changes + `packages/webui/src`. + - The copy is a `git archive` of a commit on `refactor`, written + read-only under `~/.local/share/mosaic-console//`. A worktree + can't be the copy, because the queue store refuses linked + worktrees (`packages/queue/src/store.mjs:70`). + - The queue view keeps reading through the canonical checkout's + `queue-read.mjs` with a new `serve --checkout PATH`, because the + store refuses code outside the canonical root (`store.mjs:85`). + Tradeoff: that one read child still runs live checkout code, so a + landing in `packages/queue` or `queue-read.mjs` still reaches the + Console without a reinstall. It writes nothing. Narrowing it + means a read path in the store that takes the root as an + argument, a `packages/queue` change; I'll file it if the review + or use shows a need. + - #147 reads decision 101 as a dedicated uid for the unit. Decision + 101 means the `X-authentik-uid` header in place of + `X-authentik-username`, not a separate Unix user. A Unix user + would break decision 62's process-ancestry human proof and needs + sudo, which Q18 rules out. The row adds the check decision 101 + describes: with that header, the allowed user must match + `^[0-9a-f]{64}$`. + - Reading the uid: ops-01 reads Authentik user pk 5's `uid` with + its existing admin access, writing nothing, and gives it to Mos. + Mos uses it in the reinstall only if it matches the pattern; + otherwise the unit stays on the username and Mos reports it. + - Mos does the one reinstall after the row lands: `copy`, stop, + `install --copy ID` with the uid, start, then status, `/healthz` + and the anonymous 302. diff --git a/docs/plans/2026-10-12_console-pinned-copy.md b/docs/plans/2026-10-12_console-pinned-copy.md new file mode 100644 index 000000000..e17c8be7c --- /dev/null +++ b/docs/plans/2026-10-12_console-pinned-copy.md @@ -0,0 +1,161 @@ +# 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.mjs` as 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`) and `RELEASE` (`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: `install` doesn't refuse while the unit is active, so the unit's + next `Restart=on-failure` runs 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-reload` skips the active check while the docs say + uninstall always refuses while active. +- N4: if `systemctl --user list-units` fails, the script exits 1 with no + message. +- N5: the `--allow-user` value 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.sh` +- `packages/webui/systemd/mosaic-webui.service.in` +- `packages/webui/src/cli.mjs`, the new `--checkout` flag and passing it + to `consoleReads` +- `packages/webui/src/reads.mjs`, only so the queue reader takes its own + root +- `packages/webui/tests/service.test.mjs` and new tests in + `packages/webui/tests/` +- `packages/webui/README.md`, the service section +- `docs/TOOLS.md`, the script's entry only +- `agents/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 + `.git` directory, not a worktree's `.git` file). It refuses a COMMIT + that isn't reachable from `refs/heads/refactor`. It resolves COMMIT + to the full 40-character id. +- It writes `git archive` of the paths the Console loads into + `DIR//`, by default + `${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-console//`. 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 prints `exists:` and its path. +- A test makes a copy from a fixture repository, starts `cli.mjs serve` + from the copy with a stand-in socket, and gets `/healthz` 200. So a + path missing from the list fails the suite. + +The unit: + +- `install` and `render` take `--copy ID`, which names an existing copy + by its full id. `install` refuses without it. The template's `@REPO@` + becomes the copy's path, so `WorkingDirectory` and `ExecStart` point at + the copy. +- `serve` takes `--checkout PATH`, the canonical checkout, and the unit + passes the checkout the script runs from. The queue read runs the + checkout's `queue-read.mjs` with 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. +- `status` prints the copy id the unit runs, read from its + `WorkingDirectory`, and whether `refactor` in 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: `install` refuses while the unit is active, activating or + deactivating, and its message says Jason or Mos stops it first. A test + uses a stand-in `systemctl`. +- N2: `install` and `copy` refuse when the script isn't in the canonical + checkout. `--allow-worktree` overrides the refusal for tests only, and + the help text says so. +- N3: `uninstall` checks 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 + `systemctl` call. +- N5: `--allow-user` and 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-header` is `X-authentik-uid` in any letter case, + `--allow-user` must match `^[0-9a-f]{64}$`, and `render` and `install` + refuse 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.