diff --git a/documentation/backlog/OPEN-ITEMS.md b/documentation/backlog/OPEN-ITEMS.md index 66086ca..fdc5bfd 100644 --- a/documentation/backlog/OPEN-ITEMS.md +++ b/documentation/backlog/OPEN-ITEMS.md @@ -599,3 +599,6 @@ class (an image `VOLUME` at an unmounted path) is still live — `immich-server` | **R-296** | **The orphan card's OTHER sentence makes the same promise, and the spec says it is fine.** `SPEC-orphan-card-copy-2026-08-10.md` §1 lists `backups_remote.html:98` as *"Accurate; keep"*. It is not: it ends *„A meglévő mentések nem sérültek — a hozzájuk tartozó helyreállítási kóddal később **visszaállíthatók lehetnek**."* — the identical unevaluable promise in a different conjugation, which the spec's own regression guard (`visszaállítható lehet`, singular) does not match. Line 101 was fixed; line 98 was left **deliberately unchanged**: the instruction was not to improvise Hungarian at the customer | **READY (XS) — NEW 2026-08-10** | R-294 | Needs a wording decision, then the same treatment as line 101 and an assertion covering BOTH conjugations | operator + CC | | **R-297** | **An install took whatever golden was lying around.** Step 7 of the host installer short-circuited on any local archive with no version compare, no digest and no warning, so the manifest's sha256 — vouching from a different trust root than the code host — was consulted only on the fetch path. Discovery is newest-by-filename: correct by recency, never by verification. A box reinstalled from a stale archive can come back below the version where the off-site recovery screen exists, and below the update floor. Three goldens on `demo-hp` carry controller 0.153.0 and 0.210.0; nothing compared either to the vouched version. **FIXED in `felhom-host-install.sh` v1.26.0** (digest first, then the baked `/etc/felhom-controller-image` tag; auto-discovered mismatch re-fetches, an operator-named mismatch REFUSES; an unreadable manifest refuses). **NOT PUBLISHED and NOT YET OBSERVED FAILING** — the pre-fix stale install was never watched on `drill-r50`, so the `installer-v1.26.0` tag is deliberately not cut | **SHIPPED TO main, UNPUBLISHED (M)** | — | Observe a fresh install taking a stale local golden on `drill-r50` BEFORE publishing; then cut `installer-v1.26.0` and bump both `--ref`s in `webpage.yaml` | CC | | **R-298** | **The `/storage` page's unregistered list is filtered by `role==='user-data'`, so a drive that is also the backup target can never be registered from it.** `storage.html:363` routes anything not `user-data` into the read-only protected group with NO actions. On the rebuilt `demo-hp` the NVMe is deliberately BOTH the user-data drive and the `felhom-backup` target (`/etc/pve/storage.cfg`: `dir: felhom-backup` → `/mnt/nvme-1tb`), so it renders locked. **This is the SECOND reason that page was empty** during the reinstall rehearsal, independent of R-280's candidate-source defect, and R-280's fix does not touch it — attaching is non-destructive, so the format-wizard protection is the wrong gate for a REGISTER action | **READY (S) — NEW 2026-08-10** | R-280 | Split the role gate: `user-data` keeps destructive actions; any mounted role may be REGISTERED | CC | +| **R-299** | **The orphan card's OTHER sentence made the same unevaluable promise, and the spec called it accurate.** `backups_remote.html` line 98 — the **always-visible** half of the card, where the paragraph fixed in v0.211.0 renders only after a click — ended *„a hozzájuk tartozó helyreállítási kóddal később **visszaállíthatók lehetnek**"*. It survived for two independent reasons: `SPEC-orphan-card-copy-2026-08-10.md` §1 listed it as *"Accurate; keep"*, and **the regression guard matched one INFLECTION** (`visszaállítható lehet`, singular) which the plural does not contain. **A guard matching one inflection of a Hungarian verb guards one sentence, not the claim.** Guard broadened to the stem `visszaállíthat`; proven by planting the shipped plural (stem guard convicts, old singular guard does not match it at all). Spec corrected in both places. The two accurate halves kept | **CLOSED — controller v0.212.0** | R-294 | — | CC | +| **R-300** | **Our own uninstall left the thing that makes our own reinstall refuse.** `--uninstall` removed the dnsmasq snippet and RESTARTED the daemon, leaving it enabled and unconstrained on `0.0.0.0:53`; the next byo install's preflight then hard-refuses with *"a resolver is already bound to :53"* and **the customer goes debugging a home network that was never at fault**. Ownership is now RECORDED at preflight (`dnsmasq_preexisting`) — the only moment it is a fact, since the package is installed by the AGENT (`lanresolver.go:107`), not the installer — and honoured at removal: Felhom's → stop+disable; the owner's → restart only; **no record (every box already in the field) → restart only, fail-safe, with the reason and the exact command logged**. The preflight refusal keeps its two routes and its promise not to touch DNS on a host we do not own, and gains the missing line: when the bound resolver is dnsmasq, it says it looks like ours and names the command. **NOT OBSERVED LIVE — the install→uninstall→install cycle on `drill-r50` was not run**, so `installer-v1.27.0` is NOT cut | **SHIPPED TO main, UNPUBLISHED + UNVERIFIED LIVE (M)** | — | Run the cycle on `drill-r50` (quote the second install refusing before the fix, passing after), together with R-297's stale-golden observation — one drill session covers both; only then publish | CC | +| **R-301** | **The abandon countdown banner makes the retired promise a third time, and as a flat statement.** `controller/internal/web/templates/layout.html:143`: *"A korábbi távoli mentéseidet N nap múlva véglegesen töröljük, a kérésed szerint. **Addig még visszaszerezheted őket a helyreállítási kóddal.**"* Unlike the orphan card this one is not hedged at all. **It is probably TRUE where it renders** — it is gated on `backupMgr.AbandonStatus().Active`, i.e. the customer chose to abandon a recovery offer that exists — which is why it was NOT changed (this session was fenced to the orphan card). **But it renders in `layout.html`, i.e. on EVERY page, and a rebuilt box can have an active abandon countdown WHILE its remote store is orphaned**, so a customer can read *"we cannot determine whether these can be opened"* on the backups page and *"until then you can still retrieve them with your recovery code"* in the banner above it, about copies they have no way to tell apart. **Not established:** whether the two sentences refer to the same bytes. The spec anticipated this in its §4 note | **READY (S) — NEW 2026-08-12** | R-299 | Establish whether the two can co-render for one customer and whether they name the same copies; if so, make the banner name WHICH copy | operator + CC | diff --git a/scripts/CHANGELOG.md b/scripts/CHANGELOG.md index 1ac0e55..85f53d0 100644 --- a/scripts/CHANGELOG.md +++ b/scripts/CHANGELOG.md @@ -1,3 +1,41 @@ +## felhom-host-install.sh v1.27.0 — our removal no longer blocks our reinstall (2026-08-12, R-300) + +**Felhom's own uninstall left the thing that makes Felhom's own next install refuse.** `--uninstall` +removed the `/etc/dnsmasq.d/felhom-*.conf` snippet and **restarted** the daemon, leaving the package +installed and the unit enabled. Unconstrained, dnsmasq binds `0.0.0.0:53`, and the next install's byo +preflight hard-refuses with *"a resolver is already bound to :53"*. **The customer reads a message +that looks like it is about their home network, which was never at fault.** Measured on `demo-hp` +2026-08-09; the counterfactual (stop + disable, nothing else changed) made the preflight pass. + +**Ownership is recorded, never inferred.** The package is installed **by the AGENT** +(`felhom-agent/internal/lanresolver/lanresolver.go:107`, `apt-get install -y -q dnsmasq`), not by this +script, and conditionally — so at uninstall time "did Felhom install it?" cannot be reconstructed from +anything on the box. **Preflight now records `dnsmasq_preexisting` before anything is installed**, +which is the only moment it is a fact. Deliberately NOT a package-file mtime: that is a heuristic +dressed as a fact, and this project has been bitten by exactly that shape. + +**Three cases at removal, and the third is the one that matters for the field:** + +| record | action | +|---|---| +| `no` — Felhom installed it | **stop + disable**, so our leftover cannot block our reinstall | +| `yes` — it pre-dated us | restart only (today's behaviour). We never stop a resolver we did not install | +| *absent* — **every box already in the field** | **restart only, exactly as before**, and say so out loud | + +**What the record does for machines already in the field: nothing, deliberately.** They carry no +record, so removal fails safe to the owner's case and leaves dnsmasq running, logging the reason and +the exact command. Silently disabling a resolver on a host we cannot prove we own is the one outcome +worse than the wall this fixes. Those boxes are covered by the preflight message instead. + +**The preflight refusal keeps everything it had** — the finding, the two routes, and the explicit +promise not to touch DNS on a host Felhom does not own — and gains the one thing it lacked: **when the +bound resolver is dnsmasq, it says it looks like ours and gives the command** (`systemctl disable --now +dnsmasq`), while still saying to leave it alone if it is the household's. + +**NOT OBSERVED LIVE, and therefore NOT PUBLISHED.** The required install → uninstall → install cycle on +`drill-r50` was not run this session, so — like R-297 — this rides in `main`, which publishes nothing +(R-110). **No `installer-v1.27.0` tag is cut.** Both installer fixes now await one drill session. + ## felhom-host-install.sh v1.26.0 — an install takes the golden you approved (2026-08-10, R-297) **Step 7 short-circuited on ANY local golden archive: no version compare, no digest, no warning.** diff --git a/scripts/felhom-host-install.sh b/scripts/felhom-host-install.sh index ecb4a4d..a7dfd3d 100644 --- a/scripts/felhom-host-install.sh +++ b/scripts/felhom-host-install.sh @@ -184,7 +184,7 @@ set -euo pipefail -SCRIPT_VERSION="1.26.0" # the SINGLE version source (F-1): -h and the run banners follow it. +SCRIPT_VERSION="1.27.0" # the SINGLE version source (F-1): -h and the run banners follow it. # The hub used to carry a copy for its Setup tab; R-94 DELETED it # (2026-08-02) because the hub cannot know which version a box runs — # the Setup command fetches this script at run time. scripts/ @@ -1077,8 +1077,42 @@ run_uninstall() { [[ -e "$dconf" ]] || continue run rm -f "$dconf"; _dnsmasq_touched=true done - if $_dnsmasq_touched && systemctl is-active --quiet dnsmasq 2>/dev/null; then - run systemctl restart dnsmasq || true + # R-300 — OUR OWN REMOVAL USED TO LEAVE THE THING THAT MAKES OUR OWN REINSTALL REFUSE. + # + # Removing the snippet and RESTARTING dnsmasq leaves it running UNCONSTRAINED, so it binds + # 0.0.0.0:53 — and the next install's byo preflight hard-refuses with "a resolver is already bound + # to :53". The customer then reads a message that looks like it is about their home network, which + # was never at fault. Measured on demo-hp 2026-08-09; stopping and disabling it made the preflight + # pass with nothing else changed. + # + # Ownership is honoured, never inferred: `dnsmasq_preexisting` is recorded at PREFLIGHT, before + # anything is installed, which is the only moment it is a fact. Three cases, and the third is the + # one that matters for the field: + # no → Felhom's. Stop + disable, so a reinstall is not blocked by our own leftover. + # yes → the owner's. Restart only (today's behaviour) — we never stop a resolver we did not install. + # "" → NO RECORD. Every box installed before this change is here. FAIL SAFE to the owner's case: + # restart only, exactly as before, and say why. Silently disabling a resolver on a host we + # cannot prove we own is the one outcome worse than the wall this fixes; the preflight's + # new message covers these boxes instead, by naming the leftover and the command. + local _dnsmasq_owner; _dnsmasq_owner=$(_state_get dnsmasq_preexisting) + if $_dnsmasq_touched || systemctl is-enabled --quiet dnsmasq 2>/dev/null; then + case "$_dnsmasq_owner" in + no) + log_info " dnsmasq was installed by Felhom (recorded at install) — stopping + disabling it" + run systemctl disable --now dnsmasq || true + ;; + yes) + log_info " dnsmasq pre-dates Felhom (recorded at install) — leaving it running, restarting only" + systemctl is-active --quiet dnsmasq 2>/dev/null && run systemctl restart dnsmasq || true + ;; + *) + log_warn " dnsmasq: no ownership record for this box (installed before that was recorded)." + log_warn " Leaving it RUNNING — we do not stop a resolver we cannot prove we installed." + log_warn " If this host had no dnsmasq before Felhom, a reinstall will refuse on the :53" + log_warn " gate; clear it yourself with: systemctl disable --now dnsmasq" + systemctl is-active --quiet dnsmasq 2>/dev/null && run systemctl restart dnsmasq || true + ;; + esac fi # 5. pveum removal (presence-checked; tolerate-absent; roles deleted only after their grants). @@ -1138,7 +1172,10 @@ run_uninstall() { _uninstall_statement full log_success "UNINSTALL complete — removed: guest $vmid, the felhom-agent (unit/sudoers/binary/state/config+baks/user + selfupdate-artifacts/shared-parent/mkfs-wrapper/pbs-apply-wrapper/hook-snippet/dnsmasq-snippets), the pveum role/user/token/ACL,$( $pool_removed && printf ' the %s pool,' "$PVE_POOL") and $STATE_FILE." if $REMOVE_GOLDEN; then log_info " golden vzdump: removed."; else log_info " golden vzdump: left in place (--remove-golden to remove)."; fi - log_info " NOTE: the 'sudo' and 'dnsmasq' packages were left installed (system packages); the host record still exists in the hub — remove it there if desired." + # R-300: the packages are still not PURGED (they are system packages and purging them on a host we + # may not own is the wrong blast radius) — but dnsmasq's UNIT is now stopped+disabled when the + # install-time record says Felhom installed it, so our own leftover no longer blocks our own reinstall. + log_info " NOTE: the 'sudo' and 'dnsmasq' packages were left INSTALLED (system packages, not purged); dnsmasq's unit was stopped+disabled only if Felhom installed it. The host record still exists in the hub — remove it there if desired." $DRY_RUN && log_warn " DRY-RUN: nothing above was actually executed." return 0 } @@ -1669,6 +1706,23 @@ step_preflight() { log_info " acl storages all present or pre-positioned: ${PVE_STORAGES[*]}" fi + # R-300 — RECORD WHETHER dnsmasq PRE-EXISTS US, HERE, BEFORE ANYTHING IS INSTALLED. + # + # This is the ONLY moment the question is answerable as a fact rather than a guess. The package is + # installed later and conditionally, by the AGENT (`internal/lanresolver`, `apt-get install -y -q + # dnsmasq`) — not by this script — so "did Felhom install it?" cannot be reconstructed at uninstall + # time from anything on the box. Deliberately NOT inferred from a package file's mtime: that is a + # heuristic dressed as a fact, and this project has been bitten by exactly that shape. + # + # Recorded on EVERY mode, because appliance is where the agent actually installs it. + if command -v dpkg-query >/dev/null 2>&1 && dpkg-query -W -f='${Status}' dnsmasq 2>/dev/null | grep -q "install ok installed"; then + _state_put dnsmasq_preexisting yes + log_info " dnsmasq: already installed BEFORE Felhom — recorded; uninstall will not touch it" + else + _state_put dnsmasq_preexisting no + log_info " dnsmasq: not present before Felhom — recorded; uninstall may stop+disable it if we install it" + fi + # Host DNS :53 gate (byo only) — GL-8/F6. In byo the agent's own lan-resolver stays OFF (a config # assert enforces it), so ANY process bound to :53 is the OWNER's — and Felhom needs the guest # reachable by name on the LAN. Felhom must NOT stop/mask/kill a service on a host it does not own @@ -1681,9 +1735,21 @@ step_preflight() { if [[ -n "$_dns53" ]]; then log_error " a resolver is already bound to :53 on this host:" echo "$_dns53" | tr -s '[:space:]' ' ' | cut -c1-200 | sed 's/^/ /' >&2 + # R-300: the one thing this refusal lacked. A leftover of OUR OWN making reads exactly + # like the household's resolver, and the customer goes debugging a home network that was + # never at fault. Say so when it looks like ours, and give the exact command. + local _hint="" + if echo "$_dns53" | grep -q "dnsmasq"; then + _hint=" + THIS LOOKS LIKE OURS. A previous Felhom install leaves the dnsmasq PACKAGE installed and its unit + enabled (only our config snippet is removed), and unconstrained it binds 0.0.0.0:53 — which is what + this gate is seeing. If this host had no dnsmasq before Felhom, clear it with: + systemctl disable --now dnsmasq + Then re-run this installer. If dnsmasq is YOURS, leave it and use one of the two routes above." + fi die "a resolver is already bound to :53 on this host — Felhom needs the guest reachable by name on your LAN. Stop or reconfigure that resolver, OR point your LAN DNS at the guest's address, then re-run. - (Felhom does NOT touch DNS services on a host it does not own — this is a refusal, not a change.)" + (Felhom does NOT touch DNS services on a host it does not own — this is a refusal, not a change.)${_hint}" fi log_info " host DNS (:53): free" else