docs(agent): D1 — README self-update section, REUSE, CHANGELOG v0.70.0, CONTEXT
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -191,6 +191,54 @@ The agent is **external** to the controller container, so it survives the contro
|
||||
mid-swap (which the controller cannot do to itself). `GuestBinder.GuestExec` is the single `pct exec`
|
||||
seam. Exercise directly with `--selftest=controller-swap -vmid <id> -image <ref>`.
|
||||
|
||||
## Agent self-update (operator-signed, A/B slots, crash-loop auto-rollback — v0.70.0, TASK D1)
|
||||
|
||||
The agent updates ITSELF the same way it swaps the controller: **the thing that performs rollback is
|
||||
never the thing being updated.** For the agent that means systemd + an ~80-line root shell wrapper
|
||||
(`configs/felhom-selfupdate-guarded`) that changes almost never; the Go binary is what flips.
|
||||
|
||||
**Trust model.** An update is an **operator-signed `agent_update` op** delivered through the existing
|
||||
signed-jobs pipeline (same LOCKED authz gate as `storage_wipe`/`decommission`). The signed params pin
|
||||
the exact **version + sha256**, so the pinned sha is the *only* integrity root — **neither a
|
||||
compromised hub (dumb transport) nor a compromised Gitea (dumb storage) can substitute a binary.**
|
||||
The operator signs offline with `felhom-opsign -op agent_update -agent-version <v> -sha256 <hex>`.
|
||||
|
||||
**The flow** (`internal/selfupdate/` = the Go half; the wrapper = the root half):
|
||||
|
||||
1. The control loop sees a pending signed op → the gate verifies it (pinned-key SSHSIG → namespace →
|
||||
allow-list → crypto → host → time → **durable nonce-burn**) → the `agent_update` executor runs.
|
||||
2. Executor downloads the binary for the signed version from the config'd artifact host
|
||||
(`selfupdate.url_template`, `{version}` interpolated) to `/var/lib/felhom-agent/selfupdate/`,
|
||||
verifies its sha256 against the **signed** value (mismatch → refuse, remove, agent untouched),
|
||||
and hands it to `sudo -n felhom-selfupdate-guarded apply <staged> <sha>`. The job is completed on
|
||||
the hub **after verify+download, before apply** (the nonce is already burned — a queued job would
|
||||
only re-fetch and no-op on the spent nonce; a failed/rolled-back update is visible via the report).
|
||||
3. The wrapper (as root) **re-verifies** the sha, confines the staged path to the staging dir, asserts
|
||||
same-filesystem (the atomic-rename guarantee), snapshots the current binary to `.prev`, atomically
|
||||
`mv`s the new binary into place, writes a `pending.json` marker, `reset-failed`s, and schedules a
|
||||
**detached** restart (`systemd-run --on-active=2s … systemctl restart felhom-agent`, so the caller
|
||||
survives to log the handoff).
|
||||
4. The **new** binary boots; after it has run cleanly for a dwell (`selfupdate.dwell_seconds`, default
|
||||
60) *and* core init is done, `internal/selfupdate.Manager` calls the wrapper's `commit` (clears the
|
||||
marker; `.prev` retained as a manual net). A pending marker naming a *different* version than the
|
||||
running binary is **not** committed — loud WARN, marker left so the report shows why (a human
|
||||
decides).
|
||||
5. **Crash-loop auto-rollback (the safety property).** If the new binary crashes, systemd's
|
||||
`OnFailure=felhom-agent-rollback.service` (the `felhom-agent-limits.conf` drop-in) runs the
|
||||
wrapper's `rollback`: pending marker present → restore `.prev` byte-identical → clear marker →
|
||||
restart → the old binary is back **within seconds of the first crash**. On systemd 257 `OnFailure=`
|
||||
fires on *every* crash, so rollback triggers at the first one; the marker-guard makes every later
|
||||
fire (and any crash with no update in flight) a harmless no-op. The tuned start-limit
|
||||
(`[Unit] StartLimitIntervalSec=120 + StartLimitBurst=4`) is the terminal **backstop** (e.g. an
|
||||
environmental crash loop of the known-good binary → terminal `failed` ≈20s → the hub's
|
||||
`host_staleness` dead-man's-switch alerts the operator).
|
||||
|
||||
**Design provenance:** every systemd behaviour above is empirically validated in
|
||||
`felhom.eu/documentation/audits/SPIKE-agent-selfupdate-2026-07-05.md` (the SF-findings). The host
|
||||
report carries `selfupdate_pending` (+ version) so a runs-but-never-commits binary is visible even
|
||||
though it never crashes. v1 scope: no hub-floor auto-update, no auto-retry of a failed update, no
|
||||
pending-timeout auto-rollback (a stuck-but-alive binary is caught by `host_staleness`).
|
||||
|
||||
### TLS trust
|
||||
|
||||
The host serves a self-signed cert. Verification is **not** blanket-disabled. Pick one in
|
||||
|
||||
Reference in New Issue
Block a user