12 KiB
REPORT — R-77: endpoint-drift detection, samba protected-set gate, channel log honesty (2026-07-26)
Overwritten per the standing rule. Controller v0.173.0 live on demo-felhom 9201, demo-hp 9201
and drill-r50 9201; hub v0.74.0 live. MinAgent unchanged; felhom-agent untouched.
1. Baselines used
| Repo | start | end | version |
|---|---|---|---|
| felhom-controller | head after R-75 (c7a3a90) |
9056f01 |
v0.172.0 → v0.173.0 |
| felhom.eu | 9e94479 |
9cfa619 |
hub v0.73.1 live → v0.74.0 |
| felhom-agent | dfd5d731 |
untouched | v0.96.0 |
| app-catalog-felhom.eu | 4252121 |
untouched | — |
2. Part 0 — operational repair (supervised, done first)
Its own STOP condition — "if fingerprint or token also disagree, STOP" — did not trigger:
only the address had moved on either box.
| demo-felhom 9201 | demo-hp 9201 | |
|---|---|---|
| endpoint BEFORE | 192.168.0.162:8443 |
192.168.0.87:8443 |
| endpoint AFTER | 169.254.253.1:8443 |
169.254.253.1:8443 |
| fingerprint agrees | true | true |
| token agrees | true | true |
| backup | /var/lib/docker/volumes/felhom-controller-data/_data/controller.yaml.pre-r77.bak |
same path |
[channel] lines in 90 s after restart |
0 | 0 |
hub agent_channel_* events since |
0 | 0 |
Values were redacted at the point of collection — the probe compared fingerprint/token and emitted
booleans plus a 12-char digest; neither value left the box.
On the verbatim agent channel recovered line the brief asked for: it does not exist, by design
Check returns early on a healthy probe — return nil // healthy first-obs / steady-up → no notify
— and NotifyRecovered fires only on a down→up transition within one process's lifetime. The
repair required a restart, so the new process's first observation is healthy and therefore silent.
There is no recovery line to quote, and reporting one would mean fabricating it.
The positive evidence instead:
- zero
[channel]lines 90 s after restart on both boxes, where the pre-repair container loggedtransient downwithin 60 s and confirmedDOWNwithin 120 s; - the dashboard banner cleared (fetched through the authed endpoint, ASCII-safe grep);
- zero new
agent_channel_*hub events since 06:45 UTC; - demo-felhom health
okcontinuously.
This is now written into the runbook, because "wait for the recovered line" would leave an operator watching an empty log forever.
3. Files changed
felhom-controller (9056f01, plus REPORT.md):
| file | change |
|---|---|
internal/bootstrap/bootstrap.go |
DetectEndpointDrift, EndpointDrift + its two message builders |
internal/bootstrap/drift_test.go |
NEW — Scenarios A/B/C/D + the secret-leak assertions |
internal/web/alerts.go |
endpointDriftAlert field, SetEndpointDriftAlert, ordered ABOVE the channel banner in GetAlerts |
internal/notify/notifier.go |
NotifyEndpointDrift + EndpointDriftDetails |
cmd/controller/main.go |
startup wiring (once, after config settles) |
internal/monitor/healthcheck.go |
EffectiveProtected gate + the corrected doc comment |
internal/monitor/effective_protected_test.go |
Scenario E; existing toggle test updated |
internal/channelhealth/checker.go |
stateUnconfirmed, orUnseeded, re-arm condition, field comment |
internal/channelhealth/checker_test.go |
Scenario F ×2 |
felhom.eu (9cfa619): hub/internal/api/handler.go (allowlist), hub/CHANGELOG.md,
manifests/hub.yaml, documentation/runbooks/RUNBOOK-local-api-endpoint-drift.md (NEW),
ROADMAP (R-77 shipped + R-78/79/80), capability-map note.
Scope addition, flagged: the hub was NOT in the brief's scope, and had to be
handleEvent validates event_type against allowedEventTypes and returns
400 Invalid event_type otherwise. Shipping local_api_endpoint_drift controller-side alone would
have produced an alert that never reaches the operator — the inert-seam class this project has hit
four times, and precisely the failure mode R-77 exists to prevent. I added the one-line allowlist
entry, bumped the hub to v0.74.0 and deployed it. Live leg 2 proves the end-to-end path.
4. Tests + the three mandatory red-proofs
951 → 959 test functions; 27 controller packages green; hub green.
go build ./... && go vet ./... && go test ./... clean in both repos.
Red-proof A — run in BOTH failure directions
(i) the pre-fix shape (v0.172.0: no detection at all):
--- FAIL: TestScenarioA_DriftDetectedAndNamed_NoWrite (0.00s)
drift_test.go:78: drift must be DETECTED — this is the exact live shape from the 2026-07-25 outage
(ii) the tempting wrong turn — a variant that detects and helpfully corrects, i.e. R-78 done prematurely:
--- FAIL: TestScenarioA_DriftDetectedAndNamed_NoWrite (0.00s)
drift_test.go:101: controller.yaml was MODIFIED — detection must never write (that is R-78)
The second is the one that matters: "an error was logged" is a hollow assertion; "and nothing was written" is the contract.
Red-proof E
--- FAIL: TestScenarioE_SambaProtectedOnlyWhenActuallyDeployed (0.00s)
effective_protected_test.go:99: (2) sharing ON, no password: samba protected = true, want false
— THE BUG: reconcileSambaAt refuses to deploy without a password — a deliberate state, not a fault
Red-proof F
--- FAIL: TestScenarioF_BornDownLogsUnseededNotUp (0.00s)
checker_test.go:308: an unseeded checker must not report state "up" after a suppressed first failure
checker_test.go:318: born-down must log "unseeded->down:unreachable"; got:
[WARN] [channel] agent channel DOWN (up->down:unreachable): dial tcp 192.168.0.87:8443: …
And the non-change half: all nine pre-existing channelhealth tests still pass, including
TestF2_BornDownNonTransient_AlertsOnce, TestF2_OldSeedSilentLogicWouldNotAlert and
TestF2_BornDownTransient_Debounced. Scenario F asserts sink call count and arguments
(reason, eventType, severity, dashboard flag, probe count), not just the log string.
One pre-existing test was deliberately changed
TestEffectiveProtectedTracksSharingToggle used SMBSettings{Enabled: true} and asserted samba WAS
watched — i.e. it encoded the bug. Updated to Enabled: true, UserSet: true with a comment saying
why, and the three-state matrix added alongside. Flagged here because "changed a passing test" always
deserves to be visible.
Gates
template_id_gate, emoji_gate, mojibake_gate, native_confirm_gate, app_row_dedup_gate,
offbox_rename_gate — rc=0. docker_run_volume_path_gate rc=1, PRE-EXISTING (R-29(a), red since
v0.129.0, in appexport/estimate.go which this change does not touch).
5. Deployed versions
demo-felhom 9201 : gitea.dooplex.hu/admin/felhom-controller:0.173.0 Up (healthy)
demo-hp 9201 : gitea.dooplex.hu/admin/felhom-controller:0.173.0 Up (healthy)
drill-r50 9201 : gitea.dooplex.hu/admin/felhom-controller:0.173.0 Up (healthy)
hub : gitea.dooplex.hu/admin/felhom-hub:0.74.0 Synced / Healthy, rolled out
6. Live legs
Leg 1 — no drift alert on the aligned production boxes (Scenario B live). Zero DRIFT lines and
zero [channel] lines on both after the v0.173.0 restart. Agreement really is silent; the check does
not cry wolf on every healthy boot.
Leg 2 — drill-r50, the untouched real drift (Scenario A live). It was left broken on purpose.
Before (controller 0.161.0):
controller.yaml sha256 BEFORE : 54892f1bc691c29b70ff9c323aab9e0b6ad758c602c533130064006e9a2af737
controller.yaml endpoint : 192.168.0.176:8443
bootstrap.json endpoint : 169.254.253.1:8443
DRIFTED : True
After deploying v0.173.0:
[ERROR] bootstrap: local_api endpoint DRIFT — /opt/docker/felhom-controller/controller.yaml says
"192.168.0.176:8443" but /etc/felhom-bootstrap/bootstrap.json says "169.254.253.1:8443"; the
controller is dialling the FORMER. Pin agrees: true. Not auto-corrected (R-78 owns the authority
ruling) — fix the intended file and restart the controller.
[INFO] Event pushed: local_api_endpoint_drift (error) — …
controller.yaml sha256 AFTER : 54892f1bc691c29b70ff9c323aab9e0b6ad758c602c533130064006e9a2af737
Checksums identical — zero write, proven on a live divergence. And hub-side, proving the allowlist end-to-end:
events: 2026-07-26 07:21:31 drill-r50 local_api_endpoint_drift error
notification_log: 2026-07-26 07:21:32 drill-r50 local_api_endpoint_drift operator sent
drill-r50 was left drifted — repairing it is the operator's call, and it is now the only live
fixture for this alert.
Leg 3 — demo-hp's false samba alarm is gone, sharing still on without a password.
[DEBUG] [monitor] Checking 4 protected containers: [traefik cloudflared felhom-controller filebrowser]
[DEBUG] [monitor] All protected containers running
[DEBUG] [monitor] Final status: ok (issues=0, warnings=0, info=5)
Hub-side the flip is unambiguous: fail on 0.172.0 at 07:15:56 → ok on 0.173.0 at 07:18:27.
Leg 4 — the other half of Scenario E, live (operator-present state change). Household password
set through the real POST /sharing/password endpoint (303):
felhom-samba Up 45 seconds
[DEBUG] [monitor] Checking 5 protected containers: [traefik cloudflared felhom-controller filebrowser felhom-samba]
[DEBUG] [monitor] Final status: ok (issues=0, warnings=0, info=5)
Samba deployed and is watched again — the fix suppresses the deliberate state, not the real one.
7. NOT yet live-validated
- A drift where the FINGERPRINT also moved. Unit-tested
(
TestDrift_FingerprintDisagreementIsSurfacedNotLeaked); no live fixture exists, and manufacturing one would mean deliberately corrupting a pin. - The drift banner rendered in a browser. The alert and event are proven; the Hungarian dashboard
line is unit/
AlertManager-level only. No browser on DooPlex. - Drift on a box where
controller.yamlis the CORRECT file. The detector is symmetric by construction, but every live case so far hasbootstrap.jsoncorrect. This is exactly the asymmetry R-78 must rule on. - A recovery-line transition (
down→upwithin one process). The repair path restarts, so it is structurally unreachable; therecoveredbranch is unit-tested only. - The samba gate through a full enable→password→disable cycle. Enable-without-password and password-set were both exercised; disabling sharing again was not.
- Whether
drill-r50's channel actually recovers once its endpoint is aligned — it was left drifted on purpose.
8. Observations — noticed, not acted on
manifests/hub.yamlwas pinned to0.73.1whilehub/CHANGELOG.mdtopped out at0.73.2— the documented "live image can lag the CHANGELOG" trap, found while bumping. v0.73.2's content is in the 0.74.0 image now, but v0.73.2 as a released artifact was never deployed. Worth a glance at what else it contained.expected_backup_missedstill fires nightly on all three customers — filed as R-80, with the arithmetic the DIAG hedged on: 7.3 days of stale backup materially exceeds the ~1.5-day channel outage, so backups were already stale ~6 days before the channel broke. R-77's repair will not resolve it, and it reaches a customer channel. It plausibly outranks this task.report.Issues/Warningsremain English on a Hungarian surface — R-79, whole-surface, deliberately not swept here.- The drift check is startup-only. Correct today (both files are read at boot and neither changes
under a running controller), but if anything ever rewrites
bootstrap.jsonlive, the check would not notice until the next restart. Named in the code comment so the assumption is visible. mergeLocalAPIreplaces the whole block, so endpoint/fingerprint/token cannot be reconciled independently today. That constrains R-78's design space and is recorded in its ROADMAP entry.- Sharing on demo-hp now has a household password and samba is running (leg 4). That is a real, deliberate state change from the brief, and it changes the box's LAN exposure — SMB is now actually served. Flagging so it is a decision, not a surprise.