Rocko's work committed by Sage (Rocko has no git authority). Candidate
manifest sha256 4927de66, base 27a1a2bb.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
204 lines
11 KiB
Markdown
204 lines
11 KiB
Markdown
# Row 77 (#1571): Console pinned copy, update step and service hardening, candidate packet, round 1
|
|
|
|
Author: Rocko. Reviewers: Darkwing and Filbert. Gate: Sage. Brief:
|
|
`docs/plans/2026-10-12_console-pinned-copy.md`, section "Console pinned
|
|
copy, update step and service hardening" (lead decision 112). Base:
|
|
`27a1a2bb` (`base.txt`); `refactor` has not touched any of the files below
|
|
since (`git log 27a1a2bb..1d4ba6bb` on them is empty). The packet is
|
|
uncommitted. I made no commits, pushes or Gitea calls, read no token or
|
|
private binding, and never started, stopped, restarted or installed the
|
|
live unit. Every test run uses `--dir`, a stand-in `systemctl` and
|
|
fixture copies.
|
|
|
|
## Files (`files.txt`, `candidate-manifest.sha256`)
|
|
|
|
| File | Change |
|
|
|---|---|
|
|
| `scripts/webui-service.sh` | `copy` verb; `install`/`render --copy ID [--copies DIR]`; `--allow-worktree`; N1 to N5; the uid rule; `status` copy and drift lines |
|
|
| `packages/webui/systemd/mosaic-webui.service.in` | header comment only: `@REPO@` is the copy, `--checkout`, and decision 102's wording (Jason or Mos) in lines 6-9 |
|
|
| `packages/webui/src/cli.mjs` | `serve --checkout PATH` (absolute directory, else exit 2), passed to `consoleReads` as `queueRoot`; a `Queue:` start line |
|
|
| `packages/webui/src/reads.mjs` | `consoleReads({ root, queueRoot = root })`; the queue reader takes `queueRoot`, nothing else changes |
|
|
| `packages/webui/tests/service.test.mjs` | existing tests moved to `--copy`, the uid and the state read; +7 tests |
|
|
| `packages/webui/tests/pinned-copy.test.mjs` | new, 6 tests |
|
|
| `packages/webui/README.md` | one paragraph at the end of the service section |
|
|
| `docs/TOOLS.md` | the "Console service" entry only |
|
|
| `agents/rocko/work/queue-77/mutants.py` | the mutant rerun: `python3 mutants.py <tree>` |
|
|
| `agents/rocko/work/queue-77/byte-check.sh` | render without `--copy`, base script against candidate: `byte-check.sh <tree> <base> <out>` |
|
|
|
|
## The copy
|
|
|
|
`copy COMMIT [--copies DIR]`:
|
|
|
|
- Refuses outside the canonical checkout (`$REPO/.git` must be a
|
|
directory) unless `--allow-worktree` (tests only; the help text says so).
|
|
- COMMIT must start with a letter or digit and use only `A-Za-z0-9._/-`;
|
|
it resolves with `rev-parse --verify "$commit^{commit}"` to the full id,
|
|
and `merge-base --is-ancestor` against `refs/heads/refactor` refuses
|
|
anything off the branch. `--copies` must be absolute, in the unit's
|
|
character set.
|
|
- An existing `DIR/<id>` prints `exists:` and exits 0, untouched.
|
|
- Otherwise `git archive <id> -- COPY_PATHS | tar -x` into
|
|
`mktemp -d DIR/.<id>.XXXXXX` (removed by a trap on any failure),
|
|
`chmod a-w` on every file, `chmod 0755` on the temp directory, then
|
|
`mv -T` to `DIR/<id>`. A rename that loses a race to another `copy`
|
|
prints `exists:`.
|
|
|
|
Two choices against the brief's wording, both stricter:
|
|
|
|
- **chmod before the rename.** The brief says rename, then remove write
|
|
permission. I do it the other way round, so the final path is never
|
|
seen whole-but-writable, and a crash between the steps leaves only a
|
|
temp directory, not a writable copy at the id.
|
|
- **Files read-only, directories not.** Directories keep their write bit,
|
|
so the copy can still be removed with plain `rm -rf` ("copies are kept
|
|
until someone removes them"). A file can't be edited in place; a new
|
|
file could be added to a directory. Making directories read-only too is
|
|
a one-line change if the reviewers want it.
|
|
|
|
`COPY_PATHS` (in the script, with a comment): the `src` of bus, business,
|
|
cli, conversation, discord, harness, seat and webui, `packages/webui/README.md`,
|
|
`scripts/mosaic-config.mjs`, `roles` and `RELEASE`. `packages/queue` is
|
|
deliberately left out: the queue read runs in the checkout.
|
|
|
|
## The unit
|
|
|
|
- `install` and `render` take `--copy ID` (40 lower-case hex) and
|
|
`--copies DIR`. The copy must hold `packages/webui/src/cli.mjs`, else
|
|
"no copy ... make it with: scripts/webui-service.sh copy ID". `@REPO@`
|
|
becomes the copy path, and the flags get `--checkout <REPO>` in front.
|
|
- `install` refuses without `--copy`. `render` without `--copy` is the
|
|
base unit byte for byte (byte check below), and `render` takes no
|
|
`--allow-worktree` (it only prints).
|
|
- `status` reads `WorkingDirectory`. If its last part is 40 hex it prints
|
|
`copy: <id>` and one of `refactor: <sha>, the copy's commit`,
|
|
`refactor: <sha>, another commit; the copy is <id>`, or
|
|
`refactor: unknown; ...`. Otherwise `copy: none; the unit runs from <wd>`.
|
|
|
|
## N1 to N5 and the uid rule
|
|
|
|
- **N1, broader than the brief.** `running()` reads
|
|
`systemctl --user list-units --no-legend --plain mosaic-webui.service`
|
|
and treats any ACTIVE state but `inactive` or `failed` as running, so
|
|
`reloading` (and any state systemd adds) refuses too, not only the
|
|
brief's three. `install` refuses with "refusing: mosaic-webui.service is
|
|
running (STATE); Jason or Mos stops it first (systemctl --user stop
|
|
mosaic-webui), then install, then start it", and nothing is written.
|
|
- **N2.** `copy` and `install` call `canonical()`.
|
|
- **N3, I chose "always checks".** `uninstall` reads the state with or
|
|
without `--no-reload`; `--no-reload` skips only `daemon-reload`.
|
|
`install` does the same. The docs say so.
|
|
- **N4.** A failed state read dies with one line: "refusing: systemctl
|
|
--user list-units --no-legend --plain mosaic-webui.service failed, so
|
|
the unit's state is unknown; nothing was changed". systemctl's own
|
|
stderr is dropped so the line stays one line.
|
|
- **N5.** `--public-host` and `--auth-header` already had leading-character
|
|
checks that no test covered; `--allow-user` gains one ("allowed user must
|
|
start with a letter or digit"). Tests cover `-` and `.` on all three.
|
|
This also retires the case row 83 documented, `--allow-user --send`
|
|
without sending: it is now refused, and the old test that installed it
|
|
now asserts the refusal.
|
|
- **uid rule.** With `--auth-header` equal to `x-authentik-uid` in any
|
|
case (`${AUTH_HEADER,,}`), `--allow-user` must match `^[0-9a-f]{64}$`,
|
|
or render and install refuse naming the rule and decision 101.
|
|
|
|
Order in `install`: flags, canonical, `--copy` present, render (all value
|
|
checks), then the state read, then the write. A refused value never
|
|
reaches systemctl.
|
|
|
|
## Tests
|
|
|
|
`node --test tests/service.test.mjs tests/pinned-copy.test.mjs`: 28 pass.
|
|
|
|
service.test.mjs: every run now gets a stand-in `systemctl` (a quiet one
|
|
that lists nothing by default), since `install` reads the unit's state; no
|
|
run can reach the real systemd. New tests: install refuses in four
|
|
running states with and without `--no-reload` and goes ahead for
|
|
inactive, failed, another unit and nothing; a failed state read refuses
|
|
install and uninstall with one exact line; `--copy` refusals, `--copies`,
|
|
and the worktree refusal (the suite runs in a worktree, so it asserts the
|
|
refusal when `.git` is a file); `render --copy` byte for byte; the uid
|
|
rule in four header spellings, six bad values, render and install, and
|
|
the username header still taking a username; leading `-` and `.`; status
|
|
copy, drift and `copy: none`.
|
|
|
|
pinned-copy.test.mjs builds a fixture canonical repository on `refactor`
|
|
from this tree's packages, scripts and roles, with its own RELEASE. It
|
|
copies by short id, checks the top level, that every file is read-only,
|
|
`exists:` on repeats, the default XDG directory, and that the checkout is
|
|
untouched; refuses off-branch commits, bad names, bad copies directories
|
|
and a linked worktree, and accepts the worktree with `--allow-worktree`;
|
|
leaves the copies directory empty when `git archive` fails; serves from
|
|
the copy; refuses a bad `--checkout`; and checks `consoleReads` uses each
|
|
root.
|
|
|
|
The serve test against the brief: it starts `cli.mjs serve` from the copy
|
|
and gets `/healthz` 200, `Bus: acme` (the config check, from the copy),
|
|
RELEASE `0.0.0-fixture` and the business read through the copy's roles,
|
|
and the queue read in the `--checkout` directory with its cwd there.
|
|
Without `--checkout` the copy's own queue read fails closed (503), since
|
|
the copy has no `packages/queue`. Two departures, stated plainly: there
|
|
is no literal stand-in socket, because `/healthz` and the reads above make
|
|
no bus call (serve only builds the caller at start); and the bus child,
|
|
`packages/bus/src/human-cli.mjs`, spawned per bus call from the copy's
|
|
`packages/cli/src/transport.mjs`, is not exercised, because tests never
|
|
spawn the real human-cli. Both files are inside `COPY_PATHS`
|
|
(`packages/bus/src`, `packages/cli/src`), and serve imports from both at
|
|
start, so a missing one fails the serve test. The `seat` mutant below
|
|
shows the test catches a package missing from the list.
|
|
|
|
## Mutants (`mutants.py`, `out/mutants.txt`)
|
|
|
|
18 mutants, each applied alone to the candidate tree, all killed:
|
|
failed counted as running; reloading not running; a failed state read
|
|
ignored; install skipping the state read; uninstall skipping it under
|
|
`--no-reload`; the uid header case-sensitive; upper-case hex accepted;
|
|
allowed user free to start with `-`; no `--checkout` in the unit;
|
|
`canonical()` always passing; install without `--copy`; a missing copy
|
|
accepted; status drift always "the copy's commit"; `packages/seat/src`
|
|
dropped from the list; files left writable; off-branch commits accepted;
|
|
the queue reader on `root`; a relative `--checkout` accepted.
|
|
|
|
The last one first survived: the test's relative path didn't exist, so
|
|
`stat` refused it either way. The test now uses an existing relative
|
|
directory under the child's cwd, with a 10 s spawn timeout so a mutant
|
|
that really serves fails instead of hanging the suite (it hung once; I
|
|
killed my own fixture serve on port 0).
|
|
|
|
## Byte check (`byte-check.sh`, `out/byte-check.txt`, `out/byte-check/`)
|
|
|
|
The base script, placed beside the candidate for the run and removed
|
|
afterwards, and the candidate render eight flag sets without `--copy`,
|
|
including both preview headers, `--send`, a refused `--port 0` and a
|
|
dotted business id. With comment lines dropped (the template's header
|
|
changed by design), all 8 match, exit codes too, and the template's
|
|
non-comment body is identical.
|
|
|
|
## Gate (`out/gate/`, `out/gate.sh`)
|
|
|
|
Sequential, in the scratch worktree at `27a1a2bb` with the candidate and
|
|
`node_modules` linked, full output kept per suite, load average 20-33:
|
|
|
|
| Suite | Result |
|
|
|---|---|
|
|
| webui node (`packages/webui/tests/*.test.mjs`) | 94 pass, 0 fail |
|
|
| test-auth | 15 passed, 0 failed |
|
|
| test-conductor | 17 passed, 0 failed |
|
|
| test-config | 24 passed, 0 failed |
|
|
| test-discord | 66 passed, 0 failed |
|
|
| test-extension-package | 18 passed, 0 failed |
|
|
| test-foundation | 44 passed, 0 failed |
|
|
| test-queue | 27 passed, 0 failed (the canonical-root checks skip in a worktree, as always) |
|
|
| test-release | 14 passed, 0 failed |
|
|
| test-task | 98 passed, 0 failed |
|
|
|
|
No `✖` or `not ok` in any output file. `rocko-r77_default` was removed
|
|
after the Docker suites.
|
|
|
|
## For the reviewers
|
|
|
|
- The stale comment in `status` about `--allow-user --send` is updated:
|
|
the allowed user can no longer be `--send`.
|
|
- The live unit is untouched. Mos's reinstall after the landing is
|
|
`copy` at the landed commit, stop, `install --copy ID` with the uid,
|
|
start (brief, "Gate").
|