review(rocko): row 75 round 1 packet, the Console as a user service (#1569)

Committed by sage for rocko (no credential). Candidate manifest 7dc571db, packet manifest 0bc9a870, base 43d2328f.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-11 17:33:54 -05:00
co-authored by Claude Opus 5.5
parent 2d04426c5b
commit bce20fcbbe
19 changed files with 1129 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
# 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 URL` and 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._@+-]`.
- None of the sets allows a space, newline, `%`, `$`, quote, backslash, `&` or `|`. That means neither systemd nor the `sed` render can read a value as anything other than the value.
- Every refusal exits 1 and writes nothing. A refused `install` doesn't create the unit directory.
- `ExecStart=@NODE@ @REPO@/packages/webui/src/cli.mjs serve @FLAGS@`. NODE is
`command -v node` at render time, kept as given (here `/usr/bin/node`).
systemd doesn't look up an ExecStart command in `Environment=PATH`, so a bare
`node` would not be found the way the brief's wording suggests. The REPO
and node paths are held to `[A-Za-z0-9/._+-]`.
- `WorkingDirectory` and `PATH` are 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 are `Restart=on-failure`,
`RestartPreventExitStatus=2`, `KillSignal=SIGTERM`, `TimeoutStopSec=30`,
`NoNewPrivileges=yes` and `WantedBy=default.target`. There is no other
`Environment=` line and no `MOSAIC_*`, `CLAUDE*`, `CODEX*` or `PI_*` variable.
- `install` prints the `systemctl --user enable --now mosaic-webui` line 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).
- `uninstall` refuses while `list-units` shows the unit active, activating or
deactivating, unless `--no-reload` is given, as in bus-service.
- `status` prints is-enabled, is-active and `show`, then reads `--port` from the
installed unit's ExecStart and prints `GET http://127.0.0.1:<port>/healthz`:
the status code and body, or `unreachable (<code>)`. It exits 0 either way,
as bus-service status does.
## Points for the reviewer
1. **Exit 2 isn't retried.** `serve` exits 2 for usage, config and listen errors,
EADDRINUSE included (`serve.mjs` lines 149–150 reject, `cli.mjs` sets
exitCode 2). With `RestartPreventExitStatus=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.
2. **The preview flags need row 74.** Until row 74 lands, a unit rendered with
the preview set makes `serve` refuse with exit 2 (`unknown option`) and not
restart. Rendering and installing are unaffected.
3. **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 verify` on the rendered
unit exits 0.
4. **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 `MARKERS` that the test parses from `packages/bus/src/human.mjs`, and its only Environment key is PATH;
- install into `--dir` with `--no-reload` writes the file whole and identical to `render`, 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 `systemctl` on PATH, then removes the unit once it is inactive.
I added three tests beyond the brief:
- install without `--no-reload` calls only `daemon-reload`;
- status against an in-process `/healthz` server on port 0, then with the server down, then with no unit installed;
- usage exits 4, including the `start`, `stop`, `enable` and `restart` verbs.
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.
+1
View File
@@ -0,0 +1 @@
43d2328f3a413b066f435c13cacd4df462670c57
+523
View File
@@ -0,0 +1,523 @@
diff --git a/docs/TOOLS.md b/docs/TOOLS.md
index 9a28a090..631e1814 100644
--- a/docs/TOOLS.md
+++ b/docs/TOOLS.md
@@ -238,6 +238,37 @@ stdin and stdout it exits 4 with the usage.
Exit codes: `0` ok · `1` failed or outcome unknown · `2` invalid input ·
`3` refused or config problem · `4` usage. Details: `packages/cli/README.md`.
+## 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 uninstall [--dir DIR] [--no-reload]
+scripts/webui-service.sh status [--dir DIR]
+# FLAGS: [--port N] [--business ID] [--board URL]
+# [--public-host NAME --auth-header HEADER --allow-user ID]
+```
+
+The systemd user unit `mosaic-webui.service` runs the Console,
+`node packages/webui/src/cli.mjs serve`, with serve's flags as given:
+`--port` defaults to 7332 (7330 is the operator's own Console), and the
+preview set goes all three or none. 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 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.
+
+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; an agent starting it, even through systemd, works around the
+human proof (lead decisions 62 and 94). Jason starts it from his own shell:
+`systemctl --user enable --now mosaic-webui`.
+Exit codes: `0` ok · `1` failed or refused · `4` usage.
+
## Discord connector (`scripts/discord.sh`)
```bash
diff --git a/packages/webui/systemd/mosaic-webui.service.in b/packages/webui/systemd/mosaic-webui.service.in
new file mode 100644
index 00000000..081bd963
--- /dev/null
+++ b/packages/webui/systemd/mosaic-webui.service.in
@@ -0,0 +1,35 @@
+# Mosaic Console: `node packages/webui/src/cli.mjs serve`, one instance.
+# Rendered by scripts/webui-service.sh from packages/webui/systemd/; @REPO@,
+# @PATH@, @NODE@ and the serve flags below are filled in at install time.
+# Edit the template and reinstall; do not edit the installed copy.
+#
+# The Console reads the bus through the human transport, so whoever starts
+# this unit holds the human's capability. Jason starts it from his own
+# shell (`systemctl --user enable --now mosaic-webui`); an agent starting
+# it works around the human proof (lead decisions 62 and 94). The unit sets
+# no MOSAIC_*, CLAUDE*, CODEX* or PI_* variable.
+[Unit]
+Description=Mosaic Console
+Documentation=file://@REPO@/packages/webui/README.md
+StartLimitIntervalSec=600
+StartLimitBurst=5
+
+[Service]
+Type=simple
+WorkingDirectory=@REPO@
+Environment=PATH=@PATH@
+# The server binds 127.0.0.1 only. Usage, config and listen refusals
+# (a port already in use included) exit 2 and are not retried; fix the
+# flags or free the port, then start it again.
+ExecStart=@NODE@ @REPO@/packages/webui/src/cli.mjs serve @FLAGS@
+Restart=on-failure
+RestartSec=15
+RestartPreventExitStatus=2
+# `systemctl --user stop` sends SIGTERM; the server stops accepting and
+# closes. Open connections that outlast TimeoutStopSec are killed.
+KillSignal=SIGTERM
+TimeoutStopSec=30
+NoNewPrivileges=yes
+
+[Install]
+WantedBy=default.target
diff --git a/packages/webui/tests/service.test.mjs b/packages/webui/tests/service.test.mjs
new file mode 100644
index 00000000..d50b6652
--- /dev/null
+++ b/packages/webui/tests/service.test.mjs
@@ -0,0 +1,220 @@
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { spawn } from 'node:child_process';
+import { createServer } from 'node:http';
+import { once } from 'node:events';
+import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+// scripts/webui-service.sh (row 75, #1569). Nothing here starts a unit or
+// reaches the real systemd: install and uninstall run with --no-reload or a
+// stand-in systemctl on PATH, and XDG_CONFIG_HOME points into a temp dir.
+const REPO = fileURLToPath(new URL('../../..', import.meta.url)).replace(/\/$/, '');
+const SCRIPT = join(REPO, 'scripts', 'webui-service.sh');
+const NODE_DIR = dirname(process.execPath);
+const PREVIEW = ['--public-host', 'console.example.com', '--auth-header', 'X-authentik-uid', '--allow-user', 'jason'];
+
+function tmp(t) {
+ const dir = mkdtempSync(join(tmpdir(), 'webui-service-'));
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
+ return dir;
+}
+
+// A stand-in systemctl that prints `lines` for list-units and logs every call.
+function standIn(dir, lines = '') {
+ const bin = join(dir, 'bin');
+ mkdirSync(bin);
+ writeFileSync(join(dir, 'list-units.txt'), lines);
+ writeFileSync(join(bin, 'systemctl'), `#!/bin/sh
+echo "$*" >> "${dir}/calls.txt"
+case "$*" in
+ *list-units*) cat "${dir}/list-units.txt" ;;
+ *is-enabled*) echo enabled ;;
+ *is-active*) echo active ;;
+esac
+exit 0
+`);
+ chmodSync(join(bin, 'systemctl'), 0o755);
+ return bin;
+}
+
+// Async, so an in-process server can answer the script's /healthz request.
+async function run(dir, args, bin = null) {
+ const env = { PATH: `${bin ? `${bin}:` : ''}${NODE_DIR}:/usr/bin:/bin`, HOME: dir, XDG_CONFIG_HOME: join(dir, 'config') };
+ const child = spawn(SCRIPT, args, { env });
+ let stdout = '', stderr = '';
+ child.stdout.on('data', (d) => { stdout += d; });
+ child.stderr.on('data', (d) => { stderr += d; });
+ const [status] = await once(child, 'close');
+ return { status, stdout, stderr };
+}
+
+const execStart = (unit) => unit.split('\n').find((l) => l.startsWith('ExecStart='));
+
+test('render prints the unit without the preview flags', async (t) => {
+ const dir = tmp(t);
+ const r = await run(dir, ['render']);
+ assert.equal(r.status, 0, r.stderr);
+ assert.match(r.stdout, new RegExp(`^WorkingDirectory=${REPO.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm'));
+ assert.match(r.stdout, /^Environment=PATH=\S*\/usr\/local\/bin:\/usr\/bin:\/bin$/m);
+ assert.equal(execStart(r.stdout), `ExecStart=${process.execPath} ${REPO}/packages/webui/src/cli.mjs serve --port 7332`);
+ for (const line of ['Restart=on-failure', 'RestartPreventExitStatus=2', 'NoNewPrivileges=yes', 'WantedBy=default.target']) {
+ assert.ok(r.stdout.split('\n').includes(line), line);
+ }
+ assert.doesNotMatch(r.stdout, /@[A-Z]+@/, 'every placeholder is filled');
+});
+
+test('render carries every serve flag, the preview set included', async (t) => {
+ const dir = tmp(t);
+ const r = await run(dir, ['render', '--port', '7340', '--business', 'mosaic.dev_1', '--board', 'http://127.0.0.1:7331', ...PREVIEW]);
+ assert.equal(r.status, 0, r.stderr);
+ assert.equal(execStart(r.stdout), `ExecStart=${process.execPath} ${REPO}/packages/webui/src/cli.mjs serve --port 7340 --business mosaic.dev_1 --board http://127.0.0.1:7331 --public-host console.example.com --auth-header X-authentik-uid --allow-user jason`);
+});
+
+test('a partial preview set is refused', async (t) => {
+ const dir = tmp(t);
+ for (const args of [PREVIEW.slice(0, 2), PREVIEW.slice(2, 6), [...PREVIEW.slice(0, 2), ...PREVIEW.slice(4, 6)]]) {
+ const r = await run(dir, ['render', ...args]);
+ assert.equal(r.status, 1, args.join(' '));
+ assert.equal(r.stdout, '');
+ assert.match(r.stderr, /all three or none/);
+ }
+});
+
+test('a value the unit cannot carry is refused, for every flag', async (t) => {
+ const dir = tmp(t);
+ const set = (flag, value) => {
+ const preview = PREVIEW.slice();
+ const i = preview.indexOf(flag);
+ if (i >= 0) { preview[i + 1] = value; return preview; }
+ return [flag, value];
+ };
+ for (const flag of ['--business', '--board', '--public-host', '--auth-header', '--allow-user']) {
+ for (const bad of [' x', '%n', '\n', '$HOME', '"', '\\', '|', '&']) {
+ const value = flag === '--board' ? `http://127.0.0.1:7331${bad}` : `a${bad}b`;
+ const r = await run(dir, ['render', ...set(flag, value)]);
+ assert.equal(r.status, 1, `${flag} ${JSON.stringify(value)}`);
+ assert.equal(r.stdout, '', `${flag} ${JSON.stringify(value)}`);
+ }
+ }
+ for (const port of ['0', '65536', '7a', '', '-1', '999999']) {
+ const r = await run(dir, ['render', '--port', port]);
+ assert.equal(r.status, 1, `port ${JSON.stringify(port)}`);
+ }
+ for (const args of [['--business', 'Mosaic'], ['--business', '-x'], ['--business', 'a'.repeat(65)], ['--board', 'https://127.0.0.1:7331'], ['--board', '127.0.0.1:7331'], set('--public-host', 'console.example.com:443'), set('--public-host', 'https://console.example.com'), set('--auth-header', 'X_uid')]) {
+ const r = await run(dir, ['render', ...args]);
+ assert.equal(r.status, 1, args.join(' '));
+ }
+});
+
+test('the rendered unit names none of the human proof agent markers and sets no agent variable', async (t) => {
+ const dir = tmp(t);
+ const source = readFileSync(join(REPO, 'packages', 'bus', 'src', 'human.mjs'), 'utf8');
+ const markers = [...source.match(/const MARKERS = \[([^\]]*)\]/)[1].matchAll(/'([A-Z_]+)'/g)].map((m) => m[1]);
+ assert.ok(markers.length >= 7 && markers.includes('MOSAIC_BUS_CAP') && markers.includes('CLAUDECODE'), markers.join(' '));
+ for (const args of [[], [...PREVIEW, '--business', 'mosaic']]) {
+ const r = await run(dir, ['render', ...args]);
+ assert.equal(r.status, 0, r.stderr);
+ for (const marker of markers) assert.ok(!r.stdout.includes(marker), marker);
+ const envLines = r.stdout.split('\n').filter((l) => /^\s*(Environment|EnvironmentFile|PassEnvironment|UnsetEnvironment)\s*=/.test(l));
+ assert.deepEqual(envLines.map((l) => l.split('=')[1]), ['PATH']);
+ assert.doesNotMatch(r.stdout, /^[^#]*\b(MOSAIC_|CLAUDE|CODEX|PI_)/m);
+ }
+});
+
+test('install into --dir with --no-reload writes the file whole, then reports unchanged', async (t) => {
+ const dir = tmp(t);
+ const unitDir = join(dir, 'units');
+ const bin = standIn(dir);
+ const rendered = await run(dir, ['render', '--business', 'mosaic']);
+ const r = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--business', 'mosaic'], bin);
+ assert.equal(r.status, 0, r.stderr);
+ const target = join(unitDir, 'mosaic-webui.service');
+ assert.match(r.stdout, new RegExp(`^written: ${target}$`, 'm'));
+ assert.match(r.stdout, /systemctl --user enable --now mosaic-webui/);
+ assert.equal(readFileSync(target, 'utf8'), rendered.stdout);
+ assert.equal(statSync(target).mode & 0o777, 0o644);
+ assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service'], 'no temp file left behind');
+ const again = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--business', 'mosaic'], bin);
+ assert.equal(again.status, 0, again.stderr);
+ assert.match(again.stdout, new RegExp(`^unchanged: ${target}$`, 'm'));
+ assert.ok(!existsSync(join(dir, 'calls.txt')), 'no systemctl call with --no-reload');
+ // A refused value writes nothing and leaves the installed unit alone.
+ const bad = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--business', 'a b'], bin);
+ assert.equal(bad.status, 1);
+ assert.equal(readFileSync(target, 'utf8'), rendered.stdout);
+ assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service']);
+ const fresh = join(dir, 'never');
+ assert.equal((await run(dir, ['install', '--dir', fresh, '--no-reload', '--port', '0'], bin)).status, 1);
+ assert.ok(!existsSync(fresh), 'a refusal creates no directory');
+});
+
+test('install without --no-reload runs daemon-reload and nothing that starts the unit', async (t) => {
+ const dir = tmp(t);
+ const bin = standIn(dir);
+ const r = await run(dir, ['install', '--dir', join(dir, 'units')], bin);
+ assert.equal(r.status, 0, r.stderr);
+ assert.match(r.stdout, /^daemon-reload done$/m);
+ assert.equal(readFileSync(join(dir, 'calls.txt'), 'utf8'), '--user daemon-reload\n');
+});
+
+test('uninstall refuses while the unit is active, and removes it once it is not', async (t) => {
+ const dir = tmp(t);
+ const unitDir = join(dir, 'units');
+ const target = join(unitDir, 'mosaic-webui.service');
+ for (const state of ['active', 'activating', 'deactivating']) {
+ const bin = standIn(mkdtempSync(join(dir, 'si-')), `mosaic-webui.service loaded ${state} running Mosaic Console\n`);
+ assert.equal((await run(dir, ['install', '--dir', unitDir, '--no-reload'])).status, 0);
+ const r = await run(dir, ['uninstall', '--dir', unitDir], bin);
+ assert.equal(r.status, 1, state);
+ assert.match(r.stderr, /refusing: mosaic-webui\.service is still running/);
+ assert.ok(existsSync(target), `still installed while ${state}`);
+ }
+ const bin = standIn(mkdtempSync(join(dir, 'si-')), 'mosaic-webui.service loaded inactive dead Mosaic Console\n');
+ const r = await run(dir, ['uninstall', '--dir', unitDir], bin);
+ assert.equal(r.status, 0, r.stderr);
+ assert.match(r.stdout, new RegExp(`^removed: ${target}$`, 'm'));
+ assert.match(r.stdout, /^daemon-reload done$/m);
+ assert.ok(!existsSync(target));
+ const absent = await run(dir, ['uninstall', '--dir', unitDir, '--no-reload'], bin);
+ assert.equal(absent.status, 0);
+ assert.match(absent.stdout, /^absent: /m);
+});
+
+test('status prints the unit state and GET /healthz on the installed port', async (t) => {
+ const dir = tmp(t);
+ const unitDir = join(dir, 'units');
+ const bin = standIn(dir);
+ const server = createServer((req, res) => {
+ if (req.url === '/healthz') { res.writeHead(200, { 'content-type': 'application/json' }); res.end('{"ok":true}\n'); return; }
+ res.writeHead(404); res.end();
+ });
+ server.listen(0, '127.0.0.1');
+ await once(server, 'listening');
+ t.after(() => server.close());
+ const { port } = server.address();
+ assert.equal((await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port)])).status, 0);
+ const r = await run(dir, ['status', '--dir', unitDir], bin);
+ assert.equal(r.status, 0, r.stderr);
+ assert.match(r.stdout, /^ {2}enabled: enabled$/m);
+ assert.match(r.stdout, /^ {2}active: {2}active$/m);
+ assert.match(r.stdout, new RegExp(`^healthz: http://127\\.0\\.0\\.1:${port}/healthz 200 \\{"ok":true\\}$`, 'm'));
+ server.close();
+ await once(server, 'close');
+ const down = await run(dir, ['status', '--dir', unitDir], bin);
+ assert.equal(down.status, 0, down.stderr);
+ assert.match(down.stdout, /^healthz: \S+ unreachable \(ECONNREFUSED\)$/m);
+ const none = await run(dir, ['status', '--dir', join(dir, 'empty')], bin);
+ assert.match(none.stdout, /^healthz: not checked; no unit at /m);
+});
+
+test('usage: an unknown verb or flag, and start or enable, exit 4', async (t) => {
+ const dir = tmp(t);
+ for (const args of [[], ['start'], ['stop'], ['enable'], ['restart'], ['render', '--bogus'], ['render', '--port'], ['render', '--dir', dir], ['render', '--no-reload'], ['uninstall', '--port', '7332'], ['status', '--business', 'mosaic'], ['status', '--no-reload']]) {
+ const r = await run(dir, args);
+ assert.equal(r.status, 4, args.join(' '));
+ assert.match(r.stderr, /^usage: scripts\/webui-service\.sh/);
+ }
+});
diff --git a/scripts/webui-service.sh b/scripts/webui-service.sh
new file mode 100755
index 00000000..4ff06833
--- /dev/null
+++ b/scripts/webui-service.sh
@@ -0,0 +1,208 @@
+#!/usr/bin/env bash
+# `scripts/webui-service.sh render|install|uninstall|status`: the systemd
+# user unit that runs the Console, `node packages/webui/src/cli.mjs serve`
+# (lead decision 94). The template is
+# packages/webui/systemd/mosaic-webui.service.in; the rendered unit is
+# mosaic-webui.service in the systemd user directory, one instance.
+#
+# render [FLAGS] print the rendered unit on stdout
+# install [--dir DIR] [--no-reload] [FLAGS]
+# write the unit (write to a temp file, then
+# rename) and run `systemctl --user daemon-reload`
+# uninstall [--dir DIR] [--no-reload]
+# remove the unit; refuses while it is active
+# status [--dir DIR] unit state, then GET /healthz on its port
+#
+# FLAGS are serve's: --port N (default 7332; 7330 is the operator's own
+# Console), --business ID, --board URL, and the preview set --public-host
+# NAME --auth-header HEADER --allow-user ID, all three or none. systemd
+# expands % and $ and splits on spaces, so each value is held to a plain
+# character set and refused otherwise. None is a secret.
+#
+# 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. An agent starting it, even detached through systemd, works
+# around the human proof (lead decisions 62 and 94). Jason starts it from
+# his own shell: `systemctl --user enable --now mosaic-webui`.
+#
+# Exit codes: 0 ok, 1 operation failed, 4 usage.
+set -euo pipefail
+REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+TEMPLATE="$REPO/packages/webui/systemd/mosaic-webui.service.in"
+UNIT_NAME="mosaic-webui.service"
+USAGE="usage: scripts/webui-service.sh render [FLAGS] | install [--dir DIR] [--no-reload] [FLAGS] | uninstall [--dir DIR] [--no-reload] | status [--dir DIR]
+ FLAGS: [--port N] [--business ID] [--board URL] [--public-host NAME --auth-header HEADER --allow-user ID]"
+
+die() { echo "webui-service: $*" >&2; exit 1; }
+usage() { echo "$USAGE" >&2; exit 4; }
+
+UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
+RELOAD=1
+DIR_FLAGS=0
+PORT=7332
+BUSINESS=""
+BOARD=""
+PUBLIC_HOST=""
+AUTH_HEADER=""
+ALLOW_USER=""
+SERVE_SET=0
+
+parse_flags() {
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ --dir) [ $# -ge 2 ] || usage; UNIT_DIR="$2"; DIR_FLAGS=1; shift 2 ;;
+ --no-reload) RELOAD=0; DIR_FLAGS=1; shift ;;
+ --port) [ $# -ge 2 ] || usage; PORT="$2"; SERVE_SET=1; shift 2 ;;
+ --business) [ $# -ge 2 ] || usage; BUSINESS="$2"; SERVE_SET=1; shift 2 ;;
+ --board) [ $# -ge 2 ] || usage; BOARD="$2"; SERVE_SET=1; shift 2 ;;
+ --public-host) [ $# -ge 2 ] || usage; PUBLIC_HOST="$2"; SERVE_SET=1; shift 2 ;;
+ --auth-header) [ $# -ge 2 ] || usage; AUTH_HEADER="$2"; SERVE_SET=1; shift 2 ;;
+ --allow-user) [ $# -ge 2 ] || usage; ALLOW_USER="$2"; SERVE_SET=1; shift 2 ;;
+ *) usage ;;
+ esac
+ done
+}
+
+# The serve flags as one ExecStart fragment. Each character set excludes
+# space, %, $, quotes, backslash, & and |, so neither systemd nor the sed
+# below can read a value as anything but itself.
+serve_flags() {
+ case "$PORT" in ''|*[!0-9]*) die "port must be 1..65535: $PORT" ;; esac
+ [ "${#PORT}" -le 5 ] && [ "$PORT" -ge 1 ] && [ "$PORT" -le 65535 ] || die "port must be 1..65535: $PORT"
+ local flags="--port $PORT"
+ if [ -n "$BUSINESS" ]; then
+ case "$BUSINESS" in [a-z0-9]*) ;; *) die "business must be a business id: $BUSINESS" ;; esac
+ case "$BUSINESS" in *[!a-z0-9._-]*) die "business must be a business id: $BUSINESS" ;; esac
+ [ "${#BUSINESS}" -le 64 ] || die "business must be a business id: $BUSINESS"
+ flags="$flags --business $BUSINESS"
+ fi
+ if [ -n "$BOARD" ]; then
+ # serve checks the rest (a loopback origin, no path or credentials).
+ case "$BOARD" in http://?*) ;; *) die "board must be an http:// URL: $BOARD" ;; esac
+ case "$BOARD" in *[!A-Za-z0-9.:/_-]*) die "board contains a character the unit cannot carry: $BOARD" ;; esac
+ flags="$flags --board $BOARD"
+ fi
+ local preview=0
+ [ -z "$PUBLIC_HOST" ] || preview=$((preview + 1))
+ [ -z "$AUTH_HEADER" ] || preview=$((preview + 1))
+ [ -z "$ALLOW_USER" ] || preview=$((preview + 1))
+ if [ "$preview" -ne 0 ]; then
+ [ "$preview" -eq 3 ] || die "--public-host, --auth-header and --allow-user go together: all three or none"
+ case "$PUBLIC_HOST" in [A-Za-z0-9]*) ;; *) die "public host must be a hostname, no port or scheme: $PUBLIC_HOST" ;; esac
+ case "$PUBLIC_HOST" in *[!A-Za-z0-9.-]*) die "public host must be a hostname, no port or scheme: $PUBLIC_HOST" ;; esac
+ case "$AUTH_HEADER" in [A-Za-z0-9]*) ;; *) die "auth header must be a header name: $AUTH_HEADER" ;; esac
+ case "$AUTH_HEADER" in *[!A-Za-z0-9-]*) die "auth header must be a header name: $AUTH_HEADER" ;; esac
+ case "$ALLOW_USER" in *[!A-Za-z0-9._@+-]*) die "allowed user contains a character the unit cannot carry: $ALLOW_USER" ;; esac
+ flags="$flags --public-host $PUBLIC_HOST --auth-header $AUTH_HEADER --allow-user $ALLOW_USER"
+ fi
+ echo "$flags"
+}
+
+render() {
+ [ -f "$TEMPLATE" ] || die "template missing: $TEMPLATE"
+ local node
+ node="$(command -v node || true)"
+ [ -n "$node" ] || die "node not found on PATH"
+ # systemd does not look an ExecStart command up in Environment=PATH,
+ # so the unit runs node by the path found here, kept as given.
+ case "$node" in /*) ;; *) die "node path must be absolute: $node" ;; esac
+ case "$node" in *[!A-Za-z0-9/._+-]*) die "node path contains a character the unit cannot carry: $node" ;; esac
+ case "$REPO" in *[!A-Za-z0-9/._+-]*) die "repository path contains a character the unit cannot carry: $REPO" ;; esac
+ local node_dir path="/usr/local/bin:/usr/bin:/bin"
+ node_dir="$(dirname "$node")"
+ case ":$path:" in *":$node_dir:"*) ;; *) path="$node_dir:$path" ;; esac
+ local flags
+ flags="$(serve_flags)" || exit 1
+ sed -e "s|@REPO@|$REPO|g" -e "s|@PATH@|$path|g" -e "s|@NODE@|$node|g" -e "s|@FLAGS@|$flags|g" "$TEMPLATE"
+}
+
+install_unit() {
+ parse_flags "$@"
+ local unit
+ unit="$(render)" || exit 1
+ mkdir -p "$UNIT_DIR"
+ local target="$UNIT_DIR/$UNIT_NAME" tmp
+ tmp="$(mktemp "$UNIT_DIR/.$UNIT_NAME.XXXXXX")"
+ printf '%s\n' "$unit" > "$tmp"
+ chmod 0644 "$tmp"
+ if [ -f "$target" ] && cmp -s "$tmp" "$target"; then
+ rm -f "$tmp"
+ echo "unchanged: $target"
+ else
+ mv -f "$tmp" "$target"
+ echo "written: $target"
+ fi
+ echo "serve: $(sed -n 's/^ExecStart=//p' "$target")"
+ if [ "$RELOAD" = 1 ]; then
+ systemctl --user daemon-reload || die "daemon-reload failed"
+ echo "daemon-reload done"
+ fi
+ cat <<MSG
+this script starts nothing: the Console holds the human's capability, so the
+human starts it from their own shell:
+ systemctl --user enable --now mosaic-webui start now and at login
+ systemctl --user status mosaic-webui
+ journalctl --user -u mosaic-webui -f the Console's log
+ systemctl --user stop mosaic-webui SIGTERM; restartable
+ scripts/webui-service.sh status unit state and /healthz
+survive logout and reboot only with lingering on: loginctl enable-linger ${USER:-$(id -un)}
+MSG
+}
+
+uninstall_unit() {
+ parse_flags "$@"
+ [ "$SERVE_SET" = 0 ] || usage
+ local target="$UNIT_DIR/$UNIT_NAME"
+ if [ "$RELOAD" = 1 ]; then
+ local active
+ active="$(systemctl --user list-units --no-legend --plain "$UNIT_NAME" 2>/dev/null | awk '$3 == "active" || $3 == "activating" || $3 == "deactivating" {print $1}')"
+ [ -z "$active" ] || die "refusing: $UNIT_NAME is still running; the human stops it first (systemctl --user stop mosaic-webui)"
+ fi
+ if [ -f "$target" ]; then
+ rm -f "$target"
+ echo "removed: $target"
+ else
+ echo "absent: $target"
+ fi
+ if [ "$RELOAD" = 1 ]; then
+ systemctl --user daemon-reload || die "daemon-reload failed"
+ echo "daemon-reload done"
+ fi
+}
+
+status_unit() {
+ parse_flags "$@"
+ [ "$SERVE_SET" = 0 ] && [ "$RELOAD" = 1 ] || usage
+ local target="$UNIT_DIR/$UNIT_NAME"
+ echo "unit: $UNIT_NAME"
+ echo " enabled: $(systemctl --user is-enabled "$UNIT_NAME" 2>&1 || true)"
+ echo " active: $(systemctl --user is-active "$UNIT_NAME" 2>&1 || true)"
+ systemctl --user show "$UNIT_NAME" -p MainPID -p NRestarts -p ExecMainStartTimestamp -p Result 2>/dev/null | sed 's/^/ /' || true
+ if [ ! -f "$target" ]; then
+ echo "healthz: not checked; no unit at $target"
+ return 0
+ fi
+ local port
+ port="$(sed -n 's/^ExecStart=.* --port \([0-9][0-9]*\).*$/\1/p' "$target")"
+ [ -n "$port" ] || { echo "healthz: not checked; no --port in $target"; return 0; }
+ local url="http://127.0.0.1:$port/healthz"
+ echo "healthz: $url $(node -e '
+fetch(process.argv[1], { signal: AbortSignal.timeout(5000) })
+ .then(async (res) => console.log(`${res.status} ${(await res.text()).trim()}`))
+ .catch((err) => console.log(`unreachable (${err.cause?.code ?? err.name})`));
+' "$url")"
+}
+
+[ $# -ge 1 ] || usage
+cmd="$1"; shift
+case "$cmd" in
+ render)
+ parse_flags "$@"
+ [ "$DIR_FLAGS" = 0 ] || usage
+ render ;;
+ install) install_unit "$@" ;;
+ uninstall) uninstall_unit "$@" ;;
+ status) status_unit "$@" ;;
+ -h|--help) echo "$USAGE" ;;
+ *) usage ;;
+esac
@@ -0,0 +1,4 @@
12ab94c9541afa26c4d823fbae53712834b1cdcefbd8f1970fd29145eecef9ad docs/TOOLS.md
9c2eec77207baad825bb4ab6a7d2dac17d92cf62f1cf4ab173a4902b7c07002c packages/webui/systemd/mosaic-webui.service.in
9c1a6c8b2eeace79f6d061e67d97cfb22195d5fb1e1ea6040049de95c833cb20 packages/webui/tests/service.test.mjs
69fad4eb85c3e9297f6ebba2ec84d17d49a5ef028d1787e981f5af61fc9a8912 scripts/webui-service.sh
+4
View File
@@ -0,0 +1,4 @@
docs/TOOLS.md
packages/webui/systemd/mosaic-webui.service.in
packages/webui/tests/service.test.mjs
scripts/webui-service.sh
@@ -0,0 +1 @@
2026-10-11T22:32:03Z
@@ -0,0 +1 @@
2026-10-11T22:25:46Z
@@ -0,0 +1,9 @@
test-auth rc=0
test-conductor rc=0
test-config rc=0
test-discord rc=0
test-extension-package rc=0
test-foundation rc=0
test-queue rc=0
test-release rc=0
test-task rc=0
@@ -0,0 +1,17 @@
OK status with missing harness credential exits 3 and still lists accounts
OK status reports harness credential (read-only) + mosaic accounts
OK api key material never reaches output
OK oauth token material never reaches output
OK unparseable credential file exits 2
OK symlinked credential file exits 4
OK env-side credential names reported
OK env var values never reach output
OK accounts without an accounts dir reports none and creates nothing
OK accounts lists files and marks the active one
OK loose account perms flagged in listing
OK agent --auth with missing account file refuses (exit 4)
OK agent --auth with non-0600 account file refuses
OK agent --auth with invalid account name refuses
OK auth.sh without valid config refuses
selftest: 15 passed, 0 failed
@@ -0,0 +1,55 @@
Note: switching to '43d2328f3a413b066f435c13cacd4df462670c57'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
Not currently on any branch.
nothing to commit, working tree clean
Note: switching to '43d2328f3a413b066f435c13cacd4df462670c57'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
OK dry-run: allowed change, exit 0, nothing committed (exit 0)
OK dry-run committed nothing
OK apply: allowed change exits 0 (exit 0)
OK apply: attribution in commit subject
OK apply: target tree clean after commit
OK disallowed path refused (exit 1)
OK disallowed path: target untouched
OK syntax gate refused broken .mjs (exit 1)
OK syntax gate: target untouched
OK suite failure refused (exit 1)
OK suite failure: target reverted to clean
OK disabled policy refused (exit 2)
OK disabled policy: target untouched
OK failed run refused (exit 1)
OK failed run: target untouched
OK missing run exits 4 (exit 4)
OK invalid policy exits 2 (exit 2)
selftest: 17 passed, 0 failed
@@ -0,0 +1,26 @@
OK absent adapter defaults to pi
OK adapter mock validates (exit 0)
OK unsupported adapter exits 2 (exit 2)
OK env exports adapter
OK bootstrap creates default when absent (exit 0)
OK bootstrap wrote config file
OK bootstrap is idempotent on existing config (exit 0)
OK bootstrap did not rewrite existing config
OK validate missing config exits 3 (exit 3)
OK malformed JSON exits 2 (exit 2)
OK unsupported configVersion exits 2 (exit 2)
OK unknown top-level key exits 2 (exit 2)
OK unknown execution key exits 2 (exit 2)
OK unsupported backend exits 2 (exit 2)
OK unsupported environment exits 2 (exit 2)
OK relative dataRoot exits 2 (exit 2)
OK non-canonical dataRoot exits 2 (exit 2)
OK filesystem root dataRoot exits 2 (exit 2)
OK home directory dataRoot exits 2 (exit 2)
OK dataRoot containing config dir exits 2 (exit 2)
OK control character in provider exits 2 (exit 2)
OK symlinked config file exits 2 (exit 2)
OK env exports resolve correctly
OK failed validation modified nothing
selftest: 24 passed, 0 failed
@@ -0,0 +1,70 @@
toolchain: node v26.8.1
OK syntax: packages/discord/src/approvals.mjs
OK syntax: packages/discord/src/authorize.mjs
OK syntax: packages/discord/src/binding.mjs
OK syntax: packages/discord/src/cli.mjs
OK syntax: packages/discord/src/connector.mjs
OK syntax: packages/discord/src/context.mjs
OK syntax: packages/discord/src/engine-pi.mjs
OK syntax: packages/discord/src/errors.mjs
OK syntax: packages/discord/src/gateway.mjs
OK syntax: packages/discord/src/git.mjs
OK syntax: packages/discord/src/journal.mjs
OK syntax: packages/discord/src/notify.mjs
OK syntax: packages/discord/src/rest.mjs
OK syntax: packages/discord/src/setspark.mjs
OK syntax: packages/discord/src/tools.mjs
OK syntax: packages/discord/src/web.mjs
OK syntax: packages/discord/bin/git-credential.mjs
OK syntax: packages/discord/extension/tools.mjs
OK syntax: packages/discord/tests/approvals.test.mjs
OK syntax: packages/discord/tests/authorize.test.mjs
OK syntax: packages/discord/tests/binding.test.mjs
OK syntax: packages/discord/tests/connector.test.mjs
OK syntax: packages/discord/tests/context.test.mjs
OK syntax: packages/discord/tests/engine.test.mjs
OK syntax: packages/discord/tests/fake-pi.mjs
OK syntax: packages/discord/tests/gateway.test.mjs
OK syntax: packages/discord/tests/git.test.mjs
OK syntax: packages/discord/tests/helpers.mjs
OK syntax: packages/discord/tests/journal.test.mjs
OK syntax: packages/discord/tests/notify.test.mjs
OK syntax: packages/discord/tests/recover.test.mjs
OK syntax: packages/discord/tests/rest.test.mjs
OK syntax: packages/discord/tests/setspark.test.mjs
OK syntax: packages/discord/tests/tools.test.mjs
OK syntax: packages/discord/tests/web.test.mjs
OK syntax: packages/discord/fixtures/claim-worker.mjs
OK syntax: packages/discord/fixtures/legacy-owner-worker.mjs
OK syntax: scripts/discord.sh
OK syntax: scripts/discord-service.sh
OK packages/discord declares no dependencies
OK no bot-token-shaped string in packages/discord
OK fixture binding uses placeholder ids only
OK fixture binding validates
OK real pi with the extension exposes exactly list_dir, read_file, search and no built-in tool
OK real pi with a writable root exposes exactly the three reads plus write_file and edit_file, and writes nothing at start
OK real pi with a web key exposes the three reads plus web_fetch and web_search, and no write tool without a writable root
OK real pi with a git root exposes the reads, writes and the four git verbs, commits nothing at start, and never shows the token
OK real pi with protocol vault adds reserve_id to the git verbs
OK real pi with a setspark key exposes the reads and the eight record verbs, no counters, and never shows the key
OK real pi refuses a git key on a read-only root (fail closed)
OK real pi with the pilot flags (--no-tools) exposes no tool at all
OK real pi exits non-zero without MOSAIC_DISCORD_TOOLS: no session, no tools (fail closed)
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
OK node --test packages/discord/tests/ (ℹ pass 178)
OK scripts/discord.sh --help exits 0
OK scripts/discord.sh check without a binding exits 4
OK scripts/discord.sh recover without a binding exits 4
OK scripts/discord.sh reload without a binding exits 4
OK scripts/discord-service.sh without a command exits 4
OK service unit renders with the repository path, a supervised run as the main process, exit 3 never retried, and reload as SIGHUP
OK service install writes the rendered unit (0644) and leaves no temp file
OK service install a second time reports unchanged
OK systemd-analyze verify accepts the rendered unit
OK service uninstall removes the unit file
OK service install with an unknown flag exits 4
OK service install with USER unset finishes and names the account for lingering
discord suite: 66 passed, 0 failed
@@ -0,0 +1,21 @@
OK initial ordinary-file install
OK installed tree matches canonical source
OK installed tree has no symlinks
OK check detects installation drift
OK sync refuses to overwrite installation drift
OK check detects an extra destination file
OK check detects an extra destination directory
OK check rejects a destination symlink
OK sync accepts a canonical source update
OK updated installation matches canonical source
scripts/test-extension-package.sh: line 14: 3009858 Killed "$@" > /dev/null 2>&1
OK forced interruption kills the replacing process
OK next invocation recovers old consistent installation
OK interrupted replacement rolled back
OK sync succeeds after interruption recovery
OK unlocked stale lock file does not block
OK active lock refuses a concurrent sync
OK source symlink fails closed
OK nested second entrypoint fails closed
extension package selftest: 18 passed, 0 failed
@@ -0,0 +1,53 @@
toolchain: node v26.8.1, python 3.12.8, jsonschema 4.26.0
OK syntax: scripts/foundation-inspect.mjs
OK syntax: scripts/foundation/strict-json.mjs
OK syntax: scripts/foundation/canonical.mjs
OK syntax: scripts/foundation/resolve.mjs
OK syntax: scripts/foundation/validate-record.mjs
OK syntax: scripts/foundation/fixtures/build-fixtures.mjs
OK syntax: scripts/foundation/canonical.test.mjs
OK syntax: scripts/foundation/cli.test.mjs
OK syntax: scripts/foundation/fixtures.test.mjs
OK syntax: scripts/foundation/resolve.test.mjs
OK syntax: scripts/foundation/strict-json.test.mjs
OK syntax: scripts/foundation/verify-schema.py (ast only; no bytecode written)
OK fixture generator runs
OK checked-in fixtures/bundles equal a fresh generation
OK checked-in fixtures/raw equal a fresh generation
OK checked-in fixtures/index.json equal a fresh generation
OK checked-in demo bundles equal a fresh generation
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
OK node --test scripts/foundation/ (ℹ pass 80)
OK differential schema oracle: PASS: differential schema oracle (finite corpus; compatibility evidence, not equivalence proof)
platform witness: strftime('%Y') for year 999 -> '999' (pinned checker refuses years 0001..0999)
node v26.8.1; corpus 1568 records (38 pinned fixtures, 478 unique bundle records, 1052 typeCase/mutation/lexical cases)
schema column: agree-valid 540, agree-invalid 991, DISAGREEMENTS 0; strict-only (parser-bound) cases: 27; unsupported-kind records not schema-assessed by the inspector: 10
profile column (schema-valid records only): profile-valid 510, profile-invalid 30
profile refusals asserted: 30 schema-agreed-valid records refused only by the strict typed-string profile (rule profile-pattern-mismatch), 12 declared by name; 73 named probes verified against declared schema/profile columns
OK oracle: zero schema-column disagreements with the pinned checker
OK oracle: strict-only profile refusals are counted and asserted
OK demo: permitted read preview exits 0 (exit 0)
OK demo: permitted file.change preview exits 0 (exit 0)
OK demo: assignment.change proposal is unresolved (exit 3) (exit 3)
OK demo: revoked registration is refused (exit 3) (exit 3)
OK demo: message is not authority (exit 3) (exit 3)
OK usage: no arguments exits 2 (exit 2)
OK io: missing file exits 4 (exit 4)
OK io: directory exits 4 (exit 4)
OK io: symlink exits 4 (O_NOFOLLOW) (exit 4)
OK bound: oversize fixture exits 2 (exit 2)
OK profile: one final LF in a typed selection id is refused before admission (exit 2) (exit 2)
OK profile: two final LFs fail the schema pattern itself (exit 2) (exit 2)
OK profile: escaped newlines in free-form text stay allowed (exit 0) (exit 0)
OK profile refusal is invalid-request/profile-pattern-mismatch with selection and operation withheld, value not echoed
OK text output starts with the disclaimer
OK json output is valid JSON with result allowed and exactly the charter §7 fields
OK json golden matches byte-for-byte
OK sandboxed bundle run (env -i, PATH=/nonexistent) produced the unresolved proposal
OK sandbox inventory (path/type/size/mode/uid/gid/inode/mtime/sha256) unchanged by runs
OK canary never printed (bundle run and credential-file run)
OK a non-bundle JSON file is refused at the shape gate, not read into output
OK no field of the non-bundle file is echoed
selftest: 44 passed, 0 failed
@@ -0,0 +1,35 @@
toolchain: node v26.8.1, git version 2.55.0
OK syntax: packages/queue/src/cli.mjs
OK syntax: packages/queue/src/errors.mjs
OK syntax: packages/queue/src/io.mjs
OK syntax: packages/queue/src/lock.mjs
OK syntax: packages/queue/src/queue.mjs
OK syntax: packages/queue/src/review.mjs
OK syntax: packages/queue/src/store.mjs
OK syntax: packages/queue/tests/commit.test.mjs
OK syntax: packages/queue/tests/data.test.mjs
OK syntax: packages/queue/tests/dispatch.test.mjs
OK syntax: packages/queue/tests/helpers.mjs
OK syntax: packages/queue/tests/lock.test.mjs
OK syntax: packages/queue/tests/migration.test.mjs
OK syntax: packages/queue/tests/review.test.mjs
OK syntax: packages/queue/tests/store.test.mjs
OK syntax: packages/queue/tests/write.test.mjs
OK syntax: packages/queue/tests/fixtures/fake-gitea.mjs
OK syntax: packages/queue/tests/fixtures/kill-at.mjs
OK syntax: packages/queue/tests/fixtures/lock-child.mjs
OK syntax: packages/queue/tests/fixtures/mosaic-pre-a2.sh
OK syntax: scripts/queue-commit.sh
OK syntax: scripts/git-hooks/pre-commit
OK syntax: scripts/mosaic
OK queue-commit.sh, the guard and scripts/mosaic are executable
OK packages/queue declares no dependencies
ℹ tests 150
ℹ pass 150
ℹ fail 0
OK node --test packages/queue/tests/
OK scripts/mosaic queue help
skip queue verify and render --check: this checkout (/mnt/storage/scratch/rocko-r75) is not the queue's canonical root (/mnt/storage/src/mosaic-stack)
queue suite: 27 passed, 0 failed
@@ -0,0 +1,16 @@
OK valid RELEASE resolves (exit 0)
OK invalid RELEASE exits 1 (exit 1)
OK missing RELEASE exits 1 (exit 1)
OK valid RELEASE leaves image tag consistent with version
OK status safe on empty state (exit 0)
OK status created no pointer
OK fault-injected activation refuses (exit 1)
OK refused activation wrote no pointer
OK refusal logged exactly once with valid fields
OK healthy activation succeeds (exit 0)
OK pointer written with valid fields
OK repeat activation succeeds (log grows) (exit 0)
OK log is append-only across activations
OK rollback without previous refuses (exit 1)
selftest: 14 passed, 0 failed
@@ -0,0 +1,101 @@
OK valid task validates (exit 0)
OK unknown task key exits 2 (exit 2)
OK unsupported taskVersion exits 2 (exit 2)
OK invalid task id exits 2 (exit 2)
OK empty prompt exits 2 (exit 2)
OK NUL in expectExact exits 2 (exit 2)
OK out-of-range timeout exits 2 (exit 2)
OK missing mission file exits 4 (exit 4)
OK task with valid mission validates (exit 0)
OK invalid mission exits 2 (exit 2)
OK validate missing task exits 4 (exit 4)
OK validation does not modify the task file
OK prune dry-run exits 0 (exit 0)
OK dry-run deleted nothing
OK prune --keep=2 --yes removes oldest (exit 0)
OK kept exactly 2 newest runs
OK newest run kept, oldest pruned
OK append-only receipt written (3 entries)
OK sessions/workspaces untouched by prune
OK prune with invalid keep exits 4 (exit 4)
OK mock adapter: gate passes on matching mock response (exit 0)
OK mock adapter: expect-mismatch recorded (exit 1)
OK mismatch reason recorded
OK unknown adapter fails closed (exit 1)
OK mission task runs via mock adapter (exit 0)
OK mission section injected into generated prompt
OK retry of mission run succeeds (exit 0)
OK retriedFrom lineage recorded
OK mission section present after retry (relative path resolved)
OK retry of missing run exits 4 (exit 4)
OK skill install bundled ms-tools (exit 0)
OK installed to skills-available
OK double install refuses (exit 1)
OK activate enables skill (exit 0)
OK enabled dir populated
OK uninstall while enabled refuses (exit 1)
OK deactivate moves back to available (exit 0)
OK uninstall removes from available (exit 0)
OK seat with enabled skill launches
OK skill path delivered to adapter
OK shipped researcher contract resolves (ceiling + network)
OK resolve-role refuses a non-role document (conductor policy) (exit 2)
OK resolve-role refuses name/filename mismatch (exit 2)
OK resolve-role refuses unknown network declaration (exit 2)
OK resolve-role refuses duplicate role tool (exit 2)
OK resolve-role refuses unsupported tool (exit 2)
OK resolve-role refuses missing contract file (exit 4)
OK version 1 role prints no contract line
OK shipped version 2 roles resolve with their contracts (pm, cto, coder, reviewer)
OK resolve-role accepts a minimal version 2 role (exit 0)
OK resolve-role refuses an action outside the vocabulary (exit 2)
OK resolve-role refuses a gated-only action in a role file (exit 2)
OK resolve-role refuses a Vikunja verb no role may hold (exit 2)
OK resolve-role refuses a version 2 role whose contract is missing (exit 2)
OK resolve-role refuses a contract that is a symbolic link (exit 2)
OK seat launches under role ceiling
OK role ceiling narrows seat tools (read,write,bash -> read,bash)
OK narrowing recorded loudly
OK missing role contract refuses launch (exit 2)
OK missing-contract refusal names the role
OK empty ceiling intersection -> tool-free seat
OK tool-free outcome recorded loudly
OK overridden agents dir without seat definition refuses (exit 4)
OK seat-resolution refusal names the seat
OK policy: mission only -> mission tools
OK policy: task only -> task tools
OK policy: both -> intersection (task narrowed)
OK policy: empty intersection -> tool-free
OK invalid mission capabilities rejected (exit 2)
mosaic-task: unsupported mission tool: "sudo" (supported: read, write, edit, bash, grep, find, ls)
OK task run with user layer present (exit 0)
OK user context dispatched into generated prompt
OK workspace+tools task runs via mock (exit 0)
OK workspace path + tools delivered to adapter
OK persistent workspace created on host
OK plain task still runs (no workspace/tools) (exit 0)
OK workspace var present but empty when absent
OK tools empty when absent
OK unknown tool exits 2 (exit 2)
OK workspace traversal exits 2 (exit 2)
OK live hello task succeeds with exact marker
OK result.json written in run dir
OK result.json contents are correct
OK wrong expectExact fails with exit 1 (reason: expect-mismatch)
OK each run gets a distinct run dir (no clobber)
OK list shows both runs
OK fork base: teach succeeds (exit 0)
OK fork child recalls ancestor context (exit 0)
OK forked child recalled ancestor code word
OK ancestor session untouched by fork
OK child session dir has its own branch file
OK onboard without name exits 4 (non-interactive) (exit 4)
OK onboard --name renders profile (exit 0)
OK profile written
OK canon structure: required filled, optional placeholdered
OK canon sections present
OK user recall run succeeds
OK recalled user name (response: Jason)
OK no agent identity on headless run
selftest: 98 passed, 0 failed
@@ -0,0 +1,59 @@
✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (11674.789973ms)
Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286}
✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (13706.759161ms)
✔ S5 pages in a browser: real broker rows, inert text, Copy, routes, failures, layout and no writes (14648.970608ms)
✔ bus routes serve the four reads from the reader view, unescaped JSON for the page to escape (394.51471ms)
✔ decisions route serves the roles and recent lists from the reader view (222.487933ms)
✔ bus routes are GET-only, have no write verb, and keep the same-origin checks (387.603619ms)
✔ bus refusals map to statuses and never carry a path or a stack (78.352185ms)
✔ humanCall speaks the human transport protocol without blocking the server (3140.208613ms)
✔ busReader takes --business, else the live bus host, else refuses (1268.741589ms)
✔ humanCall kills a transport that runs past its timeout (689.727705ms)
✔ serve refuses a --business value that is not a business id (1230.463868ms)
✔ conversation view: full history, collapsed tools, hidden thinking, inert hostile content, malformed and reconcile markers (10519.34976ms)
✔ conversation view: a fork keeps the open branch, says so, and opens the new one on request (4307.797103ms)
✔ conversation view: a newer session with no readable history keeps the marker (2145.820226ms)
✔ conversation view: seats without history say so and offer no reply (1639.322949ms)
✔ Discord row through real board/WebUI: independent brake/liveness, no Reply, literal content (10745.517796ms)
✔ return flow through the conversation view: send, tool call, delayed result, peer message, exact long answers, relaunch (62200.059017ms)
✔ redactor: config, dataRoot, any absolute path, Discord ids and the JSON parser quote (3.112119ms)
✔ credential state from the date alone, as the bus states it (0.367852ms)
✔ queue: rows, one row, ids and methods, notes; no path or id from a row (3294.622674ms)
✔ queue: a refused read fails closed with the store's text, redacted (1038.09559ms)
✔ queue: a store that fails to load, or a child that dies, gives a fixed message with no path or stack (5104.478556ms)
✔ business: names, vars on the allowlist, authority, credential metadata only (60.140853ms)
✔ business: missing or invalid file refuses with the module's text, redacted (52.456468ms)
✔ business without --business: the live bus host, else no-bus-host; no system config: not-configured (58.34462ms)
✔ settings: release, board, loopback address, notifier binding name only (1081.077431ms)
✔ both presentations replace old activity with relaunch notice, label retained history, then resume after new activity (11148.260802ms)
✔ reported return flow and relative Age: reply sent from the inspector, then the new answer appears there without manual refresh (28830.543544ms)
✔ loopback host and board origin fail closed (24.245814ms)
✔ real board fixture passes through WebUI; assets and isolated seen/reply work (561.19726ms)
✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (321.722171ms)
✔ unreachable board reports URL; CLI rejects unsupported options (2796.028317ms)
✔ render prints the unit without the preview flags (75.26698ms)
✔ render carries every serve flag, the preview set included (112.407931ms)
✔ a partial preview set is refused (197.407027ms)
✔ a value the unit cannot carry is refused, for every flag (3876.079297ms)
✔ the rendered unit names none of the human proof agent markers and sets no agent variable (315.404716ms)
✔ install into --dir with --no-reload writes the file whole, then reports unchanged (682.03444ms)
✔ install without --no-reload runs daemon-reload and nothing that starts the unit (588.619294ms)
✔ uninstall refuses while the unit is active, and removes it once it is not (987.496602ms)
✔ status prints the unit state and GET /healthz on the installed port (1510.367271ms)
✔ usage: an unknown verb or flag, and start or enable, exit 4 (285.484472ms)
✔ tokens.css and icons.svg are byte copies of docs/design (4.747715ms)
✔ JetBrains Mono files match the digests in their sources file, with the OFL text (8.967146ms)
✔ shipped strings are unchanged, and the web keeps its write boundary (8.310162ms)
✔ shell in a browser: System default, seeded states, 400px, focus and the Ctrl+K palette (17866.768235ms)
✔ five states on Board, Inbox, Tasks, Agents and Trail (6193.108566ms)
✔ Queue, Business and Settings in their five states, with no move button (14895.192654ms)
✔ seeded secrets, Discord ids and paths reach no response and no page; business refusals in the module's words (3021.179379ms)
✔ the server log names no config path when the system config refuses (691.292426ms)
ℹ tests 50
ℹ suites 0
ℹ pass 50
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 63231.404253
@@ -0,0 +1,18 @@
6dcdc74e02a3f49c7fed27c21860508cb655980ac71d569c96951ac013145884 ./base.txt
0f35cd1953b1038b488347e9df1b4309064fa869e1c14684370753a519b09607 ./BUILD.md
27cc48485f92764cf9a4f92a746c209d1e30dd511064cdf8071858d00c62b5a9 ./build.patch
7dc571dbe87a484f465c82e1965e499c57d9af047bfe702faa3fe041138dd154 ./candidate-manifest.sha256
896d274d49f93a194d36a2cf081574569140f8ab78ff5dead6737ab9619d0a8f ./files.txt
94e11500657683c821e50aaf9d5e746a26fc09b1321020fd4bf9b4e2c20dd533 ./out/gate/end.txt
a2171dad35389dbd0260fd8fcc70611288eca6fdff4292632e3de50ab14800ef ./out/gate/start.txt
573c60b32cb048dbe07739738eaeaa300b7a72d08764f23d63454d8154d582ae ./out/gate/summary.txt
f91fc95a173339137f981e24fbf630acfde957e32c3cd71e28304532083c6a77 ./out/gate/test-auth.log
1c464d617a1e10ef15a275cc9beeaf975d27de84d05071231970ca76af5d81fd ./out/gate/test-conductor.log
52d211444489ea729dafc3c9745d4474d9dfd50f41055cbad792424197f04073 ./out/gate/test-config.log
7d5016ded1994048642f8b9ae845e52e8af4a069ba05c056c8f580a4589bacbe ./out/gate/test-discord.log
62d2769554912857c4020cd38af23d8711134805dba60983433bd394afc14289 ./out/gate/test-extension-package.log
83dfaa21c4e4ad941088efab3f69e87ebd2f6bd3cc72ede938e21dc06da8a34f ./out/gate/test-foundation.log
4d3f40bf6a83fca6aed9cab44c2c130ae90c0d2d4943a3a2b51e9141ada71f0c ./out/gate/test-queue.log
fa832794ee35025fb4559d58d01a270f6bff2eb65e863e9075b797f47fa68545 ./out/gate/test-release.log
ccea37f4ffde1f160dcd1a1864988083a645d974e892065c956b338d0f8c853a ./out/gate/test-task.log
c0728d5190cd13bb0bee5d39bddc12776f2f8c724a28faee358ae81a034e1469 ./out/gate/webui.log