Row 85 round 2. Built by Rocko, packet agents/rocko/work/queue-85/r2/,
candidate manifest
bd7ea899c7cacef43b415b8e9ecb164e1f36718ffbe64347881a2c5653170c98.
Filbert approved on #1579, comment 28256; Darkwing approved, comment
28258.
stack-copy.sh builds shared read-only copies under
XDG_DATA_HOME/mosaic-stack/copies; the bus and Console services render
and install against a copy with --copy and --pm. No live step is run
by this commit; those need Mos's go.
Sage's gate rerun on 3a1e11b83 with rows 85, 102 and 103 applied,
Docker up: node suites bus 128, business 67, cli 165, control-board
124, conversation 182, discord 178, harness 73, ledger 78, mosaic 69,
queue 155, runs 43, seat 113, tasks 92, webui 108; scripts auth 15,
conductor 17, config 24, discord 66, extension-package 18,
foundation 44, queue 28, release 14, task 98. All pass, 0 fail.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
237 lines
11 KiB
Bash
Executable File
237 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# `scripts/bus-service.sh render|install|uninstall|status [<business>]`:
|
|
# the systemd user unit that supervises `scripts/mosaic bus start <business>`
|
|
# (lead decision 70). The template is
|
|
# packages/cli/systemd/mosaic-bus.service.in; the rendered unit is
|
|
# [email protected] in the systemd user directory, one instance per
|
|
# business (mosaic-bus@<business>). Nothing here reads a capability, the
|
|
# binding or the token. See packages/cli/README.md, "Bus host".
|
|
#
|
|
# render [--claude PATH] print the rendered unit on stdout
|
|
# install [--dir DIR] [--no-reload] [--claude PATH]
|
|
# 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 an instance is active
|
|
# status <business> unit state, then `mosaic bus status`
|
|
#
|
|
# --claude PATH pins the Claude Code binary the unit's sessions run
|
|
# (MOSAIC_CLAUDE_BIN); without it, `command -v claude` here. The path is
|
|
# kept as given, so a symlink that tracks updates keeps tracking them.
|
|
#
|
|
# Exit codes: 0 ok, 1 operation failed, 4 usage.
|
|
set -euo pipefail
|
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
TEMPLATE="$REPO/packages/cli/systemd/mosaic-bus.service.in"
|
|
UNIT_NAME="[email protected]"
|
|
USAGE="usage: scripts/bus-service.sh render [--copy ID [--copies DIR]] [--pm] [--claude PATH] | install [--copy ID [--copies DIR]] [--pm] [--dir DIR] [--no-reload] [--claude PATH] | uninstall [--dir DIR] [--no-reload] | status <business>"
|
|
|
|
die() { echo "bus-service: $*" >&2; exit 1; }
|
|
usage() { echo "$USAGE" >&2; exit 4; }
|
|
|
|
CLAUDE_BIN=""
|
|
CLAUDE_SET=0
|
|
COPY_ID=""
|
|
COPIES="${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-stack/copies"
|
|
PM=0
|
|
|
|
# The unit's MOSAIC_CLAUDE_BIN line, or a comment when there is no claude to
|
|
# pin. systemd expands % and splits Environment= on spaces, so the path is
|
|
# held to a plain character set.
|
|
claude_line() {
|
|
local bin
|
|
if [ "$CLAUDE_SET" = 1 ]; then
|
|
bin="$CLAUDE_BIN"
|
|
else
|
|
bin="$(command -v claude || true)"
|
|
if [ -z "$bin" ]; then
|
|
echo "bus-service: no claude on PATH; MOSAIC_CLAUDE_BIN left unset (--claude PATH pins one)" >&2
|
|
echo "# MOSAIC_CLAUDE_BIN unset: no claude on PATH at install time"
|
|
return 0
|
|
fi
|
|
fi
|
|
case "$bin" in /*) ;; *) die "claude path must be absolute: $bin" ;; esac
|
|
case "$bin" in *[!A-Za-z0-9/._+-]*) die "claude path contains a character the unit cannot carry: $bin" ;; esac
|
|
[ -f "$bin" ] && [ -x "$bin" ] || die "claude path is not an executable file: $bin"
|
|
echo "Environment=MOSAIC_CLAUDE_BIN=$bin"
|
|
}
|
|
|
|
render() {
|
|
[ -f "$TEMPLATE" ] || die "template missing: $TEMPLATE"
|
|
local root="$REPO"
|
|
if [ -n "$COPY_ID" ]; then
|
|
[[ "$COPY_ID" =~ ^[0-9a-f]{40}$ ]] || die "--copy must be a full 40-character commit id"
|
|
case "$COPIES" in /*) ;; *) die "copies directory must be absolute" ;; esac
|
|
case "$COPIES" in *[!A-Za-z0-9/._+-]*) die "copies directory contains a character the unit cannot carry" ;; esac
|
|
root="$COPIES/$COPY_ID"
|
|
[ -f "$root/scripts/mosaic" ] || die "no copy $COPY_ID in $COPIES"
|
|
fi
|
|
local pm_flags=""
|
|
[ "$PM" = 0 ] || pm_flags=" --pm"
|
|
local node_dir
|
|
node_dir="$(dirname "$(command -v node || true)")"
|
|
[ -n "$node_dir" ] && [ "$node_dir" != "." ] || die "node not found on PATH"
|
|
local path="/usr/local/bin:/usr/bin:/bin"
|
|
case ":$path:" in *":$node_dir:"*) ;; *) path="$node_dir:$path" ;; esac
|
|
case "$REPO" in *@*|*'|'*) die "repository path contains a character the template cannot carry: $REPO" ;; esac
|
|
local claude
|
|
claude="$(claude_line)" || exit 1
|
|
sed -e "s|@REPO@|$root|g" -e "s|@PATH@|$path|g" -e "s|@PM_FLAGS@|$pm_flags|g" -e "s|^@CLAUDE_ENV@$|$claude|" "$TEMPLATE"
|
|
}
|
|
|
|
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
|
RELOAD=1
|
|
parse_flags() {
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--dir) [ $# -ge 2 ] || usage; UNIT_DIR="$2"; shift 2 ;;
|
|
--no-reload) RELOAD=0; shift ;;
|
|
--copy) [ $# -ge 2 ] || usage; COPY_ID="$2"; shift 2 ;;
|
|
--copies) [ $# -ge 2 ] || usage; COPIES="$2"; shift 2 ;;
|
|
--pm) PM=1; shift ;;
|
|
--claude) [ $# -ge 2 ] || usage; CLAUDE_BIN="$2"; CLAUDE_SET=1; shift 2 ;;
|
|
*) usage ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# Inspect effective unit metadata: drop-ins can override the installed template.
|
|
# Any failed read refuses before a write, including --no-reload installations.
|
|
install_guard() {
|
|
local out unit file
|
|
out="$(systemctl --user list-units --all --no-legend --plain 'mosaic-bus@*.service' 2>/dev/null)" || die "refusing: systemctl --user list-units failed, so the bus state is unknown; nothing was changed"
|
|
while read -r unit load state rest; do
|
|
[[ "$unit" == mosaic-bus@*.service ]] || continue
|
|
case "$state" in inactive|failed) ;; *) die "refusing: $unit is running ($state); Jason or Mos stops it first, then install, then start it" ;; esac
|
|
done <<< "$out"
|
|
# systemctl show rejects a template name. The lookup-path scan also
|
|
# covers stopped/unloaded instances that list-units does not enumerate.
|
|
local paths directory dropdir content
|
|
paths="$(systemd-analyze --user unit-paths 2>/dev/null)" || die "refusing: systemd-analyze --user unit-paths failed; drop-in paths are unknown; nothing was changed"
|
|
[ -n "$paths" ] || die "refusing: systemd unit paths are unknown; nothing was changed"
|
|
while IFS= read -r directory; do
|
|
[ -n "$directory" ] || continue
|
|
case "$directory" in /*) ;; *) die "refusing: invalid systemd unit path $directory; nothing was changed" ;; esac
|
|
[ -e "$directory" ] || [ -L "$directory" ] || continue
|
|
[ -d "$directory" ] && [ -r "$directory" ] && [ -x "$directory" ] || die "refusing: cannot inspect unit path $directory; nothing was changed"
|
|
# * includes the empty instance, i.e. [email protected].
|
|
for dropdir in "$directory"/mosaic-bus@*.service.d; do
|
|
[ -e "$dropdir" ] || [ -L "$dropdir" ] || continue
|
|
[ -d "$dropdir" ] && [ -r "$dropdir" ] && [ -x "$dropdir" ] || die "refusing: cannot inspect drop-ins $dropdir; nothing was changed"
|
|
for file in "$dropdir"/*.conf; do
|
|
[ -e "$file" ] || [ -L "$file" ] || continue
|
|
content="$(cat "$file" 2>/dev/null)" || die "refusing: cannot read drop-in $file; nothing was changed"
|
|
if grep -Eq '^[[:space:]]*(ExecStart|WorkingDirectory)[[:space:]]*=' <<< "$content"; then
|
|
die "refusing: drop-in $file overrides ExecStart or WorkingDirectory; Mos removes it first and reloads systemd"
|
|
fi
|
|
done
|
|
done
|
|
done <<< "$paths"
|
|
}
|
|
|
|
install_unit() {
|
|
parse_flags "$@"
|
|
install_guard
|
|
mkdir -p "$UNIT_DIR"
|
|
local target="$UNIT_DIR/$UNIT_NAME" tmp unit
|
|
unit="$(render)" || exit 1
|
|
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
|
|
local bin
|
|
bin="$(sed -n 's/^Environment=MOSAIC_CLAUDE_BIN=//p' "$target")"
|
|
if [ -n "$bin" ]; then
|
|
echo "claude: $bin ($("$bin" --version 2>/dev/null | head -n 1 || true))"
|
|
else
|
|
echo "claude: MOSAIC_CLAUDE_BIN unset; sessions run claude from the unit's PATH"
|
|
fi
|
|
if [ "$RELOAD" = 1 ]; then
|
|
systemctl --user daemon-reload || die "daemon-reload failed"
|
|
echo "daemon-reload done"
|
|
fi
|
|
cat <<MSG
|
|
next, for one business (one per data root):
|
|
mkdir -m 0700 -p <dataRoot>/notify/<business> the notifier refuses a looser directory
|
|
write <dataRoot>/notify/<business>/notify.json, mode 0600:
|
|
{"notifyVersion": 1, "binding": "<discord binding>"} or "binding": null for no DMs
|
|
systemctl --user enable --now mosaic-bus@<business> start now and at login
|
|
systemctl --user status mosaic-bus@<business>
|
|
journalctl --user -u mosaic-bus@<business> -f the host's log
|
|
systemctl --user stop mosaic-bus@<business> SIGTERM; restartable
|
|
survive logout and reboot only with lingering on: loginctl enable-linger ${USER:-$(id -un)}
|
|
MSG
|
|
}
|
|
|
|
uninstall_unit() {
|
|
parse_flags "$@"
|
|
[ "$CLAUDE_SET" = 0 ] && [ -z "$COPY_ID" ] && [ "$PM" = 0 ] || usage
|
|
local target="$UNIT_DIR/$UNIT_NAME"
|
|
local out active
|
|
out="$(systemctl --user list-units --all --no-legend --plain 'mosaic-bus@*.service' 2>/dev/null)" || die "refusing: systemctl read failed; nothing was changed"
|
|
active="$(awk '$3 != "inactive" && $3 != "failed" {print $1}' <<< "$out")"
|
|
[ -z "$active" ] || die "refusing: instance(s) still running: $active; Jason or Mos stops them first"
|
|
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() {
|
|
[ $# -eq 1 ] || usage
|
|
local business="$1"
|
|
case "$business" in ''|*[!a-z0-9-]*) die "business id must be [a-z0-9-]+" ;; esac
|
|
local unit="mosaic-bus@$business.service"
|
|
echo "unit: $unit"
|
|
echo " enabled: $(systemctl --user is-enabled "$unit" 2>&1 || true)"
|
|
echo " active: $(systemctl --user is-active "$unit" 2>&1 || true)"
|
|
systemctl --user show "$unit" -p MainPID -p NRestarts -p ExecMainStartTimestamp -p Result -p Environment 2>/dev/null | sed 's/^/ /'
|
|
local effective root exec_start drops copy=checkout
|
|
effective="$(systemctl --user show "$unit" -p LoadState -p WorkingDirectory -p ExecStart -p DropInPaths 2>/dev/null)" || die "cannot read effective unit $unit"
|
|
root="$(sed -n 's/^WorkingDirectory=//p' <<< "$effective")"
|
|
exec_start="$(sed -n 's/^ExecStart=//p' <<< "$effective")"
|
|
drops="$(sed -n 's/^DropInPaths=//p' <<< "$effective")"
|
|
if [[ "${root##*/}" =~ ^[0-9a-f]{40}$ ]] && [ -f "$root/scripts/mosaic" ];then copy="${root##*/}";fi
|
|
echo "tree: ${root:-absent}"
|
|
echo "copy: $copy"
|
|
if [[ "$exec_start" =~ [[:space:]]--pm([[:space:];]|$) ]];then echo 'pm: on';else echo 'pm: off';fi
|
|
echo "drop-ins: ${drops:-none}"
|
|
local head
|
|
head="$(git -C "$REPO" rev-parse refs/heads/refactor 2>/dev/null || true)"
|
|
if [ "$copy" != checkout ] && [ "$head" != "$copy" ];then echo "refactor: ${head:-unknown} (another commit)";else echo "refactor: ${head:-unknown}";fi
|
|
"$REPO/scripts/mosaic" bus status | sed 's/^/ /' || true
|
|
}
|
|
|
|
[ $# -ge 1 ] || usage
|
|
cmd="$1"; shift
|
|
case "$cmd" in
|
|
render)
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--copy) [ $# -ge 2 ] || usage; COPY_ID="$2"; shift 2 ;;
|
|
--copies) [ $# -ge 2 ] || usage; COPIES="$2"; shift 2 ;;
|
|
--pm) PM=1; shift ;;
|
|
--claude) [ $# -ge 2 ] || usage; CLAUDE_BIN="$2"; CLAUDE_SET=1; shift 2 ;;
|
|
*) usage ;;
|
|
esac
|
|
done
|
|
render ;;
|
|
install) install_unit "$@" ;;
|
|
uninstall) uninstall_unit "$@" ;;
|
|
status) status_unit "$@" ;;
|
|
-h|--help) echo "$USAGE" ;;
|
|
*) usage ;;
|
|
esac
|