feat(host-install)+docs: D1 — install self-update artifacts on day-0 + architecture §11 self-update (implemented)
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user