aboutsummaryrefslogtreecommitdiff
path: root/docs/specs/2026-07-11-bt-doctor-expansion-spec.org
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-11 00:54:56 -0500
committerCraig Jennings <c@cjennings.net>2026-07-11 00:54:56 -0500
commit70d2ebcad0fbc43b1b80d4bfff3d8eddeda4a66b (patch)
treeef67e0ccd1ba8e1cb66e97b65bbc3f905a83bb7d /docs/specs/2026-07-11-bt-doctor-expansion-spec.org
parent7ac0c363c830840090ad81b5e9f07f350c6f6e57 (diff)
downloadarchsetup-70d2ebcad0fbc43b1b80d4bfff3d8eddeda4a66b.tar.gz
archsetup-70d2ebcad0fbc43b1b80d4bfff3d8eddeda4a66b.zip
docs: catalogue net + bt failure modes and draft two doctor expansion specs
I ran the audio doctor's design arc for the net and bluetooth doctors: a blind by-layer research catalogue of failure modes, a symptom-cluster triage, then a spec per doctor. The taxonomy holds ~74 network and ~55 bluetooth distinct root causes, each sourced to a forum or issue-tracker report, sorted into eight network and five bluetooth symptom clusters. The clusters are keyed to each doctor's existing probe ladder, so the fix-versus-guide boundary falls along the tiers the doctor already walks. Every entry carries a remedy class (auto, privileged, reboot-tail, or guide), reusing the audio doctor's four-class run-time privilege model. The two specs are expansions, not rewrites. The net doctor is already the most mature of the three: its classifier reaches six of the eight clusters today, so its spec adds only the control-plane cluster (a rival network manager, a masked NetworkManager, a bad keyfile) and sharper auth naming. The bt doctor's chain is structurally right but blind at both ends, so its spec adds a probe that names the firmware blob the kernel already logged and a probe that catches an adapter that power-on won't keep on across reboots. Both stay DRAFT with three open decisions each, headed for spec-review.
Diffstat (limited to 'docs/specs/2026-07-11-bt-doctor-expansion-spec.org')
-rw-r--r--docs/specs/2026-07-11-bt-doctor-expansion-spec.org184
1 files changed, 184 insertions, 0 deletions
diff --git a/docs/specs/2026-07-11-bt-doctor-expansion-spec.org b/docs/specs/2026-07-11-bt-doctor-expansion-spec.org
new file mode 100644
index 0000000..0775a18
--- /dev/null
+++ b/docs/specs/2026-07-11-bt-doctor-expansion-spec.org
@@ -0,0 +1,184 @@
+#+TITLE: Bt Doctor Expansion — name the blob, catch the boot-disable
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-11
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Bt Doctor Expansion
+:PROPERTIES:
+:ID: 3d4d61c4-e5df-44e9-b8e0-40b31452c3f7
+:END:
+- [2026-07-11 Sat @ 00:08:41 -0500] DRAFT — drafted. Extends the existing bt doctor (=~/.dotfiles/bluetooth/=, shipped) using the bluetooth half of the failure taxonomy ([[file:../design/2026-07-10-net-bt-failure-taxonomy.org][2026-07-10-net-bt-failure-taxonomy.org]]). Grounded in a read of the live engine, not memory.
+
+* Metadata
+
+| Field | Value |
+|----------+-----------------------------------------------------------------------------------|
+| Status | draft |
+|----------+-----------------------------------------------------------------------------------|
+| Owner | Craig Jennings |
+|----------+-----------------------------------------------------------------------------------|
+| Reviewer | Craig Jennings |
+|----------+-----------------------------------------------------------------------------------|
+| Related | [[file:../design/2026-07-10-net-bt-failure-taxonomy.org][net/bt failure taxonomy]] ; the cross-panel run-time-privilege and copy+close tasks |
+|----------+-----------------------------------------------------------------------------------|
+
+* Summary
+
+The bt doctor walks a clean chain — adapter → rfkill → bluetooth.service → powered → per-device → audio profile — and applies four safe repairs (unblock, power-on, service-restart, a2dp). The failure taxonomy shows the chain is structurally right but blind in two specific spots the user hits most: when there is no adapter it says "no adapter found" without naming *why* (a missing firmware blob the dmesg log already names), and it never checks whether the adapter is configured to power on at boot, so an =AutoEnable=false= laptop that is dead every login reads as a healthy powered-off adapter. This spec adds those two probes and adopts the cross-panel privilege model. It is an expansion of a working doctor.
+
+* Problem / Context
+
+The taxonomy sorted ~55 real bluetooth failure modes into five clusters keyed to the doctor's chain. Read against the live engine (=~/.dotfiles/bluetooth/src/bt/doctor.py=), the doctor already handles cluster 2 well (unblock, service-restart, power-on are its exact auto-fix tiers) and cluster 5 partially (the =a2dp= repair forces the A2DP profile for a card stuck in HSP/HFP or with no sink). Clusters 3 (pairing) and 4 (connection stability) are deliberately light — the doctor treats connecting and pairing as user intents, not health repairs, and that is a correct design choice, not a gap. The two real gaps are at the ends of the chain.
+
+** "No adapter found" doesn't say why (cluster 1)
+
+=_adapter_step= reports a missing adapter as "no Bluetooth adapter found (hardware/driver)" and stops. But the taxonomy's cluster 1 is almost entirely *firmware* faults, and the kernel already logged the specific cause: "Direct firmware load for intel/ibt-…sfi failed", "mediatek/BT_RAM_CODE_MT7961… failed", "BCM: Patch …hcd not found", "Reading QCA version information failed". The doctor has that log a =journalctl -k= read away and doesn't consult it. A user who sees "no adapter — hardware/driver" is left to search; a user who sees "the Intel firmware blob ibt-20-1-4.sfi failed to load — update linux-firmware and reboot" has the fix. This is the direct parallel to the audio doctor's cluster-1 dmesg-hint proposal, and the same read-only, bounded shape.
+
+** A boot-disabled adapter reads as merely powered-off (cluster 2)
+
+=_powered_step= checks whether the adapter is powered *now* and offers =power-on=. But =power-on= via bluetoothctl doesn't persist: on an =AutoEnable=false= machine (bluez's static default when main.conf is absent), or a TLP laptop that disables bluetooth on startup, the adapter is dead again next boot. The doctor fixes the symptom every session and never names the cause. The taxonomy's cluster 2 has a whole sub-family here — AutoEnable off, service-not-enabled-at-boot, TLP-disables-on-startup, systemd-rfkill-restores-a-stale-block — that all present as "powered off / blocked" and all need a *persistent* fix the doctor doesn't distinguish from a one-shot power-on.
+
+** Pairing and connection clusters stay light on purpose (clusters 3, 4)
+
+The doctor doesn't auto-pair or auto-connect, and the destructive re-pair is always user-confirmed. That is right. The one addition worth considering is a *stale-bond signature* — a device that fails to connect with a bond present, repeatedly — so the doctor can *offer* the re-pair with confidence instead of the user guessing. That is a candidate, held as a decision, not a v1 commitment. The rest of clusters 3/4 (physical range, USB3 noise, coexistence, connection parameters) are Guide-only and mostly out of a health doctor's reach.
+
+* Goals and Non-Goals
+
+** Goals
+
+- When there is no adapter, name the specific firmware/driver cause by reading the kernel log, and print the matching fix — instead of a generic "hardware/driver."
+- Distinguish a *persistently* disabled adapter (AutoEnable off, service not enabled at boot, TLP-disabled) from a merely powered-off one, and offer the persistent fix rather than a one-shot power-on that dies next boot.
+- Adopt the cross-panel run-time privilege model for the new root-needing repairs (editing main.conf's AutoEnable, =systemctl enable=, editing tlp.conf), under the Confirm/Arm-default floor.
+
+** Non-Goals
+
+- Auto-pairing or auto-connecting devices. Connecting and pairing are user intents; the doctor's existing stance stands.
+- Fixing the firmware itself. The dmesg-hint probe *names* the missing blob and prints the update/reboot instruction; it does not install firmware or rebuild initramfs inside a diagnose run (that is a Reboot-tail Guide the user runs).
+- The physical/coexistence connection-stability faults (USB3 noise, range, 2.4GHz coexistence, connection parameters). These are Guide-only and stay out of the health chain.
+- Silent privileged action. Editing main.conf, enabling the service, or touching tlp.conf all default to Confirm/Arm, never silent Auto.
+
+** Scope tiers
+
+- *v1:* the dmesg firmware-hint probe for the no-adapter case (cluster 1); the boot-enablement probe distinguishing AutoEnable-off / service-not-enabled / TLP-disabled from a plain power-off (cluster 2), with the persistent fix; adoption of the run-time privilege model.
+- *Out of scope:* auto-pair/auto-connect; firmware installation inside a run; the physical/coexistence connection faults.
+- *vNext:* the stale-bond re-pair-offer signature (cluster 3, pending the decision below); a connection-parameter/coexistence hint tail for a "keeps dropping" verdict (cluster 4); the bluetooth-specific audio-profile expansion beyond the current a2dp repair (codec fallback, default-sink, absolute-volume — several overlap the audio taxonomy and want coordination with the audio doctor). All logged to =todo.org=.
+
+* Design
+
+** For the user
+
+Two verdicts that today under-inform start telling the user the actual cause.
+
+When there is no adapter, the wall stops at "no adapter" no longer. It names the blob and the fix: "the MediaTek BT firmware (BT_RAM_CODE_MT7961) failed to load — update linux-firmware and reboot," pulled straight from the kernel log the doctor now reads. When the log shows a clean absence (no firmware error, genuinely no controller), it says that instead — the same fail-honest distinction the audio doctor draws between "the server lost a device" and "nothing is attached."
+
+When the adapter is powered off, the doctor asks a second question: is it *supposed* to be on at boot? If AutoEnable is off, or bluetooth.service isn't enabled, or TLP is disabling it on startup, the verdict names that — "your adapter powers off every boot because AutoEnable is false" — and FIX offers the persistent fix (set AutoEnable, enable the service) rather than a power-on that will be undone by the next reboot. A plain "it's off right now" still gets the quick power-on.
+
+** For the implementer
+
+*** The dmesg firmware-hint probe
+
+When =_adapter_step= finds no adapter, run a bounded =journalctl -k -b --no-pager= (or =dmesg=) read, scanned for the known firmware-load-failure signatures per vendor (Intel ibt-*.sfi, MediaTek BT_RAM_CODE, Realtek rtl_bt, Broadcom BCM .hcd, Qualcomm QCA version-read). A match yields a specific =no-adapter-firmware= verdict naming the blob and the Reboot-tail Guide (update linux-firmware / symlink the blob / reboot). No match yields the existing generic =no-adapter=. The read is bounded and read-only; it runs only in the no-adapter branch, so it costs nothing on a healthy adapter.
+
+*** The boot-enablement probe
+
+When =_powered_step= finds the adapter powered off (or =_rfkill_step= finds a soft-block), consult three persistence signals: bluez =AutoEnable= (parse =/etc/bluetooth/main.conf= [Policy], defaulting to the bluez static default when absent), =systemctl is-enabled bluetooth=, and whether TLP's =DEVICES_TO_DISABLE_ON_STARTUP= lists bluetooth. A powered-off adapter with a persistence fault gets a =powered-off-persistent= verdict distinct from the plain =powered-off=; its fix is the persistent one (set AutoEnable / enable the service / edit tlp.conf), Privileged/Confirm. The existing =power-on= stays for the plain case.
+
+*** The privilege model
+
+The new repairs — editing main.conf, =systemctl enable bluetooth=, editing tlp.conf — are root-needing and adopt the cross-panel run-time model: run where passwordless sudo exists, prompt on a tty, default to Confirm/Arm, never silent Auto. Same shared implementation as the net and audio doctors. The existing four auto-fix tiers (unblock, power-on, service-restart, a2dp) are unchanged — they are already user-scope or already the doctor's safe tier.
+
+* Alternatives Considered
+
+** Print a generic "check firmware" hint without reading the log
+
+- Good, because it needs no journal read.
+- Bad, because "check your firmware" is exactly the uselessly-generic verdict this fixes; the whole value is naming the specific blob the kernel already identified.
+- Rejected. The log has the answer; read it.
+
+** Make power-on always persistent (set AutoEnable on every power-on)
+
+- Good, because the adapter would then stay on across boots without a separate verdict.
+- Bad, because it conflates a deliberate boot-disable (a user or TLP choosing bluetooth-off-by-default for battery) with a fault, and silently overrides a policy the user may have set on purpose. Persistence is a distinct decision that deserves a distinct, confirmed action.
+- Rejected in favor of a separate verdict the user confirms.
+
+** Build the stale-bond re-pair signature into v1
+
+- Good, because it would let the doctor offer re-pair with confidence.
+- Bad, because re-pair is destructive (removes the bond) and the signature ("fails to connect with a bond present, repeatedly") needs care to not fire on a merely-out-of-range device; getting it wrong offers a destructive fix for a transient condition.
+- Held as an open decision, not a v1 commitment.
+
+* Decisions [0/3]
+
+** TODO The dmesg firmware-hint probe
+Context: cluster 1 is almost all firmware faults the kernel already logs, and the doctor reports a generic "hardware/driver" instead of the named blob.
+Decision (proposed): we will read the kernel log in the no-adapter branch, match the known per-vendor firmware-load-failure signatures, and emit a =no-adapter-firmware= verdict naming the blob and its Reboot-tail Guide; no match falls back to the generic verdict.
+Consequences: the most common no-adapter cause becomes self-explaining; harder — a per-vendor signature table to maintain, and the read must stay bounded and only run in the no-adapter branch so it never slows a healthy run.
+Owner: Craig. By: before Phase 1 lands.
+
+** TODO The boot-enablement probe and the persistent-vs-transient split
+Context: =power-on= doesn't persist, so an AutoEnable-off / not-enabled / TLP-disabled adapter is dead every boot and the doctor fixes only the symptom.
+Decision (proposed): we will add a persistence probe (AutoEnable, service-enabled, TLP) and a =powered-off-persistent= verdict distinct from =powered-off=, whose fix is the persistent one under the Confirm floor; the plain power-on stays for the transient case.
+Consequences: the boot-disable cause is named and fixed once instead of every session; harder — the doctor must not override a *deliberate* boot-disable, so the persistent fix is always confirmed and the verdict names the specific persistence cause rather than blanket-enabling.
+Owner: Craig. By: before Phase 2 lands.
+
+** TODO Whether the stale-bond re-pair-offer lands in v1 or vNext
+Context: a device failing to connect with a bond present, repeatedly, is a re-pair candidate — but re-pair is destructive and the signature can misfire on an out-of-range device.
+Decision (proposed): defer to vNext. v1 keeps re-pair strictly user-initiated; the signature is designed and validated before the doctor ever *offers* it.
+Consequences: no risk of the doctor offering a destructive fix for a transient condition in v1; harder — the "keeps failing to connect" case stays unnamed for now, which is the one cluster-3 gap a user might reasonably expect the doctor to catch.
+Owner: Craig. By: before Phase 1 (scope confirmation).
+
+* Implementation phases
+
+Each phase leaves the tree green and independently useful, as the existing bt phases did.
+
+** TODO Phase 0 — the two read-only probes
+Pure engine, no repair changes. The dmesg firmware-hint reader (no-adapter branch) and the boot-enablement reader (AutoEnable / service-enabled / TLP), both reporting into the diagnose report. =bt diag --json= (or the equivalent) shows the new signals. Fakes: a canned journal buffer with each vendor's signature, and injected main.conf / =systemctl is-enabled= / tlp.conf states.
+
+** TODO Phase 1 — the firmware-hint verdict + privilege model
+=diagnose= emits =no-adapter-firmware= (naming the blob, Reboot-tail Guide) when the signature matches. The run-time privilege resolution lands (shared cross-panel code). No auto-fix — this verdict is a Guide.
+
+** TODO Phase 2 — the persistent-power verdict and its fix
+=powered-off-persistent= distinct from =powered-off=; the persistent fix (set AutoEnable / enable service / tlp) registers Privileged/Confirm. =bt doctor --fix= applies it under the Confirm floor; the plain power-on path is unchanged.
+
+** TODO Phase 3 — flip this spec to IMPLEMENTED
+And log the vNext items (stale-bond signature, connection-parameter hints, bt-audio-profile expansion) to =todo.org=.
+
+* Acceptance criteria
+
+- [ ] With no adapter and a MediaTek/Intel/Realtek/Broadcom/Qualcomm firmware-load error in the kernel log, the doctor names the specific blob and prints the update-and-reboot Guide.
+- [ ] With no adapter and no firmware error in the log, the doctor reports the generic "no controller attached" without inventing a firmware cause.
+- [ ] An adapter that is powered off with AutoEnable=false reports =powered-off-persistent= and FIX offers the persistent fix, not just a one-shot power-on.
+- [ ] An adapter powered off with AutoEnable on (a transient off) still gets the quick power-on.
+- [ ] Every new root-needing repair defaults to Confirm/Arm and never runs silently as Auto.
+- [ ] The existing chain (rfkill/service/powered/device/audio) and its four auto-fix tiers classify and repair exactly as today (regression).
+
+* Readiness dimensions
+
+- *Data model & ownership* — the diagnose report gains a firmware-hint field (generated from the kernel log) and boot-persistence signals (generated from main.conf / systemctl / tlp.conf). The doctor writes main.conf / tlp.conf / the service enablement only under the Confirm floor.
+- *Errors, empty states & failure* — an unreadable journal or main.conf yields "unknown," never a false firmware or persistence verdict. A no-match firmware scan falls back to the existing generic verdict (the safe direction).
+- *Security & privacy* — device MACs are already redacted by =redact.py=; the new probes read the kernel log and config files, no secrets. The firmware-signature scan must not surface unrelated journal content.
+- *Observability* — the wall names the blob and the persistence cause. The =--json= output carries the new signals.
+- *Performance & scale* — the journal read runs only in the no-adapter branch; the persistence read only when powered-off/blocked. Neither touches a healthy run.
+- *Reuse & lost opportunities* — reuses the existing chain, =btctl=, =sysio=, =priv.py=, and the shared cross-panel privilege model rather than a bt-local one. =diagnose= stays the single report authority.
+- *Architecture fit* — both probes are additive inside existing chain branches (=_adapter_step=, =_powered_step=), not new chain links. Weak point: the firmware-signature table is vendor-specific and will drift as new hardware appears; it degrades to the generic verdict rather than mis-naming, which bounds the risk.
+- *Config surface* — none new for the doctor; it *reads* bluez/tlp config and *writes* it only as a confirmed repair. N/A for its own knobs.
+- *Documentation plan* — module docstrings, as the package does today. The wall is the user documentation.
+- *Dev tooling* — =make test= and the bt panel smoke cover it; the new probes need a canned journal fixture and injected config states, fixture shapes the package can adopt.
+- *Rollout, compatibility & rollback* — additive; the existing chain is untouched. The persistent fixes change bluez/tlp config and service enablement, so all are Confirm-tier and reversible by the user.
+- *External APIs & deps* — =journalctl -k=/=dmesg=, =/etc/bluetooth/main.conf=, =systemctl is-enabled=, and =/etc/tlp.conf= layouts are verified against the live system before Phase 0. The per-vendor firmware signatures come from the taxonomy's cluster-1 sources; no new packages.
+
+* Risks, rabbit holes, and drawbacks
+
+The firmware-signature table is the maintenance rabbit hole: each vendor logs its failure differently and new hardware adds new strings. The mitigation is that a miss degrades to the existing generic verdict — the doctor is never *worse* than today, only sometimes not-better — so the table can grow incrementally without a correctness cliff.
+
+The persistent-power fix must not fight a deliberate choice. A user (or TLP on a battery-conscious laptop) may want bluetooth off by default; blanket-enabling AutoEnable would override that silently. The verdict names the specific persistence cause and the fix is always confirmed, so the user sees exactly what would change before it does.
+
+The bt-audio-profile expansion (vNext) overlaps the audio taxonomy's Bluetooth-mic cluster. When it is picked up, it needs coordination with the audio doctor so the two panels don't both claim the same A2DP/HFP diagnosis with divergent verdicts. Named here so the seam is known.
+
+* Review and iteration history
+
+** 2026-07-11 Sat @ 00:08:41 -0500 — Craig Jennings — Author
+- What: drafted the bt doctor expansion from the bluetooth half of the failure taxonomy. v1 adds the dmesg firmware-hint probe (cluster 1) and the boot-enablement probe (cluster 2); the pairing/connection/audio expansions are staged to vNext.
+- Why: the taxonomy showed the doctor's chain is structurally right but blind at its ends — a no-adapter verdict that won't name the firmware blob the kernel already logged, and a power-on that doesn't persist across boots.
+- Artifacts: [[file:../design/2026-07-10-net-bt-failure-taxonomy.org][the net/bt failure taxonomy]]; code read across =~/.dotfiles/bluetooth/src/bt/= (=doctor.py=, =repair.py=, =btctl.py=, =sysio.py=, =audio.py=, =priv.py=).