Files
stack/scripts/webui-service.sh
T
rocko b2b33aee55 row 178 (#1676, P19 385): the Console checks a proxy proof header on public Hosts
webui serve takes --proof-header NAME --proof-file PATH, only with the --public-host preview flags.
Before listen it reads the file once through O_NOFOLLOW and fstat: a regular file owned by the
Console uid, mode 0400 or 0600, at least 32 base64url bytes with one optional final LF. Any file
problem gives one fixed refusal that names neither the path nor the contents. The header name can't
collide with Host, Origin, auth or tester names.

On a public Host the guard runs before any auth header is read. It needs exactly one proof header
and compares equal-length buffers with timingSafeEqual. Missing, wrong and repeated proofs get the
same fixed 401. A proof header on another Host or on the tester listener gets 403. The existing
auth, Origin, Fetch Metadata and human write checks are unchanged.

webui-service.sh renders both flags and checks the file's metadata and canonical location without
opening it, and the send-status regex accepts the proof pair. stack-release.sh only gains a comment:
it already carries Console flags through re-pin and rollback, and new tests pin that.

This is a partial fix. Same-uid seats can still read the proof file. Activation (middleware, unit
flags, restart, smoke) is a separate Mos-checked window and isn't part of this commit.

Dewey approved round 2 in #1676 comment 29386, candidate 9300a670. Round 1 changes were in 29374.
2026-10-20 00:37:08 -05:00

436 lines
23 KiB
Bash
Executable File

#!/usr/bin/env bash
# `scripts/webui-service.sh copy|render|install|uninstall|status`: the
# systemd user unit that runs the Console, `node packages/webui/src/cli.mjs
# serve` (lead decision 94), from a read-only copy of one landed commit
# (row 77, #1571, lead decision 112). The template is
# packages/webui/systemd/mosaic-webui.service.in; the rendered unit is
# mosaic-webui.service in the systemd user directory, one instance.
#
# copy COMMIT [--copies DIR] [--allow-worktree]
# write `git archive` of the paths the Console
# loads at COMMIT into DIR/<full id>/, read-only;
# DIR defaults to
# ${XDG_DATA_HOME:-~/.local/share}/mosaic-stack/copies
# render [--copy ID [--copies DIR]] [FLAGS]
# print the rendered unit on stdout; without
# --copy it runs from this checkout
# install --copy ID [--copies DIR] [--dir DIR] [--no-reload] [--allow-worktree] [FLAGS]
# write the unit (write to a temp file, then
# rename) and run `systemctl --user daemon-reload`;
# refuses while the unit is running
# uninstall [--dir DIR] [--no-reload]
# remove the unit; refuses while it is running
# status [--dir DIR] unit state, the copy it runs and whether
# refactor has moved past it, whether it sends,
# the tester listener, then GET /healthz on its port
#
# With --copy the unit's WorkingDirectory and ExecStart point at the copy,
# and ExecStart passes this checkout as serve's --checkout: the queue read
# runs the checkout's queue-read.mjs, since the queue store refuses code
# outside the canonical checkout; everything else comes from the copy.
#
# copy and install run only from the canonical checkout, whose root holds a
# .git directory (a linked worktree has a .git file). --allow-worktree lifts
# that refusal and exists for the tests only, which run in worktrees.
#
# The update step: `copy COMMIT`; Jason or Mos stops the unit; `install
# --copy ID` with the same flags; Jason or Mos starts it. Rollback is the
# same with the earlier id. Copies stay until someone removes them. install
# refuses unless the bus unit runs from the same copy (bus_on_copy below),
# row 85 supplies the shared copy and bus unit flags; Mos owns switching.
#
# FLAGS are serve's: --port N (default 7332; 7330 is the operator's own
# Console), --tester-port N (1024..65535, neither --port nor the tunnel's
# 7332; binds the tester on loopback), --business ID, --board URL, and the preview set --public-host
# 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
# character set and refused otherwise. --proof-header NAME --proof-file PATH
# go together under preview; only the path is carried here, never the secret.
# render checks ownership, mode and location without opening the proof file.
#
# There is no start, stop, enable or restart verb. The Console reads the
# main listener's bus through the human transport; the optional loopback
# listener binds the tester. Whoever starts it holds the human's
# capability. Jason starts and restarts it
# (`systemctl --user enable --now mosaic-webui`), and so may Mos under lead
# 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).
# install and uninstall refuse while the unit is running, with or without
# --no-reload: a unit rewritten under a running Console would take the new
# ExecStart at its next Restart=on-failure, with nobody starting it.
#
# 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 copy COMMIT [--copies DIR] [--allow-worktree] | render [--copy ID [--copies DIR]] [FLAGS] | install --copy ID [--copies DIR] [--dir DIR] [--no-reload] [--allow-worktree] [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 [--send]] [--tester-port N] [--proof-header NAME --proof-file PATH]
--tester-port: 1024..65535, different from --port and never 7332; loopback tester listener.
--allow-worktree is for the tests only: copy and install otherwise refuse outside the canonical checkout."
die() { echo "webui-service: $*" >&2; exit 1; }
usage() { echo "$USAGE" >&2; exit 4; }
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
COPIES="${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-stack/copies"
COPY_ID=""
COPY_FLAGS=0
ALLOW_WORKTREE=0
RELOAD=1
DIR_FLAGS=0
PORT=7332
TESTER_PORT=""
TESTER_SET=0
BUSINESS=""
BOARD=""
PUBLIC_HOST=""
AUTH_HEADER=""
ALLOW_USER=""
SEND=0
PROOF_HEADER=""
PROOF_FILE=""
PROOF_SET=0
SERVE_SET=0
parse_flags() {
while [ $# -gt 0 ]; do
case "$1" in
--dir) [ $# -ge 2 ] || usage; UNIT_DIR="$2"; DIR_FLAGS=1; shift 2 ;;
--copy) [ $# -ge 2 ] || usage; COPY_ID="$2"; COPY_FLAGS=1; shift 2 ;;
--copies) [ $# -ge 2 ] || usage; COPIES="$2"; COPY_FLAGS=1; shift 2 ;;
--allow-worktree) ALLOW_WORKTREE=1; shift ;;
--no-reload) RELOAD=0; DIR_FLAGS=1; shift ;;
--port) [ $# -ge 2 ] || usage; PORT="$2"; SERVE_SET=1; shift 2 ;;
--tester-port) [ $# -ge 2 ] || usage; TESTER_PORT="$2"; TESTER_SET=1; 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 ;;
--proof-header) [ $# -ge 2 ] || usage; PROOF_HEADER="$2"; PROOF_SET=1; SERVE_SET=1; shift 2 ;;
--proof-file) [ $# -ge 2 ] || usage; PROOF_FILE="$2"; PROOF_SET=1; SERVE_SET=1; shift 2 ;;
--send) SEND=1; SERVE_SET=1; shift ;;
*) usage ;;
esac
done
# Validate here so a bad tester value exits 4 before render's existing
# operation-failure wrappers, and before install reads or changes units.
[ "$TESTER_SET" = 0 ] || tester_port_ok
}
tester_port_usage() {
echo "webui-service: --tester-port must be 1024..65535, different from --port and never 7332: $TESTER_PORT" >&2
exit 4
}
tester_port_ok() {
case "$TESTER_PORT" in ''|*[!0-9]*) tester_port_usage ;; esac
# At most five digits as written, before the zeros go, as serve and stack-release.sh count them.
[ "${#TESTER_PORT}" -le 5 ] || tester_port_usage
local tester_port="$TESTER_PORT"
# Compare decimal values, including values written with leading zeros.
while [ "${tester_port#0}" != "$tester_port" ]; do tester_port="${tester_port#0}"; done
[ -n "$tester_port" ] && [ "${#tester_port}" -le 5 ] || tester_port_usage
[ "$tester_port" -ge 1024 ] && [ "$tester_port" -le 65535 ] || tester_port_usage
[ "$tester_port" -ne 7332 ] || tester_port_usage
# Invalid main ports retain their existing operation-failure behavior.
if [[ "$PORT" =~ ^[0-9]+$ ]] && [ "${#PORT}" -le 5 ]; then
[ "$tester_port" -ne "$PORT" ] || tester_port_usage
fi
}
# 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))
# 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
[ "$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 must start with a letter or digit: $ALLOW_USER" ;; esac
case "$ALLOW_USER" in *[!A-Za-z0-9._@+-]*) die "allowed user contains a character the unit cannot carry: $ALLOW_USER" ;; esac
# Lead decision 101: the uid header carries the Authentik uid, 64
# lower-case hex characters, and nothing else is let through on it.
if [ "${AUTH_HEADER,,}" = x-authentik-uid ] && ! [[ "$ALLOW_USER" =~ ^[0-9a-f]{64}$ ]]; then
die "with --auth-header $AUTH_HEADER the allowed user must be an Authentik uid, 64 lower-case hex characters (^[0-9a-f]{64}\$, lead decision 101): $ALLOW_USER"
fi
flags="$flags --public-host $PUBLIC_HOST --auth-header $AUTH_HEADER --allow-user $ALLOW_USER"
fi
if [ "$PROOF_SET" = 1 ]; then
[ -n "$PROOF_HEADER" ] && [ -n "$PROOF_FILE" ] && [ "$preview" -eq 3 ] || die "proxy proof needs both flags and preview mode"
[[ "$PROOF_HEADER" =~ ^[A-Za-z0-9][A-Za-z0-9-]{0,63}$ ]] || die "invalid proxy proof header"
case "${PROOF_HEADER,,}" in host|origin|x-mosaic-tester|"${AUTH_HEADER,,}") die "proxy proof header must differ from Host, Origin, tester and auth headers" ;; esac
case "$PROOF_FILE" in /*) ;; *) die "proxy proof file must be an absolute path" ;; esac
case "$PROOF_FILE" in *[!A-Za-z0-9/._+-]*) die "proxy proof path cannot be carried by the unit" ;; esac
# Metadata only: render never opens the proof, even to validate its contents.
[ ! -L "$PROOF_FILE" ] && [ -f "$PROOF_FILE" ] || die "invalid proxy proof file metadata"
local proof_path copies_path proof_mode proof_uid
proof_path="$(realpath -e -- "$PROOF_FILE")" || die "cannot stat proxy proof file"
copies_path="$(realpath -m -- "$COPIES")" || die "cannot resolve copies directory"
case "$proof_path" in "$REPO"|"$REPO"/*|"$copies_path"|"$copies_path"/*) die "proxy proof file must be outside the repository and copies" ;; esac
proof_mode="$(stat -c '%a' -- "$PROOF_FILE")" || die "cannot stat proxy proof file"
proof_uid="$(stat -c '%u' -- "$PROOF_FILE")" || die "cannot stat proxy proof file"
{ [ "$proof_mode" = 600 ] || [ "$proof_mode" = 400 ]; } && [ "$proof_uid" = "$(id -u)" ] || die "invalid proxy proof file metadata"
flags="$flags --proof-header $PROOF_HEADER --proof-file $PROOF_FILE"
fi
[ "$SEND" = 0 ] || flags="$flags --send"
[ "$TESTER_SET" = 0 ] || flags="$flags --tester-port $TESTER_PORT"
echo "$flags"
}
# Row 85: stack-copy.sh owns the pushed runtime closure and npm install.
# This script delegates copy and shares its copies directory.
# N2 (row 75 review): copy and install read and point at this checkout, so
# they run from the canonical one, never a worktree someone may remove.
canonical() {
[ -d "$REPO/.git" ] || [ "$ALLOW_WORKTREE" = 1 ] || die "refusing: $REPO is not the canonical checkout (its .git is not a directory); run copy and install from the canonical checkout"
}
# The copies directory goes into the unit as part of the copy's path.
copies_ok() {
case "$COPIES" in /*) ;; *) die "copies directory must be an absolute path: $COPIES" ;; esac
case "$COPIES" in *[!A-Za-z0-9/._+-]*) die "copies directory contains a character the unit cannot carry: $COPIES" ;; esac
}
# The root the unit runs from: the copy named by --copy, else this checkout.
unit_root() {
if [ -z "$COPY_ID" ]; then echo "$REPO"; return 0; fi
case "$COPY_ID" in *[!0-9a-f]*) die "--copy must be a full 40-character commit id: $COPY_ID" ;; esac
[ "${#COPY_ID}" -eq 40 ] || die "--copy must be a full 40-character commit id: $COPY_ID"
copies_ok
[ -f "$COPIES/$COPY_ID/packages/webui/src/cli.mjs" ] || die "no copy $COPY_ID in $COPIES; make it with: scripts/webui-service.sh copy $COPY_ID"
echo "$COPIES/$COPY_ID"
}
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 root
root="$(unit_root)" || exit 1
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
[ -z "$COPY_ID" ] || flags="--checkout $REPO $flags"
sed -e "s|@REPO@|$root|g" -e "s|@PATH@|$path|g" -e "s|@NODE@|$node|g" -e "s|@FLAGS@|$flags|g" "$TEMPLATE"
}
# The unit's active state from list-units, printed when it is anything but
# inactive or failed (active, activating, deactivating, reloading...). A
# unit systemd has not loaded is not listed and prints nothing. N4: a
# failed call refuses with one line naming it; the state is unknown, so
# nothing is written or removed.
running() {
local out
out="$(systemctl --user list-units --no-legend --plain "$UNIT_NAME" 2>/dev/null)" || die "refusing: systemctl --user list-units --no-legend --plain $UNIT_NAME failed, so the unit's state is unknown; nothing was changed"
printf '%s\n' "$out" | awk -v unit="$UNIT_NAME" '$1 == unit && $3 != "inactive" && $3 != "failed" {print $3}'
}
# The bus host proves a human by checking that the calling process runs its
# own packages/bus/src/human-cli.mjs (packages/bus/src/human.mjs), and the
# Console spawns the one beside its own files. So a Console on a copy works
# only with a bus on that same copy: install refuses unless every bus unit
# it would call has an ExecStart path inside the copy. With --business the
# unit is [email protected]; without it, every mosaic-bus@ unit systemd
# has loaded, and none loaded refuses. Like the state read, a failed call
# refuses with one line.
bus_on_copy() {
local copy="$1" units unit out load paths p working
if [ -n "$BUSINESS" ]; then
units="mosaic-bus@$BUSINESS.service"
else
out="$(systemctl --user list-units --all --no-legend --plain 'mosaic-bus@*.service' 2>/dev/null)" || die "refusing: systemctl --user list-units --all --no-legend --plain mosaic-bus@*.service failed, so the bus unit is unknown; nothing was changed"
units="$(printf '%s\n' "$out" | awk '$1 ~ /^mosaic-bus@.+\.service$/ {print $1}')"
[ -n "$units" ] || die "refusing: no mosaic-bus@ unit is loaded, and the Console on the copy $copy needs its bus on that copy; name the business with --business ID"
fi
for unit in $units; do
out="$(systemctl --user show "$unit" -p LoadState -p WorkingDirectory -p ExecStart 2>/dev/null)" || die "refusing: systemctl --user show $unit failed, so the bus unit is unknown; nothing was changed"
load="$(printf '%s\n' "$out" | sed -n 's/^LoadState=//p')"
[ "$load" = loaded ] || die "refusing: no bus unit $unit is installed (LoadState ${load:-unknown}); the Console on the copy $copy needs its bus on that copy"
paths="$(printf '%s\n' "$out" | sed -n 's/^ExecStart=//p' | grep -o 'path=[^ ;]*' | sed 's/^path=//' || true)"
[ -n "$paths" ] || die "refusing: $unit has no ExecStart path, so where its bus runs from is unknown; nothing was changed"
for p in $paths; do
case "$p" in "$copy"/*) ;; *) die "refusing: $unit runs from $p, not the copy $copy; the bus accepts only its own human-cli.mjs, so every Console bus call would fail human-required until the bus runs from the same copy" ;; esac
done
working="$(printf '%s\n' "$out" | sed -n 's/^WorkingDirectory=//p')"
[ "$working" = "$copy" ] || die "refusing: $unit WorkingDirectory is ${working:-unknown}, not the copy $copy; nothing was changed"
done
}
install_unit() {
parse_flags "$@"
canonical
[ -n "$COPY_ID" ] || die "install needs --copy ID, a copy made with: scripts/webui-service.sh copy COMMIT"
local unit
unit="$(render)" || exit 1
bus_on_copy "$COPIES/$COPY_ID"
# N1: a running unit would take the new ExecStart at its next restart.
local state
state="$(running)" || exit 1
[ -z "$state" ] || die "refusing: $UNIT_NAME is running ($state); Jason or Mos stops it first (systemctl --user stop mosaic-webui), then install, then start it"
mkdir -p "$UNIT_DIR"
local target="$UNIT_DIR/$UNIT_NAME" tmp
tmp="$(mktemp "$UNIT_DIR/.$UNIT_NAME.XXXXXX")"
printf '%s\n' "$unit" > "$tmp"
chmod 0644 "$tmp"
if [ -f "$target" ] && cmp -s "$tmp" "$target"; then
rm -f "$tmp"
echo "unchanged: $target"
else
mv -f "$tmp" "$target"
echo "written: $target"
fi
echo "serve: $(sed -n 's/^ExecStart=//p' "$target")"
if [ "$RELOAD" = 1 ]; then
systemctl --user daemon-reload || die "daemon-reload failed"
echo "daemon-reload done"
fi
cat <<MSG
this script starts nothing: the Console holds the human's capability. Jason
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 status mosaic-webui
journalctl --user -u mosaic-webui -f the Console's log
systemctl --user stop mosaic-webui SIGTERM; restartable
scripts/webui-service.sh status unit state and /healthz
survive logout and reboot only with lingering on: loginctl enable-linger ${USER:-$(id -un)}
MSG
}
uninstall_unit() {
parse_flags "$@"
[ "$SERVE_SET" = 0 ] && [ "$COPY_FLAGS" = 0 ] && [ "$ALLOW_WORKTREE" = 0 ] || usage
local target="$UNIT_DIR/$UNIT_NAME"
# N3: checked with --no-reload too, which skips only daemon-reload.
local state
state="$(running)" || exit 1
[ -z "$state" ] || die "refusing: $UNIT_NAME is still running ($state); Jason or Mos stops it first (systemctl --user stop mosaic-webui)"
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 ] && [ "$COPY_FLAGS" = 0 ] && [ "$ALLOW_WORKTREE" = 0 ] || 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
# The copy is the last part of WorkingDirectory; status only reads.
local wd copy head
wd="$(sed -n 's/^WorkingDirectory=//p' "$target")"
copy="${wd##*/}"
if [[ "$copy" =~ ^[0-9a-f]{40}$ ]]; then
echo "copy: $copy"
if head="$(git -C "$REPO" rev-parse --verify --quiet refs/heads/refactor 2>/dev/null)"; then
if [ "$head" = "$copy" ]; then
echo "refactor: $head, the copy's commit"
else
echo "refactor: $head, another commit; the copy is $copy"
fi
else
echo "refactor: unknown; git rev-parse refs/heads/refactor failed in $REPO"
fi
else
echo "copy: none; the unit runs from $wd"
fi
# render puts --send after the allowed user and optional proof pair,
# before the tester port. The allowed user
# starts with a letter or digit (N5), so it is never the value --send.
if grep -Eq '^ExecStart=.* --allow-user [^ ][^ ]*( --proof-header [^ ]+ --proof-file /[^ ]+)? --send( --tester-port [0-9]+)?$' "$target"; then
echo "send: on (--send; human messages to roles)"
else
echo "send: off (read-only)"
fi
local tester_port
tester_port="$(sed -n 's/^ExecStart=.* --tester-port \([0-9][0-9]*\)$/\1/p' "$target")"
if [ -n "$tester_port" ]; then
echo "tester: http://127.0.0.1:$tester_port (binds the tester)"
else
echo "tester: off"
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")"
}
# Writes the copy into a temp directory beside the target, takes write
# permission off every file, then renames it into place, so a copy at its
# final path is always whole and read-only. An existing copy is left alone.
copy_commit() {
exec "$REPO/scripts/stack-copy.sh" copy "$@"
}
[ $# -ge 1 ] || usage
cmd="$1"; shift
case "$cmd" in
copy) copy_commit "$@" ;;
render)
parse_flags "$@"
[ "$DIR_FLAGS" = 0 ] && [ "$ALLOW_WORKTREE" = 0 ] || usage
{ [ -n "$COPY_ID" ] || [ "$COPY_FLAGS" = 0 ]; } || usage
render ;;
install) install_unit "$@" ;;
uninstall) uninstall_unit "$@" ;;
status) status_unit "$@" ;;
-h|--help) echo "$USAGE" ;;
*) usage ;;
esac