webui: the unit script takes --send (#1576)
Row 83. scripts/webui-service.sh render and install take --send, only
with the full preview set; otherwise the script refuses before writing
anything. With --send the rendered ExecStart ends in --send; renders
without it are byte-identical to the base. status says whether the
installed unit sends. The script header, install message, README and
TOOLS.md carry the decision 102 start wording: Jason or Mos starts and
restarts the Console, no other agent.
Dewey's round 1, candidate manifest f764cd9e, base d13b9300. Approved
by Darkwing (27721) and Filbert (27722). Sage's gate green 04:07:30Z to
04:10:47Z.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -100,8 +100,15 @@ Under `--send` the first of those can also send (next section).
|
||||
|
||||
To run the Console as a systemd user unit, `scripts/webui-service.sh`
|
||||
renders and installs one with these flags (row 75; `docs/TOOLS.md`
|
||||
"Console service"). Jason starts it from his own shell (lead decision 94).
|
||||
The script takes no `--send`, so a unit it installs is read-only.
|
||||
"Console service"). Jason starts and restarts it, and so may Mos under lead
|
||||
decision 102, which accepts that sends from a Console Mos started enter the
|
||||
trail as Jason. Any other agent starting it works around the human proof
|
||||
(lead decisions 62 and 94). Without `--send` the unit is read-only. The script takes `--send` (row 83)
|
||||
only with the three preview flags; otherwise it exits 1 and writes nothing.
|
||||
With it, the unit's ExecStart ends with `--send`, and the Console needs the
|
||||
system config at start: without it serve refuses with `--send needs the bus
|
||||
configured` and exits 2, which the unit doesn't restart. `status` says
|
||||
whether the installed unit sends.
|
||||
|
||||
## Message a role (row 76, #1570)
|
||||
|
||||
@@ -113,9 +120,11 @@ node packages/webui/src/cli.mjs serve --port 7332 \
|
||||
`--send` is off by default. It goes only with the three preview flags and
|
||||
refuses at start without them. It also needs the system config: without it
|
||||
the start refuses with `--send needs the bus configured`, where a read-only
|
||||
Console would still serve the board. With `--send` the Inbox has a "Message
|
||||
a role" box: a role picker filled from the agents read, a text box and
|
||||
Send. The message reaches the role as a message from the human, class INFO,
|
||||
Console would still serve the board. A unit that sends comes from
|
||||
`scripts/webui-service.sh install` with `--send` (above). With `--send` the
|
||||
Inbox has a "Message a role" box: a role picker filled from the agents read,
|
||||
a text box and Send. The message reaches the role as a message from the
|
||||
human, class INFO,
|
||||
through `message.send` on the human transport. It goes the same way as one
|
||||
sent from the CLI, with class INFO where `mosaic talk` uses REQUEST (lead
|
||||
decision 95). Decisions are still answered in a
|
||||
|
||||
@@ -11,10 +11,35 @@ 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.
|
||||
// Row 83 (#1576) adds --send, taken only with the preview set.
|
||||
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'];
|
||||
const PREVIEW_FLAGS = '--public-host console.example.com --auth-header X-authentik-uid --allow-user jason';
|
||||
const SEND_REFUSAL = /^webui-service: --send needs the preview mode \(--public-host, --auth-header and --allow-user\)$/m;
|
||||
const TEMPLATE = readFileSync(join(REPO, 'packages', 'webui', 'systemd', 'mosaic-webui.service.in'), 'utf8');
|
||||
|
||||
// The unit as the base script renders it (row 75): the template with its four
|
||||
// placeholders filled in, nothing else. Row 83 adds --send and must leave
|
||||
// every unit rendered without it byte for byte as this.
|
||||
function filled(flags) {
|
||||
const base = '/usr/local/bin:/usr/bin:/bin';
|
||||
const path = `:${base}:`.includes(`:${NODE_DIR}:`) ? base : `${NODE_DIR}:${base}`;
|
||||
return TEMPLATE.replaceAll('@REPO@', REPO).replaceAll('@PATH@', path).replaceAll('@NODE@', process.execPath).replaceAll('@FLAGS@', flags);
|
||||
}
|
||||
|
||||
// A loopback port with nothing listening, so status's /healthz request
|
||||
// reaches no real Console.
|
||||
async function closedPort() {
|
||||
const server = createServer();
|
||||
server.listen(0, '127.0.0.1');
|
||||
await once(server, 'listening');
|
||||
const { port } = server.address();
|
||||
server.close();
|
||||
await once(server, 'close');
|
||||
return port;
|
||||
}
|
||||
|
||||
function tmp(t) {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'webui-service-'));
|
||||
@@ -114,7 +139,7 @@ test('the rendered unit names none of the human proof agent markers and sets no
|
||||
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']]) {
|
||||
for (const args of [[], [...PREVIEW, '--business', 'mosaic'], [...PREVIEW, '--send']]) {
|
||||
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);
|
||||
@@ -212,9 +237,129 @@ test('status prints the unit state and GET /healthz on the installed port', asyn
|
||||
|
||||
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']]) {
|
||||
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'], ['render', '--send', 'yes', ...PREVIEW]]) {
|
||||
const r = await run(dir, args);
|
||||
assert.equal(r.status, 4, args.join(' '));
|
||||
assert.match(r.stderr, /^usage: scripts\/webui-service\.sh/);
|
||||
}
|
||||
});
|
||||
|
||||
test('without --send, render is the template with its placeholders filled, byte for byte', async (t) => {
|
||||
const dir = tmp(t);
|
||||
for (const [args, flags] of [
|
||||
[[], '--port 7332'],
|
||||
[['--port', '7340'], '--port 7340'],
|
||||
[['--business', 'mosaic'], '--port 7332 --business mosaic'],
|
||||
[PREVIEW, `--port 7332 ${PREVIEW_FLAGS}`],
|
||||
[['--port', '7340', '--business', 'mosaic.dev_1', '--board', 'http://127.0.0.1:7331', ...PREVIEW], `--port 7340 --business mosaic.dev_1 --board http://127.0.0.1:7331 ${PREVIEW_FLAGS}`],
|
||||
]) {
|
||||
const r = await run(dir, ['render', ...args]);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.equal(r.stdout, filled(flags), args.join(' '));
|
||||
}
|
||||
});
|
||||
|
||||
test('render --send with the preview set ends ExecStart with --send and changes nothing else', async (t) => {
|
||||
const dir = tmp(t);
|
||||
const extra = ['--port', '7340', '--business', 'mosaic'];
|
||||
for (const args of [[...PREVIEW, '--send'], ['--send', ...PREVIEW], [...PREVIEW.slice(0, 2), '--send', ...PREVIEW.slice(2)], ['--send', ...extra, ...PREVIEW]]) {
|
||||
const plain = args.filter((a) => a !== '--send');
|
||||
const flags = `${plain.includes('--port') ? '--port 7340 --business mosaic' : '--port 7332'} ${PREVIEW_FLAGS}`;
|
||||
const r = await run(dir, ['render', ...args]);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.equal(r.stdout, filled(`${flags} --send`), args.join(' '));
|
||||
assert.ok(execStart(r.stdout).endsWith(' --allow-user jason --send'), execStart(r.stdout));
|
||||
const without = await run(dir, ['render', ...plain]);
|
||||
assert.equal(without.status, 0, without.stderr);
|
||||
const a = r.stdout.split('\n'), b = without.stdout.split('\n');
|
||||
assert.equal(a.length, b.length);
|
||||
assert.deepEqual(a.map((l, i) => (l === b[i] ? null : [l, b[i]])).filter(Boolean), [[`${execStart(without.stdout)} --send`, execStart(without.stdout)]]);
|
||||
}
|
||||
});
|
||||
|
||||
test('--send without the whole preview set is refused and writes nothing', async (t) => {
|
||||
const dir = tmp(t);
|
||||
const pairs = [PREVIEW.slice(0, 2), PREVIEW.slice(2, 4), PREVIEW.slice(4, 6)];
|
||||
const partial = [[], ['--port', '7340'], ['--business', 'mosaic'], ...pairs, [...pairs[0], ...pairs[1]], [...pairs[0], ...pairs[2]], [...pairs[1], ...pairs[2]]];
|
||||
for (const args of partial) {
|
||||
const r = await run(dir, ['render', '--send', ...args]);
|
||||
assert.equal(r.status, 1, args.join(' '));
|
||||
assert.equal(r.stdout, '', args.join(' '));
|
||||
assert.match(r.stderr, SEND_REFUSAL, args.join(' '));
|
||||
}
|
||||
const bin = standIn(dir);
|
||||
const fresh = join(dir, 'never');
|
||||
for (const args of partial) {
|
||||
const r = await run(dir, ['install', '--dir', fresh, '--no-reload', ...args, '--send'], bin);
|
||||
assert.equal(r.status, 1, args.join(' '));
|
||||
assert.match(r.stderr, SEND_REFUSAL, args.join(' '));
|
||||
}
|
||||
assert.ok(!existsSync(fresh), 'a refusal creates no directory');
|
||||
const unitDir = join(dir, 'units');
|
||||
const target = join(unitDir, 'mosaic-webui.service');
|
||||
assert.equal((await run(dir, ['install', '--dir', unitDir, '--no-reload', ...PREVIEW], bin)).status, 0);
|
||||
const before = readFileSync(target, 'utf8');
|
||||
const r = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--send', ...PREVIEW.slice(0, 4)], bin);
|
||||
assert.equal(r.status, 1);
|
||||
assert.equal(readFileSync(target, 'utf8'), before, 'the installed unit is left alone');
|
||||
assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service']);
|
||||
assert.ok(!existsSync(join(dir, 'calls.txt')), 'no systemctl call');
|
||||
});
|
||||
|
||||
test('install --send into --dir with --no-reload writes the sending unit, and status says whether it sends', async (t) => {
|
||||
const dir = tmp(t);
|
||||
const unitDir = join(dir, 'units');
|
||||
const target = join(unitDir, 'mosaic-webui.service');
|
||||
const port = await closedPort();
|
||||
const flags = `--port ${port} ${PREVIEW_FLAGS}`;
|
||||
const r = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW, '--send']);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.match(r.stdout, new RegExp(`^written: ${target}$`, 'm'));
|
||||
// Lead decision 102: Jason or Mos starts it, no other agent.
|
||||
assert.match(r.stdout, /^this script starts nothing: .* Jason\nstarts and restarts it, or Mos under lead decision 102 .*\ntrail as Jason\); no other agent does:$/m);
|
||||
assert.match(r.stdout, new RegExp(`^serve: \\S+ ${REPO.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/packages/webui/src/cli\\.mjs serve ${flags} --send$`, 'm'));
|
||||
assert.equal(readFileSync(target, 'utf8'), filled(`${flags} --send`));
|
||||
assert.equal(statSync(target).mode & 0o777, 0o644);
|
||||
assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service']);
|
||||
const bin = standIn(dir);
|
||||
const sending = await run(dir, ['status', '--dir', unitDir], bin);
|
||||
assert.equal(sending.status, 0, sending.stderr);
|
||||
assert.match(sending.stdout, /^send: on \(--send; human messages to roles\)$/m);
|
||||
assert.match(sending.stdout, /^healthz: \S+ unreachable \(ECONNREFUSED\)$/m);
|
||||
const again = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port), '--send', ...PREVIEW], bin);
|
||||
assert.match(again.stdout, new RegExp(`^unchanged: ${target}$`, 'm'));
|
||||
const readOnly = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW], bin);
|
||||
assert.match(readOnly.stdout, new RegExp(`^written: ${target}$`, 'm'));
|
||||
assert.equal(readFileSync(target, 'utf8'), filled(flags));
|
||||
const off = await run(dir, ['status', '--dir', unitDir], bin);
|
||||
assert.match(off.stdout, /^send: off \(read-only\)$/m);
|
||||
assert.doesNotMatch(off.stdout, /^send: on/m);
|
||||
// The only systemctl calls are status's reads; install ran none.
|
||||
assert.deepEqual(readFileSync(join(dir, 'calls.txt'), 'utf8').split('\n').filter(Boolean).filter((c) => !/^--user (is-enabled|is-active|show) mosaic-webui\.service/.test(c)), []);
|
||||
const none = await run(dir, ['status', '--dir', join(dir, 'empty')], bin);
|
||||
assert.doesNotMatch(none.stdout, /^send:/m, 'no unit, no send line');
|
||||
// serve takes --send as an allowed user, so ExecStart can end with
|
||||
// `--allow-user --send` on a unit that doesn't send.
|
||||
const oddUser = [...PREVIEW.slice(0, 4), '--allow-user', '--send'];
|
||||
for (const [args, ending, state] of [[oddUser, '--allow-user --send', 'off'], [[...oddUser, '--send'], '--allow-user --send --send', 'on']]) {
|
||||
const r = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port), ...args]);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.ok(execStart(readFileSync(target, 'utf8')).endsWith(` ${ending}`), args.join(' '));
|
||||
assert.match((await run(dir, ['status', '--dir', unitDir], bin)).stdout, new RegExp(`^send: ${state} `, 'm'), args.join(' '));
|
||||
}
|
||||
});
|
||||
|
||||
test('uninstall and status take no --send', async (t) => {
|
||||
const dir = tmp(t);
|
||||
const bin = standIn(dir);
|
||||
const unitDir = join(dir, 'units');
|
||||
assert.equal((await run(dir, ['install', '--dir', unitDir, '--no-reload', ...PREVIEW, '--send'])).status, 0);
|
||||
for (const args of [['uninstall', '--dir', unitDir, '--send'], ['uninstall', '--dir', unitDir, '--no-reload', '--send'], ['status', '--dir', unitDir, '--send']]) {
|
||||
const r = await run(dir, args, bin);
|
||||
assert.equal(r.status, 4, args.join(' '));
|
||||
assert.match(r.stderr, /^usage: scripts\/webui-service\.sh/);
|
||||
assert.equal(r.stdout, '', args.join(' '));
|
||||
}
|
||||
assert.ok(existsSync(join(unitDir, 'mosaic-webui.service')), 'still installed');
|
||||
assert.ok(!existsSync(join(dir, 'calls.txt')), 'no systemctl call');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user