Unify K1 discovery ownership and verification across enrollment paths

This commit is contained in:
DCCONSTRUCTIONS
2026-09-07 20:04:46 +03:00
parent 9c4d70b0e9
commit 762d77ef95
27 changed files with 615 additions and 191 deletions
@@ -0,0 +1,113 @@
# K1 connection-path audit and R15
## Observed failure sequence
The owner reported four failed enrollment results between 19:08 and 19:11 MSK
on 2026-09-07. The board is running the matched R14 pair: Node 0.8.11 and K1
0.1.11. The K1 service entered active state at 19:06:33 MSK with zero restarts.
This is not an old-package or service-restart explanation.
| Journal time (MSK) | Recorded failure boundary | Device Wi-Fi write |
| --- | --- | --- |
| 19:08:36 | `BleakDBusError`, `ensure_device_for_gatt → find_device_by_address → active_scan:477 → assert_reply` | Not reached |
| 19:08:51 | `NetworkProvisioningConflict`, exact capture admission at `facade.connect:11127` | Not reached |
| 19:09:51 | Same nested discovery boundary as 19:08:36 | Not reached |
| 19:10:41 | Same revoked-capture admission as 19:08:51 | Not reached |
The first and third failures occur before `BleakClient.__aenter__`, service
validation, baseline read and the journalled Wi-Fi write. The second and fourth
occur even earlier. A wrong Wi-Fi password cannot cause these four failures;
this does not prove the supplied credentials correct, because K1 never received
them in these calls. No credential or raw exception payload is copied here.
R14 held a `BleakScanner` open while its nested resolver started a second
scanner. Bleak 3.0.2 uses one BlueZ manager/D-Bus client per loop. BlueZ permits
one discovery session per client and adapter, not one per Python scanner.
See the [BlueZ Adapter API](https://github.com/bluez/bluez/blob/master/doc/org.bluez.Adapter.rst).
The retained path was absent, so these calls took that nested branch.
The Wi-Fi function then revoked the exact selected capture in `finally` simply
because no GATT baseline had completed. Discovery failure was thus incorrectly
treated as a failed GATT exchange. The UI classified only `connect-failed` as
a Bluetooth failure, leaving the form usable after `resolution-failed`.
The next explicit Apply met the already-revoked capture and produced the
misleading expired-search message. This is explicit invalidation, not a form
timeout or a Wi-Fi refusal.
The old journal formatter omitted structured `extra` fields and native D-Bus
codes. The exact native code of the historical calls was not retained. A test
using the installed Bleak manager and a synthetic D-Bus wire enforcing BlueZ's
documented single-session rule reproduces R14's exact call chain with
`org.bluez.Error.InProgress`. This is reproduction evidence, not a claim that
the missing historical code was recovered. The bounded Bluetooth service
journal contains no additional entries for this interval. Protected stores
were not bypassed to obtain more diagnostics.
## Path ownership review
| Path | Owner and final authority | Result of review |
| --- | --- | --- |
| Search / select in enrollment | UI selects one row from runtime + discovery + mode revision; selecting a row performs no physical connection | Retained; no automatic Apply or scan added |
| UI → Core Fleet → Node Go broker | One operation ID, durable delivery journal, deadline, exact node/runtime binding; lost response observes the same ID | Retained; delivery completion does not mean device connected |
| Node bridge → plugin facade | One bridge lock; plugin journal, lifecycle/dispatch gates and OS BLE arbiter | Retained; no second writer or recovery loop introduced |
| Fresh / retained / durable BLE identity | Exact address/native path, owner epoch and scan/session lineage; these identities have distinct admission purposes | Retained; age alone does not revoke a form selection |
| Native path resolution → Connect | Previously separate holder plus nested resolver | Replaced by one `selected_device_discovery` context; exact observations come from its own scanner; held through Connect and released before GATT I/O |
| Station provisioning / status read / AP activation | Different reviewed payloads and policies, same native connection lifecycle | All three use that single discovery context; one Connect, no automatic write repeat |
| Failure / cancellation cleanup | Transport lease, native callbacks and command journal have separate ownership | Removed duplicate revocation in exception and finally paths; only an attempted Connect/GATT exchange without baseline revokes a capture |
| Verify in enrollment / Verify in device detail | Facade policy, ledger lineage, exact persisted target, route, DeviceInfo and physical reconciliation | Removed the adapter-level divergence: both use the same `verification_parameters`; another selected device cannot inherit the saved target |
| Host Wi-Fi network list | Linux host network manager, read-only listing | Separate from BLE discovery; not a second K1 scanner |
| Wi-Fi response → control readiness | Same parent intent and owned DeviceInfo bootstrap; network-applied and control-ready are distinct | Retained; missing control confirmation never authorizes resending Wi-Fi |
| START / STOP / preview | Named acquisition, physical command journal, active session and persistent preview recording | Existing R13/R14 behavior retained; this change issues no physical test commands |
The simplification removes overlapping native discovery, duplicate failure
cleanup, divergent Node verification selection and duplicated password markup.
It does not combine different physical modes into a guessed fallback or erase
their side-effect/identity fences.
## Operator presentation and diagnostics
- Both direct and onboard enrollment consume `K1WifiPasswordField`, extracted
from the existing direct composition using Design Guideline `TextField`,
`IconButton` and eye icons. Visibility resets when the value is cleared.
Passwords remain absent from command journals and are cleared after submission.
- Search busy, unavailable Bluetooth, missing selected K1, and other discovery
failures have distinct shared messages. They state that Wi-Fi was not sent
only with `side_effect_status=none`. Unknown/after-write outcomes keep the
existing read-only reconciliation path.
- Legacy resolution errors are recognized as Bluetooth failures so an old
result cannot leave the same invalid form open for another Apply.
- Node failure logs now print a finite allowlist of stage, native BlueZ error
and write-attempt/confirmation booleans alongside the existing causal stack.
Exception messages, SSID, password, frame data and locals are excluded.
## Verification and delivery
The regression first failed on unmodified R14 with the same nested stack, then
passed after replacing the resolver. The test uses the real pinned Bleak BlueZ
manager/scanner and dbus-fast with a synthetic wire; no adapter is accessed.
Tests cover cached/missing paths, timeout, busy reply, cancellation, connection
failure and callback/session release for all three BLE operation kinds. Other
tests cover exact address/adapter, owner and capture changes, write boundary,
single submission, shared Verify admission, and password visibility.
The dev dependency explicitly includes the same pinned dbus-fast version so
this contract is also tested on macOS. Linux runtime dependencies are unchanged.
Detailed final check counts and artifact provenance are recorded after the
release checks. R15 is Node 0.8.12 plus K1 0.1.12 and uses the existing encrypted
onboard application credential. Installation and physical UI acceptance are
separate from synthetic checks; neither is claimed from a successful build.
### Source acceptance
- Backend connection/arbiter/journal suite: 189 passed.
- Installer and package lifecycle tests: 14 passed.
- Complete Control Station unit suite: 808 passed, including direct K1
recovery, frontend boundaries and shared password visibility.
- Node UI boundary test: 1 passed.
- Node Go enrollment tests passed.
- Control Station production build and TypeScript check passed (8.00 s).
- Ruff and Git whitespace checks passed.
The board was online and published idle acquisition before staging. The local
Docker daemon was not running; no Docker startup or parallel heavy builds were
used. The original canonical Core process remained on port 8000.
@@ -93,3 +93,12 @@ Mission Core returned HTTP 200; the updated home page rendered in the in-app
browser. The single canonical process remains on 8000, with no listener on 8765.
The native “Mission Core · K1 R14” installation window is open for owner-entered
sudo. Installation exit status and clean-cache physical acceptance are pending.
## Subsequent installation and failed acceptance
On 2026-09-07 the board reported installed Node 0.8.11 and K1 0.1.11.
The K1 service entered active state at 19:06:33 MSK with NRestarts=0.
Owner testing at 19:0819:11 failed enrollment. R14 is therefore installed but
not accepted. Its synthetic scanner test permitted nested discovery sessions
that the actual Bleak/BlueZ client does not support. See the R15 connection
audit for the exact sequence, reproduction and replacement of that mechanism.