fix(wake): #913 installer adoption gaps — _lib dep-check fail-loud + mosaic-wake.service systemd-search-path link+validate
Some checks failed
ci/woodpecker/pr/ci Pipeline was canceled
Some checks failed
ci/woodpecker/pr/ci Pipeline was canceled
Two non-blocking wake-canon adoption gaps surfaced by the dragon-lin pilot in the A10 component installer (wake-install.sh). Both fixes are ADDITIVE per #869: framework-manifest.txt ownership, the install-ordering-guard, and the bash/TS manifest parity contract are all UNCHANGED. (a) DEP-CHECK — wake-install.sh sourced the shared framework-manifest reader (_lib/manifest.sh) unconditionally, so an older host seed that predates that helper aborted with a bare, obscure `source: No such file or directory`. It now checks the library FIRST and FAILS LOUD naming the missing file + the remedy (re-seed the framework, then retry --component wake). The dependency is genuinely required (Gate A cannot be skipped on an enforcement path), so it fails loud rather than degrading. (b) SYSTEMD SEARCH PATH — wi_install copies mosaic-wake.service under mosaic home (systemd/user/, framework-owned) but `systemctl --user` searches ~/.config/systemd/user/, so the unit was invisible and could not be enabled/started. install now LINKS the unit into the user systemd search path (symlink -> the mosaic-home SSOT copy, so upgrades propagate) and post-install VALIDATES it resolves (search-path entry exists, dereferences to a readable, well-formed unit; an opportunistic `systemctl --user cat` probe runs only behind a guard, since the installer may run where no user manager is live). Both steps are idempotent (a re-install neither duplicates nor breaks the link). #869 ADDITIVE: the link target lives OUTSIDE mosaic home, so it is not a framework-manifest path — ownership of the SSOT unit stays systemd/** in the single framework-manifest.txt authority; NO new owned path, NO second authority. Red-first: test-wake-install.sh gains I9 (a: missing _lib/manifest.sh fails loud naming dep + remedy, not a bare source error; positive control installs) and I10 (b: install links the unit into the search path + validate resolves; negative control catches a not-in-path unit; idempotent re-install). Both go RED against the prior code and GREEN after. Wake component manifest.txt bumped 0.6.6 -> 0.6.7 (VERSION metadata only). Closes #913 Part of #892 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0158NZqN2n2ymKFeJAZ4GUCb
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user