diff --git a/agents/rocko/work/queue-75/BUILD.md b/agents/rocko/work/queue-75/BUILD.md new file mode 100644 index 000000000..cf5d360cc --- /dev/null +++ b/agents/rocko/work/queue-75/BUILD.md @@ -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:/healthz`: + the status code and body, or `unreachable ()`. 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. diff --git a/agents/rocko/work/queue-75/base.txt b/agents/rocko/work/queue-75/base.txt new file mode 100644 index 000000000..3d65f1350 --- /dev/null +++ b/agents/rocko/work/queue-75/base.txt @@ -0,0 +1 @@ +43d2328f3a413b066f435c13cacd4df462670c57 diff --git a/agents/rocko/work/queue-75/build.patch b/agents/rocko/work/queue-75/build.patch new file mode 100644 index 000000000..2b0c19e1d --- /dev/null +++ b/agents/rocko/work/queue-75/build.patch @@ -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 </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 diff --git a/agents/rocko/work/queue-75/candidate-manifest.sha256 b/agents/rocko/work/queue-75/candidate-manifest.sha256 new file mode 100644 index 000000000..a0964c13c --- /dev/null +++ b/agents/rocko/work/queue-75/candidate-manifest.sha256 @@ -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 diff --git a/agents/rocko/work/queue-75/files.txt b/agents/rocko/work/queue-75/files.txt new file mode 100644 index 000000000..85d34b30a --- /dev/null +++ b/agents/rocko/work/queue-75/files.txt @@ -0,0 +1,4 @@ +docs/TOOLS.md +packages/webui/systemd/mosaic-webui.service.in +packages/webui/tests/service.test.mjs +scripts/webui-service.sh diff --git a/agents/rocko/work/queue-75/out/gate/end.txt b/agents/rocko/work/queue-75/out/gate/end.txt new file mode 100644 index 000000000..4e9659c10 --- /dev/null +++ b/agents/rocko/work/queue-75/out/gate/end.txt @@ -0,0 +1 @@ +2026-10-11T22:32:03Z diff --git a/agents/rocko/work/queue-75/out/gate/start.txt b/agents/rocko/work/queue-75/out/gate/start.txt new file mode 100644 index 000000000..b6fb87561 --- /dev/null +++ b/agents/rocko/work/queue-75/out/gate/start.txt @@ -0,0 +1 @@ +2026-10-11T22:25:46Z diff --git a/agents/rocko/work/queue-75/out/gate/summary.txt b/agents/rocko/work/queue-75/out/gate/summary.txt new file mode 100644 index 000000000..67fb18179 --- /dev/null +++ b/agents/rocko/work/queue-75/out/gate/summary.txt @@ -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 diff --git a/agents/rocko/work/queue-75/out/gate/test-auth.log b/agents/rocko/work/queue-75/out/gate/test-auth.log new file mode 100644 index 000000000..ac2287606 --- /dev/null +++ b/agents/rocko/work/queue-75/out/gate/test-auth.log @@ -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 diff --git a/agents/rocko/work/queue-75/out/gate/test-conductor.log b/agents/rocko/work/queue-75/out/gate/test-conductor.log new file mode 100644 index 000000000..d0daf41bb --- /dev/null +++ b/agents/rocko/work/queue-75/out/gate/test-conductor.log @@ -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 + +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 + +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 diff --git a/agents/rocko/work/queue-75/out/gate/test-config.log b/agents/rocko/work/queue-75/out/gate/test-config.log new file mode 100644 index 000000000..76c05ef74 --- /dev/null +++ b/agents/rocko/work/queue-75/out/gate/test-config.log @@ -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 diff --git a/agents/rocko/work/queue-75/out/gate/test-discord.log b/agents/rocko/work/queue-75/out/gate/test-discord.log new file mode 100644 index 000000000..b013f9ffa --- /dev/null +++ b/agents/rocko/work/queue-75/out/gate/test-discord.log @@ -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 diff --git a/agents/rocko/work/queue-75/out/gate/test-extension-package.log b/agents/rocko/work/queue-75/out/gate/test-extension-package.log new file mode 100644 index 000000000..ee559c2a5 --- /dev/null +++ b/agents/rocko/work/queue-75/out/gate/test-extension-package.log @@ -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 diff --git a/agents/rocko/work/queue-75/out/gate/test-foundation.log b/agents/rocko/work/queue-75/out/gate/test-foundation.log new file mode 100644 index 000000000..6338e1e6d --- /dev/null +++ b/agents/rocko/work/queue-75/out/gate/test-foundation.log @@ -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 diff --git a/agents/rocko/work/queue-75/out/gate/test-queue.log b/agents/rocko/work/queue-75/out/gate/test-queue.log new file mode 100644 index 000000000..8b03cd793 --- /dev/null +++ b/agents/rocko/work/queue-75/out/gate/test-queue.log @@ -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 diff --git a/agents/rocko/work/queue-75/out/gate/test-release.log b/agents/rocko/work/queue-75/out/gate/test-release.log new file mode 100644 index 000000000..7202b85d4 --- /dev/null +++ b/agents/rocko/work/queue-75/out/gate/test-release.log @@ -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 diff --git a/agents/rocko/work/queue-75/out/gate/test-task.log b/agents/rocko/work/queue-75/out/gate/test-task.log new file mode 100644 index 000000000..df6724a77 --- /dev/null +++ b/agents/rocko/work/queue-75/out/gate/test-task.log @@ -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 diff --git a/agents/rocko/work/queue-75/out/gate/webui.log b/agents/rocko/work/queue-75/out/gate/webui.log new file mode 100644 index 000000000..e41e9ba7c --- /dev/null +++ b/agents/rocko/work/queue-75/out/gate/webui.log @@ -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 diff --git a/agents/rocko/work/queue-75/packet-manifest.sha256 b/agents/rocko/work/queue-75/packet-manifest.sha256 new file mode 100644 index 000000000..1287e32a8 --- /dev/null +++ b/agents/rocko/work/queue-75/packet-manifest.sha256 @@ -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