fix(wake): #913 installer adoption gaps — _lib dep-check fail-loud + mosaic-wake.service systemd-search-path link+validate (#930)
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful

Co-authored-by: jason.woltje <jason@diversecanvas.com>
Co-committed-by: jason.woltje <jason@diversecanvas.com>
This commit was merged in pull request #930.
This commit is contained in:
2026-07-26 12:37:29 +00:00
committed by Mos
parent 347c1d57c1
commit 0ea41e848b
3 changed files with 232 additions and 6 deletions

View File

@@ -49,9 +49,14 @@ WAKE_SYSTEMD_USER_DIR="${WAKE_SYSTEMD_USER_DIR:-$HOME/.config/systemd/user}"
# Retained snapshot store for the snapshot-guard (iv) — outside any repo.
WAKE_SNAPSHOT_DIR="${WAKE_SNAPSHOT_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/mosaic/wake/unit-snapshots}"
# shellcheck source=../_lib/manifest.sh disable=SC1091
. "$SCRIPT_DIR/../_lib/manifest.sh"
# systemd user unit NAME this installer deploys + links (overridable for the harness).
WAKE_UNIT_NAME="${WAKE_UNIT_NAME:-mosaic-wake.service}"
# The shared framework-manifest reader this installer needs for Gate A ownership
# validation. Overridable so the red-first harness can point it at a missing path
# to prove the fail-loud without disturbing the real tree (#913a).
WAKE_MANIFEST_LIB="${WAKE_MANIFEST_LIB:-$SCRIPT_DIR/../_lib/manifest.sh}"
# ─── output helpers (defined BEFORE the dependency check so it can fail loud) ──
if [[ -t 2 ]]; then
_C_G='\033[0;32m' _C_Y='\033[0;33m' _C_R='\033[0;31m' _C_0='\033[0m'
else
@@ -61,6 +66,28 @@ wi_ok() { printf " ${_C_G}OK${_C_0} %s\n" "$1" >&2; }
wi_warn() { printf " ${_C_Y}WARN${_C_0} %s\n" "$1" >&2; }
wi_fail() { printf " ${_C_R}FAIL${_C_0} %s\n" "$1" >&2; }
# ─── required framework library — fail LOUD if a stale host seed lacks it (#913a) ──
# The wake component installer sources the shared framework-manifest reader
# (_lib/manifest.sh) for Gate A ownership validation. Host seeds that predate that
# helper would otherwise abort here with a bare, obscure
# `source: No such file or directory` and no guidance. This dependency is GENUINELY
# REQUIRED (Gate A cannot be skipped on an enforcement path), so fail LOUD — naming
# the missing file AND the remedy — instead of degrading: a not-yet-updated host
# must be told exactly how to become installable.
if [[ ! -r "$WAKE_MANIFEST_LIB" ]]; then
wi_fail "wake-install: required framework library missing — $WAKE_MANIFEST_LIB"
{
printf ' %s\n' "This host's framework seed predates the shared manifest reader (_lib/manifest.sh)"
printf ' %s\n' "that the wake component installer needs for Gate A ownership validation (#913)."
printf ' %s\n' "REMEDY: re-seed the framework first, then retry the component install:"
printf ' %s\n' " mosaic update # or: bash <framework>/install.sh"
printf ' %s\n' " install.sh --component wake"
} >&2
exit 1
fi
# shellcheck source=../_lib/manifest.sh disable=SC1091
. "$WAKE_MANIFEST_LIB"
# ═══════════════════════════════════════════════════════════════════════════════
# (i) Idempotent component-manifest install + Gate A
# ═══════════════════════════════════════════════════════════════════════════════
@@ -128,10 +155,106 @@ wi_install() {
done < <(_wi_component_candidates)
wi_ok "wake component install: $written written, $skipped unchanged, $missing not-shipped (Gate A: all writes framework-owned)."
# (b #913) A10 canon step: place the deployed unit into the USER SYSTEMD SEARCH
# PATH and validate it resolves. wi_install copies the unit under mosaic home
# (systemd/user/, framework-owned) — but `systemctl --user` searches
# ~/.config/systemd/user/, so without this link the service can never be
# enabled/started. Both steps are idempotent; either failing fails the install.
wi_link_systemd_unit || return 1
wi_validate_systemd_path || return 1
# Machine-readable summary for the harness (idempotency assertion keys off it).
printf 'wake-install: written=%s skipped=%s missing=%s\n' "$written" "$skipped" "$missing"
}
# ═══════════════════════════════════════════════════════════════════════════════
# (b #913) systemd user-search-path link + post-install validate
# ═══════════════════════════════════════════════════════════════════════════════
# ADDITIVE / #869: the link target (~/.config/systemd/user/<unit>) lives OUTSIDE
# mosaic home, so it is not a framework-manifest path (the manifest is
# mosaic-home-relative). Ownership of the SSOT unit stays `systemd/**` in the single
# framework-manifest.txt authority — this step adds NO new owned path and creates NO
# second ownership authority. The link points BACK to the mosaic-home SSOT copy, so
# a later framework upgrade of the unit propagates with no re-link.
# wi_link_systemd_unit — idempotently place the deployed wake unit into the user
# systemd search path so `systemctl --user` can resolve it. Symlinks to the
# mosaic-home SSOT copy installed by wi_install. Idempotent: an already-correct link
# (or a byte-identical regular file) is left untouched; a stale entry is replaced.
wi_link_systemd_unit() {
local unit="$WAKE_UNIT_NAME"
local ssot="$WAKE_INSTALL_TARGET/systemd/user/$unit"
local link="$WAKE_SYSTEMD_USER_DIR/$unit"
if [[ ! -f "$ssot" ]]; then
wi_fail "systemd-link — the deployed unit is missing at $ssot; run 'wake-install.sh install' first (fail-closed)."
return 1
fi
mkdir -p "$WAKE_SYSTEMD_USER_DIR"
# Idempotent no-op if the search-path entry already resolves to the SSOT copy.
if [[ -L "$link" ]]; then
if [[ "$(readlink "$link")" == "$ssot" ]]; then
wi_ok "systemd-link — '$unit' already linked into the search path ($link -> $ssot)."
return 0
fi
elif [[ -f "$link" ]] && cmp -s "$ssot" "$link"; then
wi_ok "systemd-link — '$unit' already present in the search path ($link, byte-identical)."
return 0
fi
# Replace whatever is there (stale link / old copy) with a fresh symlink to SSOT.
if ! ln -sfn "$ssot" "$link"; then
wi_fail "systemd-link — could not link '$unit' into the user systemd search path ($link). FAIL LOUD (fail-closed)."
return 1
fi
wi_ok "systemd-link — '$unit' linked into the user systemd search path ($link -> $ssot)."
return 0
}
# wi_validate_systemd_path — post-install validate that the unit RESOLVES in the
# user systemd search path. The installer may run where the user systemd manager is
# NOT live (containers, no login session), so the AUTHORITATIVE check is
# path + well-formedness: the search-path entry exists, dereferences to a readable
# file, and parses as a unit ([Unit]/[Service]/[Install] + an ExecStart). A live
# `systemctl --user cat` probe runs ONLY opportunistically behind a guard.
wi_validate_systemd_path() {
local unit="$WAKE_UNIT_NAME"
local link="$WAKE_SYSTEMD_USER_DIR/$unit" resolved
if [[ ! -e "$link" ]]; then
wi_fail "systemd-validate — '$unit' is NOT in the user systemd search path ($link). systemctl --user cannot resolve it, so the wake service cannot be enabled/started. Run 'wake-install.sh link-systemd-unit' (part of install) (fail-closed)."
return 1
fi
if [[ -L "$link" ]]; then
resolved="$(readlink -f "$link" 2>/dev/null || true)"
else
resolved="$link"
fi
if [[ -z "$resolved" || ! -r "$resolved" ]]; then
wi_fail "systemd-validate — '$unit' search-path entry ($link) does not dereference to a readable unit file. FAIL LOUD (fail-closed)."
return 1
fi
# Well-formed check: a wake detector unit must carry the core sections + ExecStart.
local -a miss=()
grep -q '^\[Unit\]' "$resolved" || miss+=("[Unit]")
grep -q '^\[Service\]' "$resolved" || miss+=("[Service]")
grep -q '^\[Install\]' "$resolved" || miss+=("[Install]")
grep -q '^ExecStart=' "$resolved" || miss+=("ExecStart=")
if [[ ${#miss[@]} -ne 0 ]]; then
wi_fail "systemd-validate — '$unit' ($resolved) is malformed: missing ${miss[*]}. Refusing to certify an ill-formed unit (fail-closed)."
return 1
fi
# Opportunistic live probe ONLY when explicitly requested AND a user manager is
# reachable — never a hard requirement (the installer often runs without one).
if [[ "${WAKE_VERIFY_USE_SYSTEMCTL:-0}" == "1" ]] && command -v systemctl >/dev/null 2>&1; then
if systemctl --user cat "$unit" >/dev/null 2>&1; then
wi_ok "systemd-validate — 'systemctl --user cat $unit' resolves (live user manager confirmed)."
else
wi_warn "systemd-validate — file is in the search path + well-formed, but 'systemctl --user cat $unit' did not resolve (no live user manager / not daemon-reloaded). Non-fatal: run 'systemctl --user daemon-reload' in a live session."
fi
fi
wi_ok "systemd-validate — '$unit' resolves in the user systemd search path ($link -> $resolved, well-formed)."
return 0
}
# ═══════════════════════════════════════════════════════════════════════════════
# (v) Fail-closed alarm-target + HMAC-key install-validation (G1/G2a)
# ═══════════════════════════════════════════════════════════════════════════════
@@ -409,6 +532,8 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
cmd="${1:-}"; shift || true
case "$cmd" in
install) wi_install "$@" ;;
link-systemd-unit) wi_link_systemd_unit "$@" ;;
validate-systemd-path) wi_validate_systemd_path "$@" ;;
validate-targets) wi_validate_targets "$@" ;;
validate-hmac-key) wi_validate_hmac_key "$@" ;;
validate-alarm-target) wi_validate_alarm_target "$@" ;;
@@ -422,7 +547,11 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
Usage: wake-install.sh <command> [args]
Commands:
install Idempotent component-manifest install + Gate A.
install Idempotent component-manifest install + Gate A,
then link the unit into the user systemd search
path and validate it resolves (b, #913).
link-systemd-unit Link mosaic-wake.service into ~/.config/systemd/user/.
validate-systemd-path Validate the unit resolves in the user systemd search path.
validate-targets Fail-closed HMAC-key + alarm-target validate (v).
validate-hmac-key HMAC key resolves by-name, else fail loud.
validate-alarm-target Alarm sink configured + reachable, else fail loud.