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:
2026-10-11 23:10:55 -05:00
co-authored by Claude Opus 5.5
parent 23d3841354
commit 1a8ba58737
4 changed files with 202 additions and 22 deletions
+14 -7
View File
@@ -246,17 +246,22 @@ scripts/webui-service.sh install [--dir DIR] [--no-reload] [FLAGS]
scripts/webui-service.sh uninstall [--dir DIR] [--no-reload] scripts/webui-service.sh uninstall [--dir DIR] [--no-reload]
scripts/webui-service.sh status [--dir DIR] scripts/webui-service.sh status [--dir DIR]
# FLAGS: [--port N] [--business ID] [--board URL] # FLAGS: [--port N] [--business ID] [--board URL]
# [--public-host NAME --auth-header HEADER --allow-user ID] # [--public-host NAME --auth-header HEADER --allow-user ID [--send]]
``` ```
The systemd user unit `mosaic-webui.service` runs the Console, The systemd user unit `mosaic-webui.service` runs the Console,
`node packages/webui/src/cli.mjs serve`, with serve's flags as given: `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 `--port` defaults to 7332 (7330 is the operator's own Console), and the
preview set goes all three or none. The template is preview set goes all three or none. `--send` (row 83) goes only with the
preview set and is refused otherwise (exit 1, nothing written); ExecStart
then ends with `--send`, and the Console needs the system config at start
(`--send needs the bus configured`, exit 2). Without `--send` the unit is
read-only. The template is
`packages/webui/systemd/mosaic-webui.service.in`. `install` writes a temp `packages/webui/systemd/mosaic-webui.service.in`. `install` writes a temp
file, renames it and runs `systemctl --user daemon-reload`; `uninstall` file, renames it and runs `systemctl --user daemon-reload`; `uninstall`
refuses while the unit is active; `status` prints the unit state and refuses while the unit is active; `status` prints the unit state, whether
`GET /healthz` on the installed port. Each value is held to a plain the installed unit sends, and `GET /healthz` on the installed port. Each
value is held to a plain
character set the unit can carry (no space, `%`, `$`, quote, backslash, character set the unit can carry (no space, `%`, `$`, quote, backslash,
`&` or `|`) and refused otherwise. The unit sets no `MOSAIC_*`, `CLAUDE*`, `&` or `|`) and refused otherwise. The unit sets no `MOSAIC_*`, `CLAUDE*`,
`CODEX*` or `PI_*` variable. Serve's exit 2 (usage, config, or a port `CODEX*` or `PI_*` variable. Serve's exit 2 (usage, config, or a port
@@ -264,9 +269,11 @@ already in use) is not restarted.
There is no start, stop, enable or restart verb. The Console reads the bus 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 through the human transport, so whoever starts it holds the human's
capability; an agent starting it, even through systemd, works around the capability. Jason starts and restarts it
human proof (lead decisions 62 and 94). Jason starts it from his own shell: (`systemctl --user enable --now mosaic-webui`), and so may Mos under lead
`systemctl --user enable --now mosaic-webui`. decision 102, which accepts that sends from a Console Mos started enter the
trail as Jason (`human-cli`). Any other agent starting it, even through
systemd, works around the human proof (lead decisions 62 and 94).
Exit codes: `0` ok · `1` failed or refused · `4` usage. Exit codes: `0` ok · `1` failed or refused · `4` usage.
## Discord connector (`scripts/discord.sh`) ## Discord connector (`scripts/discord.sh`)
+14 -5
View File
@@ -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` 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` renders and installs one with these flags (row 75; `docs/TOOLS.md`
"Console service"). Jason starts it from his own shell (lead decision 94). "Console service"). Jason starts and restarts it, and so may Mos under lead
The script takes no `--send`, so a unit it installs is read-only. 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) ## 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 `--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 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 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 Console would still serve the board. A unit that sends comes from
a role" box: a role picker filled from the agents read, a text box and `scripts/webui-service.sh install` with `--send` (above). With `--send` the
Send. The message reaches the role as a message from the human, class INFO, 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 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 sent from the CLI, with class INFO where `mosaic talk` uses REQUEST (lead
decision 95). Decisions are still answered in a decision 95). Decisions are still answered in a
+147 -2
View File
@@ -11,10 +11,35 @@ import { fileURLToPath } from 'node:url';
// scripts/webui-service.sh (row 75, #1569). Nothing here starts a unit or // 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 // 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. // 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 REPO = fileURLToPath(new URL('../../..', import.meta.url)).replace(/\/$/, '');
const SCRIPT = join(REPO, 'scripts', 'webui-service.sh'); const SCRIPT = join(REPO, 'scripts', 'webui-service.sh');
const NODE_DIR = dirname(process.execPath); const NODE_DIR = dirname(process.execPath);
const PREVIEW = ['--public-host', 'console.example.com', '--auth-header', 'X-authentik-uid', '--allow-user', 'jason']; 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) { function tmp(t) {
const dir = mkdtempSync(join(tmpdir(), 'webui-service-')); 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 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]); 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(' ')); 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]); const r = await run(dir, ['render', ...args]);
assert.equal(r.status, 0, r.stderr); assert.equal(r.status, 0, r.stderr);
for (const marker of markers) assert.ok(!r.stdout.includes(marker), marker); 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) => { test('usage: an unknown verb or flag, and start or enable, exit 4', async (t) => {
const dir = tmp(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); const r = await run(dir, args);
assert.equal(r.status, 4, args.join(' ')); assert.equal(r.status, 4, args.join(' '));
assert.match(r.stderr, /^usage: scripts\/webui-service\.sh/); 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');
});
+27 -8
View File
@@ -11,19 +11,24 @@
# rename) and run `systemctl --user daemon-reload` # rename) and run `systemctl --user daemon-reload`
# uninstall [--dir DIR] [--no-reload] # uninstall [--dir DIR] [--no-reload]
# remove the unit; refuses while it is active # remove the unit; refuses while it is active
# status [--dir DIR] unit state, then GET /healthz on its port # status [--dir DIR] unit state, whether it sends, then GET
# /healthz on its port
# #
# FLAGS are serve's: --port N (default 7332; 7330 is the operator's own # 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 # Console), --business ID, --board URL, and the preview set --public-host
# NAME --auth-header HEADER --allow-user ID, all three or none. systemd # NAME --auth-header HEADER --allow-user ID, all three or none, and --send
# (row 83), taken only with the preview set: the unit's Console then sends
# human messages to roles and needs the system config at start. systemd
# expands % and $ and splits on spaces, so each value is held to a plain # expands % and $ and splits on spaces, so each value is held to a plain
# character set and refused otherwise. None is a secret. # character set and refused otherwise. None is a secret.
# #
# There is no start, stop, enable or restart verb. The Console reads the # 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 # bus through the human transport, so whoever starts it holds the human's
# capability. An agent starting it, even detached through systemd, works # capability. Jason starts and restarts it
# around the human proof (lead decisions 62 and 94). Jason starts it from # (`systemctl --user enable --now mosaic-webui`), and so may Mos under lead
# his own shell: `systemctl --user enable --now mosaic-webui`. # decision 102, which accepts that sends from a Console Mos started enter
# the trail as Jason (human-cli). Any other agent starting it, even detached
# through systemd, works around the human proof (lead decisions 62 and 94).
# #
# Exit codes: 0 ok, 1 operation failed, 4 usage. # Exit codes: 0 ok, 1 operation failed, 4 usage.
set -euo pipefail set -euo pipefail
@@ -31,7 +36,7 @@ REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TEMPLATE="$REPO/packages/webui/systemd/mosaic-webui.service.in" TEMPLATE="$REPO/packages/webui/systemd/mosaic-webui.service.in"
UNIT_NAME="mosaic-webui.service" 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] 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]" FLAGS: [--port N] [--business ID] [--board URL] [--public-host NAME --auth-header HEADER --allow-user ID [--send]]"
die() { echo "webui-service: $*" >&2; exit 1; } die() { echo "webui-service: $*" >&2; exit 1; }
usage() { echo "$USAGE" >&2; exit 4; } usage() { echo "$USAGE" >&2; exit 4; }
@@ -45,6 +50,7 @@ BOARD=""
PUBLIC_HOST="" PUBLIC_HOST=""
AUTH_HEADER="" AUTH_HEADER=""
ALLOW_USER="" ALLOW_USER=""
SEND=0
SERVE_SET=0 SERVE_SET=0
parse_flags() { parse_flags() {
@@ -58,6 +64,7 @@ parse_flags() {
--public-host) [ $# -ge 2 ] || usage; PUBLIC_HOST="$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 ;; --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 ;; --allow-user) [ $# -ge 2 ] || usage; ALLOW_USER="$2"; SERVE_SET=1; shift 2 ;;
--send) SEND=1; SERVE_SET=1; shift ;;
*) usage ;; *) usage ;;
esac esac
done done
@@ -86,6 +93,9 @@ serve_flags() {
[ -z "$PUBLIC_HOST" ] || preview=$((preview + 1)) [ -z "$PUBLIC_HOST" ] || preview=$((preview + 1))
[ -z "$AUTH_HEADER" ] || preview=$((preview + 1)) [ -z "$AUTH_HEADER" ] || preview=$((preview + 1))
[ -z "$ALLOW_USER" ] || preview=$((preview + 1)) [ -z "$ALLOW_USER" ] || preview=$((preview + 1))
# serve refuses --send without the preview set; refuse it here first,
# so no unit is written that would only exit 2 at start.
[ "$SEND" = 0 ] || [ "$preview" -eq 3 ] || die "--send needs the preview mode (--public-host, --auth-header and --allow-user)"
if [ "$preview" -ne 0 ]; then if [ "$preview" -ne 0 ]; then
[ "$preview" -eq 3 ] || die "--public-host, --auth-header and --allow-user go together: all three or none" [ "$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
@@ -95,6 +105,7 @@ serve_flags() {
case "$ALLOW_USER" in *[!A-Za-z0-9._@+-]*) die "allowed user contains a character the unit cannot carry: $ALLOW_USER" ;; 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" flags="$flags --public-host $PUBLIC_HOST --auth-header $AUTH_HEADER --allow-user $ALLOW_USER"
fi fi
[ "$SEND" = 0 ] || flags="$flags --send"
echo "$flags" echo "$flags"
} }
@@ -138,8 +149,9 @@ install_unit() {
echo "daemon-reload done" echo "daemon-reload done"
fi fi
cat <<MSG cat <<MSG
this script starts nothing: the Console holds the human's capability, so the this script starts nothing: the Console holds the human's capability. Jason
human starts it from their own shell: starts and restarts it, or Mos under lead decision 102 (sends then enter the
trail as Jason); no other agent does:
systemctl --user enable --now mosaic-webui start now and at login systemctl --user enable --now mosaic-webui start now and at login
systemctl --user status mosaic-webui systemctl --user status mosaic-webui
journalctl --user -u mosaic-webui -f the Console's log journalctl --user -u mosaic-webui -f the Console's log
@@ -182,6 +194,13 @@ status_unit() {
echo "healthz: not checked; no unit at $target" echo "healthz: not checked; no unit at $target"
return 0 return 0
fi fi
# render puts --send right after the allowed user, which may itself be
# the value --send; a unit that sends ends with both.
if grep -q '^ExecStart=.* --allow-user [^ ][^ ]* --send$' "$target"; then
echo "send: on (--send; human messages to roles)"
else
echo "send: off (read-only)"
fi
local port local port
port="$(sed -n 's/^ExecStart=.* --port \([0-9][0-9]*\).*$/\1/p' "$target")" 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; } [ -n "$port" ] || { echo "healthz: not checked; no --port in $target"; return 0; }