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:
2026-07-05 15:36:15 +02:00
parent 582917d12d
commit b6bad953b5
2 changed files with 56 additions and 7 deletions
+17 -7
View File
@@ -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