v0.127.0: a mount Felhom itself made is not 'something else' (R-220)
gates / gates (push) Successful in 8s

After a rebuild the customer's own drives could not be re-attached: candidates
returned initialize:[] attach:[] while both drives sat there, and the deploy
refused with 'choose an attached drive from the list' — a list that was empty.
Measured live three times.

Mechanism: enrolment mounts a drive TWICE, at /mnt/felhom-drives/<name> and at
the raw /mnt/<name> it creates on the host. The host survives a guest rebuild;
the controller's registry does not. So classifyClaim saw a mount outside the
managed prefix and concluded 'claimed by something else' — about our own mount.

The fix is CORROBORATED, not a widened prefix: a non-managed mountpoint is
forgiven only when the SAME device is also mounted under the managed path, a
pairing only our enrolment produces. A disk another system uses — /srv/data,
/media/x, even /mnt/someone-elses-disk — has no counterpart and is STILL
refused, with its own test and a red-proof showing an over-wide fix offering it
for formatting.

Read from /proc/mounts deliberately: the lsblk invocation is pinned verbatim in
configs/felhom-agent.sudoers, so using the plural MOUNTPOINTS would have coupled
this to a sudoers rollout. /proc/mounts is world-readable — no sudo, no new
allowlisted command, no config change.

Fail-safe: an unreadable mount table corroborates NOTHING, so the device
classifies exactly as before. 'Could not corroborate' must never read as 'ours'.

29 packages ok, vet clean, agent gates OK.
This commit is contained in:
2026-08-06 12:55:28 +02:00
parent aa74294a7d
commit 703db166e7
5 changed files with 251 additions and 38 deletions
+33
View File
@@ -1,3 +1,36 @@
## v0.127.0 — a mount Felhom itself made is not "something else" (2026-08-06, R-220)
**After a rebuild the customer's own drives could not be re-attached, and the refusal named an action
they could not perform.** `GET /api/disks/candidates` returned `initialize: [], attach: []` while both
drives sat there, and the deploy refused with *"choose an attached drive from the list"* — a list that
was empty. Measured live **three times**: CAMPAIGN-11 Phase 1, and twice on the R-201 re-walk.
**The mechanism.** Enrolment mounts a drive **twice** — at the managed `/mnt/felhom-drives/<name>` and
at the raw `/mnt/<name>` it creates on the host. **The host survives a guest rebuild; the controller's
registry does not.** So `classifyClaim` saw a mount outside the managed prefix and correctly concluded
"claimed by something else" — about Felhom's own mount.
**The fix is CORROBORATED, not a widened prefix.** A mountpoint outside `/mnt/felhom-drives` is
forgiven **only when the same device is ALSO mounted under the managed path** — a pairing that only
Felhom's own enrolment produces. A disk another system is using, at `/srv/data` or `/media/x` or even
`/mnt/someone-elses-disk`, has no such counterpart and **is still refused**. That fence has its own
test, and its red-proof shows an over-wide fix offering `/mnt/someone-elses-disk` for formatting.
**Read from `/proc/mounts`, deliberately.** The lsblk invocation is pinned **verbatim** in
`configs/felhom-agent.sudoers` (`lsblk -J -o NAME,FSTYPE,PTTYPE,MOUNTPOINT /dev/*`), so switching it to
the plural `MOUNTPOINTS` would have meant shipping a sudoers change with the binary — a far larger
blast radius than this finding warrants. `/proc/mounts` is world-readable: **no sudo, no new allowlisted
command, no config change.**
**Fail-safe:** an unreadable mount table corroborates **nothing**, so the device classifies exactly as
it did before this change — refused. "We could not corroborate" must never read as "it is ours".
Tests: `claim_r220_test.go` — the own-drive case, the foreign-mount fence over four paths, and the
corroboration itself (both mounts required; a lone raw mount vouches for nothing; another device's
managed mount does not vouch for this one; an unreadable table corroborates nothing).
**Red-proofs:** removing the exemption refuses the customer's own drive again
(*"device is mounted at /mnt/adatok (sdb)"*); over-widening it to any `/mnt/*` path breaks the fence.
## docs — CLAUDE.md becomes a core plus path-scoped rules (2026-08-06, R-229 leg (b)) — no version bump
**Documentation only. No Go changed, nothing built, nothing deployed.** `go build`/`vet`/`test` green