# REPORT — v0.42.0: agentic controller update — in-guest image swap + rollback (Phase 1) **Repo:** `felhom-agent` · **Version:** `v0.41.0` → **`v0.42.0`** · **Date:** 2026-06-26 ## What & why The controller's old self-update (`docker compose up -d` on an in-container compose path) is dead in the LXC architecture. Phase 1 moves the **container swap** to the host agent: the in-guest controller pulls the target image, then the agent — external to the controller container, so it survives the controller being killed mid-swap — rewrites `/etc/felhom-controller-image`, restarts the golden's `felhom-controller-bootstrap.service`, verifies health, and **rolls back** if the new controller doesn't come up. The guest is never left without a controller. ## Baseline felhom-agent `4725396` v0.41.0 → v0.42.0. Cross-repo: felhom-controller v0.85.0 (the pull + delegate half). ## Changes - **`internal/localapi/controllerswap.go` (new)** — `ControllerSwapper` (testable, over a `GuestExecutor` seam) + handlers: - `POST /controller/swap {image}` (`withGuest`) → **202** `{status, previous_image, target_image}`, then async: record previous to `/var/lib/felhom-agent/controller-swap-.json` (crash-safety) → confirm the target image is present in the guest (else abort, **no swap**) → write the image file → restart the bootstrap unit → poll the new controller to healthy (`docker inspect`, ≤90s) → **roll back** + restart if not. Strict image-ref gate (`gitea.dooplex.hu/admin/felhom-controller:`); single-flight per guest (409). - `GET /controller/swap/status` → `{state, current, previous, target, error}`. - **`internal/localapi/guestbind.go`** — `GuestBinder.GuestExec` (the single `pct exec` seam). - **`internal/localapi/server.go`** — `ControllerSwap` option + route wiring + single-flight map. - **`cmd/felhom-agent/main.go`** — `ControllerSwap: guestBinder`; `--selftest=controller-swap -vmid -image` (+ the allowlist fix — see below); version → 0.42.0. ## Tests (`internal/localapi/controllerswap_test.go`) — all green happy swap; **rollback-on-unhealthy** (image reverted to previous + restart called ≥2×); image-absent → no swap (no file write); running-with-no-healthcheck → healthy; bad image → 400; single-flight → 409. - **Companion red-proof:** disabling the verify→rollback left the guest on the bad image and **failed** `TestControllerSwap_RollbackOnUnhealthy` (`image file = …:0.84.0 after rollback, want previous …:0.77.0`) → reverted → green. The rollback assertion is load-bearing. - localapi package: 70 → **71** tests. `go build ./... && go vet ./... && go test ./...` — all `ok`. ## Live test on demo 9201 (operator-authorized; agent v0.42.0 deployed to felhom-pve) - **Bug found live + fixed:** `--selftest=controller-swap` printed flag-usage — `controller-swap` was missing from `selftestFlag.Set`'s allowlist. Fixed (commit `b17d07b`), rebuilt, redeployed. - **Swap 0.77→0.84** (`--selftest=controller-swap`, after pre-pulling 0.84): state=**done**, new controller `Up (healthy)`, `/etc/felhom-controller-image` = 0.84.0. (~9s to healthy.) - **Rollback** (swap to a present-but-broken `:99.99.99` built from a controller base with `ENTRYPOINT /bin/false`): 90s verify timeout → **rolled back to 0.84.0 healthy**, state=failed, current=0.84.0 — the guest was never left without a controller. - **E2E (real UI button)** via claude-in-chrome on `felhom.demo-felhom.eu/settings`: with the demo on 0.85.0 (new code) and 0.85.1 pushed as latest, clicked **"Frissítés telepítése"** → the controller pulled 0.85.1 in-guest and called the agent → agent log: swap → "new controller healthy" `0.85.1` → UI reloaded showing **"Sikeres (0.85.0 → 0.85.1)"**. No "docker-compose.yml nem elérhető". - Hub reflects the version via the controller's report path: `0.84.0 → 0.85.0 → 0.85.1`. ## Observations (out of scope — not acted on) 1. **Agent host-report `controller_version` is hardcoded `""`** (`internal/hub/collect.go:180`, "slice 8 fills" — never done). So the hub's `guests.controller_version` stays empty; the controller's own report path carries the real version (verified `0.85.1`). Pre-existing, untouched here. 2. **Agent-restart-mid-swap auto-resume:** the swap records previous to a state file (so a manual/operator rollback is possible), but the agent does not yet auto-reconcile a `swapping` state file on its own restart. The common failure (controller killed mid-swap) is fully handled; agent-crash-mid-swap leaves a recoverable record. A startup reconcile is a candidate follow-up. 3. **Golden freshness:** the golden bakes controller 0.77.0; a golden rebuild at current is wanted so new provisions start current (independent of this feature). ## Commits - `6f14b66` — v0.42.0 swap + rollback + tests. - `b17d07b` — fix: register `controller-swap` in the `--selftest` allowlist (found live). No secrets in any committed file; the registry token used for the live pre-pull stayed on the host (stdin login, logged out).