Compare commits

...

2 Commits

Author SHA1 Message Date
librelad
9e5b336d1e Merge claude/2 2026-05-31 20:53:54 +01:00
librelad
96b04392dc feat(distribution): Phase 3 — hotfix scan generator + severity-split auto-apply
- CFG_HOTFIX_AUTO (security-breakage|all|off, default security-breakage) seeded in
  general_terminal; reaches existing installs via the add-only config reconciler.
- webui_artifact_scan.sh (webuiArtifactScan): fetch+verify the signed index, write
  artifacts_available.json ATOMICALLY (build in temp → jq-validate → one write;
  keep the prior file on any failure — never emits broken JSON). Annotates each
  artifact with applied (a per-id record exists) + applicable (target installed).
- artifactApplyAuto + `libreportal artifact apply-auto`: enqueue apply tasks for
  the eligible signed hotfixes — only when the index is VERIFIED-signed, only
  auto==true + in the severity policy + applicable + not already applied. Each
  apply is its own task (visible in the log + History), never applied inline.
- `updater check` now also refreshes the index (webuiArtifactScan) and runs
  artifactApplyAuto — one front door, no second phone-home.

Unit-tested 13/13: policy filtering (security-breakage / off / all), auto:false
exclusion, already-applied skip, non-installed-app skip, unsigned-index fail-closed,
and the scan transform's signed/applied/applicable fields.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 20:53:54 +01:00
7 changed files with 143 additions and 0 deletions

View File

@ -2,6 +2,7 @@
# Terminal - System utilities and advanced settings **ADVANCED**
# ================================================================================
CFG_UPDATER_CHECK=60 # Update Check Interval - Hours between system update checks
CFG_HOTFIX_AUTO=security-breakage # Hotfix Auto-Apply - Which signed hotfix severities apply automatically on the update check [security-breakage|all|off]
CFG_SWAPFILE_SIZE=2G # Swap File Size - Size of swap file for memory management
CFG_GENERATED_PASS_LENGTH=14 # Password Length - Length for auto generated passwords
CFG_GENERATED_USER_LENGTH=8 # Username Length - Length for auto generated usernames

View File

@ -521,3 +521,45 @@ artifactRevert() {
isError "Revert of $id was incomplete -- the applied-record is kept so you can retry: libreportal artifact revert $id (MANUAL recovery may be needed)."
return 1
}
# ----------------------------------------------------------------------------
# artifactApplyAuto -- enqueue apply tasks for the auto-eligible hotfixes, gated
# by CFG_HOTFIX_AUTO (security-breakage|all|off). Called from `updater check`.
# Only acts on a VERIFIED-signed index; only artifacts with auto==true, in the
# severity policy, applicable, and not already applied. Each apply is enqueued as
# its own task (visible in the task log + History) -- never applied inline here.
# ----------------------------------------------------------------------------
artifactApplyAuto() {
_artifactNeedJq || return 0
local policy="${CFG_HOTFIX_AUTO:-security-breakage}"
[[ "$policy" == "off" ]] && { isNotice "Hotfix auto-apply is off (CFG_HOTFIX_AUTO=off)."; return 0; }
local index; index="$(lpFetchIndex)" || { isNotice "artifact apply-auto: no index available."; return 0; }
if [[ "$LP_INDEX_SIGSTATE" != "verified" ]]; then
isNotice "artifact apply-auto: index is unsigned (signing not activated) — not auto-applying."; return 0
fi
local ids id art sev app installed enqueued=0
ids="$(printf '%s' "$index" | jq -r '.artifacts[]? | select(.auto==true and (.type=="hotfix")) | .id' 2>/dev/null)"
while IFS= read -r id; do
[[ -z "$id" ]] && continue
art="$(printf '%s' "$index" | jq -ce --arg id "$id" '.artifacts[]?|select(.id==$id)' 2>/dev/null)"
[[ -n "$art" ]] || continue
sev="$(jq -r '.severity // "tweak"' <<<"$art")"
case "$policy" in
all) : ;;
*) [[ "$sev" == "security" || "$sev" == "breakage" ]] || continue ;; # security-breakage (default)
esac
# skip if already applied
[[ -f "$(_artifactRecordFile "$id")" ]] && continue
# skip if app-scoped but the app isn't installed (applicable gate; full
# gates re-checked at apply time)
app="$(jq -r '.applies_when.app // empty' <<<"$art")"
[[ -n "$app" && ! -d "${containers_dir%/}/$app" ]] && continue
cliTaskRun "libreportal artifact apply $id" "artifact_apply" "$id" ""
enqueued=$((enqueued + 1))
done <<< "$ids"
if (( enqueued > 0 )); then isSuccessful "Queued $enqueued auto-hotfix(es) for apply (policy: $policy)."
else isNotice "No new auto-hotfixes to apply (policy: $policy)."; fi
}

View File

@ -59,6 +59,10 @@ cliHandleArtifactCommands()
cliTaskRun "libreportal artifact revert $id" "artifact_revert" "$id" ""
fi
;;
"apply-auto")
# Decide + enqueue (gated by CFG_HOTFIX_AUTO); each apply is its own task.
artifactApplyAuto
;;
*)
cliShowArtifactHelp
;;

View File

@ -30,6 +30,16 @@ cliHandleUpdaterCommands()
source "$install_scripts_dir/webui/data/generators/updater/webui_updater_scan.sh" 2>/dev/null
fi
webuiUpdaterScan
# Hotfix channel: refresh the signed artifact index for the WebUI, then
# auto-apply the eligible signed hotfixes (gated by CFG_HOTFIX_AUTO).
if ! declare -F webuiArtifactScan >/dev/null 2>&1; then
source "$install_scripts_dir/webui/data/generators/updater/webui_artifact_scan.sh" 2>/dev/null
fi
declare -F webuiArtifactScan >/dev/null 2>&1 && webuiArtifactScan
if ! declare -F artifactApplyAuto >/dev/null 2>&1; then
source "$install_scripts_dir/cli/commands/artifact/cli_artifact_apply.sh" 2>/dev/null
fi
declare -F artifactApplyAuto >/dev/null 2>&1 && artifactApplyAuto
;;
"apply"|"now")

View File

@ -33,6 +33,7 @@ webui_scripts=(
"webui/data/generators/system/webui_system_memory.sh"
"webui/data/generators/system/webui_system_metrics.sh"
"webui/data/generators/system/webui_system_update.sh"
"webui/data/generators/updater/webui_artifact_scan.sh"
"webui/data/generators/updater/webui_updater_scan.sh"
"webui/data/lock/webui_check_update_lock.sh"
"webui/data/lock/webui_create_update_lock.sh"

View File

@ -103,6 +103,7 @@ declare -gA LP_FN_MAP=(
[appWebuiRefresh_gluetun]="gluetun/scripts/gluetun_providers.sh"
[_artifactAppliedDir]="cli/commands/artifact/cli_artifact_apply.sh"
[artifactApply]="cli/commands/artifact/cli_artifact_apply.sh"
[artifactApplyAuto]="cli/commands/artifact/cli_artifact_apply.sh"
[_artifactComposeImage]="cli/commands/artifact/cli_artifact_apply.sh"
[_artifactFetchPayload]="cli/commands/artifact/cli_artifact_apply.sh"
[_artifactGenDir]="cli/commands/artifact/cli_artifact_apply.sh"
@ -889,6 +890,7 @@ declare -gA LP_FN_MAP=(
[viewLibrePortalConfigs]="config/core/config_manage_menu.sh"
[viewLogs]="logs/installed_apps.sh"
[viewLogsAppMenu]="logs/app_log_menu.sh"
[webuiArtifactScan]="webui/data/generators/updater/webui_artifact_scan.sh"
[webuiCheckUpdateLock]="webui/data/lock/webui_check_update_lock.sh"
[webuiContainerSetup]="webui/data/utils/webui_container_setup.sh"
[webuiCreateAppFieldMappings]="webui/data/generators/categories/webui_create_app_field_mappings.sh"
@ -1043,6 +1045,7 @@ declare -gA LP_FN_ROOT=(
[appWebuiRefresh_gluetun]="containers"
[_artifactAppliedDir]="scripts"
[artifactApply]="scripts"
[artifactApplyAuto]="scripts"
[_artifactComposeImage]="scripts"
[_artifactFetchPayload]="scripts"
[_artifactGenDir]="scripts"
@ -1829,6 +1832,7 @@ declare -gA LP_FN_ROOT=(
[viewLibrePortalConfigs]="scripts"
[viewLogs]="scripts"
[viewLogsAppMenu]="scripts"
[webuiArtifactScan]="scripts"
[webuiCheckUpdateLock]="scripts"
[webuiContainerSetup]="scripts"
[webuiCreateAppFieldMappings]="scripts"
@ -2004,6 +2008,7 @@ appUpdateSpecifics_pihole() { source "${install_containers_dir}pihole/scripts/pi
appWebuiRefresh_gluetun() { source "${install_containers_dir}gluetun/scripts/gluetun_providers.sh"; appWebuiRefresh_gluetun "$@"; }
_artifactAppliedDir() { source "${install_scripts_dir}cli/commands/artifact/cli_artifact_apply.sh"; _artifactAppliedDir "$@"; }
artifactApply() { source "${install_scripts_dir}cli/commands/artifact/cli_artifact_apply.sh"; artifactApply "$@"; }
artifactApplyAuto() { source "${install_scripts_dir}cli/commands/artifact/cli_artifact_apply.sh"; artifactApplyAuto "$@"; }
_artifactComposeImage() { source "${install_scripts_dir}cli/commands/artifact/cli_artifact_apply.sh"; _artifactComposeImage "$@"; }
_artifactFetchPayload() { source "${install_scripts_dir}cli/commands/artifact/cli_artifact_apply.sh"; _artifactFetchPayload "$@"; }
_artifactGenDir() { source "${install_scripts_dir}cli/commands/artifact/cli_artifact_apply.sh"; _artifactGenDir "$@"; }
@ -2790,6 +2795,7 @@ viewConfigs() { source "${install_scripts_dir}config/core/config_main_menu.sh";
viewLibrePortalConfigs() { source "${install_scripts_dir}config/core/config_manage_menu.sh"; viewLibrePortalConfigs "$@"; }
viewLogs() { source "${install_scripts_dir}logs/installed_apps.sh"; viewLogs "$@"; }
viewLogsAppMenu() { source "${install_scripts_dir}logs/app_log_menu.sh"; viewLogsAppMenu "$@"; }
webuiArtifactScan() { source "${install_scripts_dir}webui/data/generators/updater/webui_artifact_scan.sh"; webuiArtifactScan "$@"; }
webuiCheckUpdateLock() { source "${install_scripts_dir}webui/data/lock/webui_check_update_lock.sh"; webuiCheckUpdateLock "$@"; }
webuiContainerSetup() { source "${install_scripts_dir}webui/data/utils/webui_container_setup.sh"; webuiContainerSetup "$@"; }
webuiCreateAppFieldMappings() { source "${install_scripts_dir}webui/data/generators/categories/webui_create_app_field_mappings.sh"; webuiCreateAppFieldMappings "$@"; }

View File

@ -0,0 +1,79 @@
#!/bin/bash
# WebUI artifact-index data generator
# ---------------------------------------------------------------------------
# Writes the read-only JSON the updater "Improvements" (hotfix) stream reads:
# frontend/data/updater/generated/artifacts_available.json
#
# It fetches + verifies the signed artifact index (lpFetchIndex), annotates each
# artifact with `applied` (a per-id applied-record exists) and `applicable` (the
# target app is installed / system-scope), and writes ATOMICALLY: build into a
# temp, validate it, then one runFileWrite. On ANY fetch/verify failure it KEEPS
# the prior file — it must never emit broken JSON, and the WebUI degrades
# gracefully when the file is absent. Run by `libreportal updater check`.
webuiArtifactScan() {
local out_dir="${containers_dir%/}/libreportal/frontend/data/updater/generated"
local out="$out_dir/artifacts_available.json"
local applied_dir="$out_dir/applied"
runFileOp mkdir -p "$out_dir" 2>/dev/null || true
# Lazy-loader gap: ensure the read primitives are present.
if ! declare -F lpFetchIndex >/dev/null 2>&1; then
source "$install_scripts_dir/source/fetch.sh" 2>/dev/null
source "$install_scripts_dir/source/artifacts.sh" 2>/dev/null
fi
if ! command -v jq >/dev/null 2>&1; then
isNotice "webuiArtifactScan: jq not available — keeping the prior artifacts_available.json."
return 0
fi
local index
if ! index="$(lpFetchIndex)"; then
isNotice "webuiArtifactScan: no verified index available — keeping the prior file."
return 0
fi
local signed="false"; [[ "$LP_INDEX_SIGSTATE" == "verified" ]] && signed="true"
local serial now
serial="$(_lpJsonNum "$index" index_serial)"
now="$(date -Iseconds 2>/dev/null || date)"
# Applied ids = basenames of applied/*.json, as a JSON array.
local applied_ids="[]" f
if compgen -G "$applied_dir/*.json" >/dev/null 2>&1; then
applied_ids="$(for f in "$applied_dir"/*.json; do basename "$f" .json; done | jq -R . | jq -cs .)"
fi
# Installed apps = dir names under containers_dir, for the applicable check.
local installed="[]" d
if [[ -d "$containers_dir" ]]; then
installed="$(for d in "$containers_dir"/*/; do [[ -d "$d" ]] && basename "$d"; done | jq -R . | jq -cs .)"
fi
local tmp; tmp="$(mktemp)"
printf '%s' "$index" | jq \
--arg now "$now" --arg signed "$signed" --arg serial "${serial:-0}" \
--argjson applied "$applied_ids" --argjson installed "$installed" '
{ generated_at: $now,
signed: ($signed=="true"),
serial: ($serial|tonumber? // 0),
artifacts: [ .artifacts[]? | {
id, type,
severity: (.severity // "tweak"),
auto: (.auto // false),
reversible: (.reversible // true),
title: (.title // ""),
why: (.why // ""),
app: (.applies_when.app // null),
applied: (.id as $i | ($applied | index($i)) != null),
applicable: ((.applies_when.app // null) as $a | if $a == null then true else ($installed | index($a)) != null end)
} ] }' > "$tmp" 2>/dev/null
if ! jq empty "$tmp" 2>/dev/null; then
isNotice "webuiArtifactScan: generated JSON was invalid — keeping the prior file."
rm -f "$tmp"; return 1
fi
runFileWrite "$out" < "$tmp"
rm -f "$tmp"
runFileOp chown "$docker_install_user":"$docker_install_user" "$out" 2>/dev/null || true
isSuccessful "Artifact index refreshed (serial=${serial:-?}, signed=$signed)."
}