From b6bad953b534cf98fd9381a377032b020d26e64d Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Sun, 5 Jul 2026 15:36:15 +0200 Subject: [PATCH] =?UTF-8?q?feat(host-install)+docs:=20D1=20=E2=80=94=20ins?= =?UTF-8?q?tall=20self-update=20artifacts=20on=20day-0=20+=20architecture?= =?UTF-8?q?=20=C2=A711=20self-update=20(implemented)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit felhom-host-install.sh installs felhom-selfupdate-guarded (sh -n), the rollback unit + the [Unit] start-limit drop-in (daemon-reload) so day-0 boxes get operator-signed self-update from birth; non-fatal on pre-D1 agent repos. 03-host-agent.md §11 updated to the shipped mechanism (signed op, A/B wrapper, OnFailure rollback, tuned start-limit backstop). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6 --- documentation/architecture/03-host-agent.md | 24 +++++++++---- scripts/felhom-host-install.sh | 39 +++++++++++++++++++++ 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/documentation/architecture/03-host-agent.md b/documentation/architecture/03-host-agent.md index 8d4465b..c13bf51 100644 --- a/documentation/architecture/03-host-agent.md +++ b/documentation/architecture/03-host-agent.md @@ -446,13 +446,23 @@ buildable until then; recorded here so the front-half built in slice 7 lands rea ## 11. Self-update -- **Agent (the hard case — a host service, no snapshot-rollback).** **A/B layout:** download → - verify signature → stage as the inactive slot → flip a `current → good|new` symlink → restart. - **Revert authority lives outside the swapped binary** — `Restart=always` alone just - crash-loops a bad binary — so a **separate health-gate** (a systemd oneshot `ExecStartPost` - probe, or a tiny supervisor unit) flips `current` back to last-good and restarts on a failed - health window. The new version is **committed as "good" only after a clean health window**. - Triggered by a hub signed job within the update window; manual always allowed. Journaled (§10). +- **Agent (IMPLEMENTED — v0.70.0, TASK D1; provenance `audits/SPIKE-agent-selfupdate-2026-07-05.md`).** + The hard case (a host service, no snapshot-rollback). Delivered as an **operator-signed + `agent_update` op** through the signed-jobs pipeline (same LOCKED gate as every destructive op) — + the signed params pin **version + sha256**, so the pinned sha is the ONLY integrity root: a + compromised hub (transport) or Gitea (storage) can never substitute a binary. **A/B flip** by a + root-owned guarded wrapper (`felhom-selfupdate-guarded`): the agent downloads + verifies the sha, + the wrapper re-verifies as root → `.prev` snapshot → atomic same-fs `mv` → `pending.json` marker → + detached restart (`systemd-run` transient timer, outside the agent cgroup). The new binary + **commits** (clears the marker) only after a clean dwell + core init. **Revert authority lives + outside the swapped binary:** systemd's `OnFailure=felhom-agent-rollback.service` runs the wrapper's + pending-guarded `rollback` — on systemd 257 OnFailure fires on the FIRST crash, so a bad binary is + reverted within seconds; the tuned `[Unit]` start-limit (`StartLimitIntervalSec=120`, + `StartLimitBurst=4`) is the terminal backstop that also lets the hub's `host_staleness` dead-man's + switch alert on an environmental crash loop. The wrapper + systemd units are what change almost + never — the binary is what flips. Report field `selfupdate_pending` surfaces a runs-but-never-commits + binary. v1 scope-outs: no hub-floor auto-update, no failed-update auto-retry (the operator re-signs), + no pending-timeout auto-rollback. - **Controller (the easy case — it's a guest).** The agent owns the controller's lifecycle, so the **agent updates the controller**: snapshot-before-update (free rollback, because the controller *is* a snapshottable guest) → pull new image → redeploy → health-check → rollback diff --git a/scripts/felhom-host-install.sh b/scripts/felhom-host-install.sh index 3105c41..123828c 100644 --- a/scripts/felhom-host-install.sh +++ b/scripts/felhom-host-install.sh @@ -1264,6 +1264,19 @@ step_agent_install() { log_success " installed /usr/local/sbin/felhom-mkfs-guarded (0755, the guarded mkfs path)" fi + # Self-update guarded wrapper (TASK D1) — the ONLY binary-swap path the sudoers permits. Install + # it BEFORE the sudoers (which allowlists it), 0755 root:root under /usr/local/sbin. sh -n first. + if $DRY_RUN; then + log_dry "fetch configs/felhom-selfupdate-guarded ; sh -n ; install 0755 -> /usr/local/sbin/felhom-selfupdate-guarded" + else + local sutmp; sutmp=$(mktemp -t felhom-selfupd.XXXXXX) + fetch_raw "configs/felhom-selfupdate-guarded" "$sutmp" + sh -n "$sutmp" || { rm -f "$sutmp"; die "fetched felhom-selfupdate-guarded failed sh -n — refusing to install"; } + install -m 0755 -o root -g root "$sutmp" /usr/local/sbin/felhom-selfupdate-guarded + rm -f "$sutmp" + log_success " installed /usr/local/sbin/felhom-selfupdate-guarded (0755, the guarded A/B binary-swap path)" + fi + # Sudoers — fetch the canonical file, validate with visudo -cf BEFORE installing (0440 root:root). if $DRY_RUN; then log_dry "fetch configs/felhom-agent.sudoers ; visudo -cf ; install 0440 -> $AGENT_SUDOERS" @@ -1292,6 +1305,32 @@ step_agent_install() { systemctl enable felhom-agent >/dev/null 2>&1 || true log_success " installed $AGENT_UNIT + enabled (started in step 6 after config)" fi + + # Self-update rollback unit + start-limit drop-in (TASK D1). The drop-in gives felhom-agent the + # tuned start-limit (SPIKE-agent-selfupdate: [Unit] StartLimitIntervalSec=120 + Burst=4 → a + # crash-looping update reaches terminal `failed` in ~20s instead of looping forever) and the + # OnFailure= that auto-reverts. Both are idempotent installs (+ daemon-reload). Non-fatal on a + # box whose agent repo predates them (the raw fetch would 404) — self-update just stays manual. + if $DRY_RUN; then + log_dry "fetch configs/felhom-agent-rollback.service -> /etc/systemd/system/ ; fetch configs/felhom-agent-limits.conf -> $AGENT_UNIT.d/ ; daemon-reload" + else + local rbtmp; rbtmp=$(mktemp -t felhom-rollback.XXXXXX) + if fetch_raw "configs/felhom-agent-rollback.service" "$rbtmp" 2>/dev/null; then + install -m 0644 -o root -g root "$rbtmp" /etc/systemd/system/felhom-agent-rollback.service + local ddir="${AGENT_UNIT}.d"; mkdir -p "$ddir" + local lmtmp; lmtmp=$(mktemp -t felhom-limits.XXXXXX) + fetch_raw "configs/felhom-agent-limits.conf" "$lmtmp" + # [SF-3] the keys MUST be in [Unit] — refuse a drop-in that would half-apply in [Service]. + grep -q '^\[Unit\]' "$lmtmp" || { rm -f "$lmtmp" "$rbtmp"; die "self-update limits drop-in missing [Unit] section — refusing"; } + install -m 0644 -o root -g root "$lmtmp" "$ddir/felhom-agent-limits.conf" + rm -f "$lmtmp" + systemctl daemon-reload + log_success " installed self-update rollback unit + start-limit drop-in (auto-rollback armed)" + else + log_skip " self-update rollback artifacts not in the agent repo yet — self-update stays manual" + fi + rm -f "$rbtmp" + fi _state_mark agent_install }