installer v1.28.0: the removal genuinely reverses the installation (R-316)
gates / gates (push) Successful in 13s

v1.27.0's fix worked exactly once per machine. Measured on drill-r50 from virgin,
on the PUBLISHED v1.27.0, before anything was changed: cycle 1 recorded 'no' and
freed :53; cycle 2 recorded 'yes' and left dnsmasq running on 0.0.0.0:53; cycle 3
refused, exit 1. Every box already in the field is at cycle 2, and a reinstall
onto a machine that has had Felhom is cycle 2 by definition.

Why cycle 2 says yes: the preflight's ownership question is dpkg-query package
presence and nothing else - not the absence of a record. Stopping the unit and
leaving the package made our own package read as the household's one cycle later.

Now the uninstall removes the package when the record says we installed it. Order
unchanged and load-bearing: read the record, act, then delete the state file that
holds it. TWO packages are recorded, because dnsmasq ships the unit and
dnsmasq-base ships /usr/sbin/dnsmasq, and each is taken back only if we added it.

The dependency check is a SIMULATION, not a guess: apt-get -s purge is asked what
it would remove and the purge proceeds only if that set is a subset of ours;
otherwise stop+disable, naming the package that blocked it. Never interactive,
never fatal, and the success is re-queried rather than read off an exit code.

Watched: three fixed cycles -> install 3 PASSES; a household resolver untouched; a
dependent package not purged and named; no record -> untouched with the command
named. Red-proofs with the mutation asserted applied: remove the purge -> cycle 3
refuses in those exact words; remove the ownership check -> a household resolver is
purged; infer ownership -> the guess is taken.

Also: R-317 (the agent stats a path dnsmasq-base owns to decide whether to install
dnsmasq - pre-existing, now reachable), R-318 (no honest ownership marker exists
for existing boxes; the preflight message is the mechanism), and the status page's
decisions section rewritten to say what each decision costs and what doing nothing
selects.
This commit is contained in:
2026-08-13 08:16:04 +02:00
parent d102ca5767
commit fc737b0fc0
5 changed files with 239 additions and 21 deletions
+52
View File
@@ -1,3 +1,55 @@
## felhom-host-install.sh v1.28.0 — the removal genuinely reverses the installation (2026-08-13, R-316)
**v1.27.0's fix worked exactly once per machine, and this is the measurement.** Three full cycles on
`drill-r50` from `virgin`, on the PUBLISHED v1.27.0, before anything was changed:
| cycle | recorded | uninstall did | result |
|---|---|---|---|
| 1 | `no` | stop + disable | `:53` FREE |
| 2 | **`yes`** | **left it running** | `0.0.0.0:53` taken |
| 3 | — | — | **PRE-FLIGHT FAIL (exit 1)***"a resolver is already bound to :53"* |
**Why cycle 2 says `yes`:** the preflight asks `dpkg-query` whether `dnsmasq` is installed and nothing
else — **package presence alone** (not the absence of a record). v1.27.0 stopped the unit and left the
package, so the answer stayed yes and our own package became "the household's" one cycle later. **Every
box already in the field is at cycle 2**, and a reinstall onto a machine that has had Felhom is cycle 2
by definition.
**Now:** when the install-time record says we installed it, the uninstall **removes the package** as
well as stopping the unit — which is what makes the next install a first install again. Order is
unchanged and load-bearing: read the record, act, and only then delete the state file that holds it.
**TWO packages are recorded, not one.** `dnsmasq` ships the systemd unit; **`dnsmasq-base` ships
`/usr/sbin/dnsmasq`**. They are separately installable, so each is recorded at preflight and taken back
only if we added it.
**Guard rails, all measured on the box rather than reasoned:**
- **Ownership is read, never inferred** — no mtimes, no package metadata, no file dates.
- **The dependency check is a SIMULATION.** `apt-get -s purge` is asked what it would remove, and the
purge proceeds only if the answer is a subset of what we are entitled to remove. Otherwise: stop +
disable, and the log names the package that stopped us.
- **Never interactive, never fatal.** A wedged apt cannot strand a teardown — the failure is recorded
and restated in the closing NOTE, because a half-completed teardown is how cycle 3 refuses.
- **The success is asserted, not assumed:** after `apt-get` exits 0 the package is re-queried, because
an exit code is not an observation.
**Scenarios, each watched:** three fixed cycles → **install 3 PASSES**; a household resolver →
untouched; a dependent package (`household-dns-thing`) → **not purged, named in the log**, `:53` still
freed; no ownership record → untouched, reason logged, exact command named.
**Red-proofs, mutation asserted applied each time:** remove the purge → **cycle 3 refuses again in
those exact words**; remove the ownership check → **a household resolver is purged**; infer ownership
when there is no record → **the guess is taken and a field box loses its own DNS**.
**Known residue, filed as R-317:** where `dnsmasq-base` pre-dated Felhom we correctly keep it, but it
owns `/usr/sbin/dnsmasq`, which the agent stats to decide whether to install
(`felhom-agent internal/lanresolver/lanresolver.go:105`). On that host shape the next install can skip
the apt step and then fail to enable a unit that is gone. The uninstall says so explicitly rather than
leaving it to be rediscovered from a silent resolver.
---
## PUBLISHED — `installer-v1.27.0` (2026-08-12)
**v1.26.0 (R-297) and v1.27.0 (R-300) were both written, pushed and deliberately left unpublished