wip(k1): checkpoint connection recovery rewrite
Capture the current unreleased K1 connection, recovery, lifecycle, viewer, and test work as a single known-bad baseline for subsequent fixes.
This commit is contained in:
@@ -136,7 +136,7 @@ The current XGRIDS contribution maps its proven internal workflow into those
|
||||
platform states without changing the wire protocol:
|
||||
|
||||
```text
|
||||
confirm power -> scan BLE -> select candidate -> enter Wi-Fi
|
||||
scan BLE -> select candidate -> enter Wi-Fi
|
||||
-> provision once -> receive LAN address -> start source
|
||||
-> wait for first point frame -> streaming
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ADR 0013: explicit K1 local connection matrix
|
||||
|
||||
- Status: amended 2026-07-20; Bridge is the product path, Quick Connect retained as a prepared-host laboratory path
|
||||
- Status: amended 2026-08-08; Bridge is the product path, Quick Connect retained as a prepared-host laboratory path
|
||||
- Date: 2026-07-19
|
||||
- Extends: ADR 0004, ADR 0005 and ADR 0012
|
||||
|
||||
@@ -47,6 +47,23 @@ for the previous session. An active acquisition is never retargeted in place.
|
||||
The later correlated MQTT `DeviceInfo` response supplies model, firmware,
|
||||
serial and vendor identity; IP equality alone cannot identify a K1.
|
||||
|
||||
The 2026-08-08 physical Bridge trace corrected the earlier field model: the
|
||||
first `7f02` text slot is `WIFI_AP` in AP state but contains the joined network
|
||||
name in FW 3.0.2 station state. Mission Core therefore normalizes that station
|
||||
response to `WIFI_CLIENT` internally and admits Bridge/Direct only when the raw
|
||||
post-write name exactly matches the current explicit request plus a valid
|
||||
non-AP private address. The raw name is not persisted in the secret-free
|
||||
network audit or published through API state.
|
||||
|
||||
The limitation still applies when recording an interrupted attempt that has no
|
||||
exact post-write network-name observation. A changed private DHCP address alone
|
||||
cannot identify the selected network. An already AP-ready baseline likewise
|
||||
cannot prove the outcome of an interrupted Quick-to-Quick attempt. Mission Core
|
||||
therefore records that attempt as terminal `outcome-unknown` and never replays it automatically. The
|
||||
historical uncertainty is not a permanent barrier: after the old active
|
||||
operation and cleanup have ended, a later explicit operator scan, selection,
|
||||
and connect is a distinct session with its own one reviewed write.
|
||||
|
||||
Product decision on 2026-07-20: Bridge/direct-LAN is the continuing route.
|
||||
Quick Connect remains visible and executable on an already prepared host, but
|
||||
is not a deployment dependency or portability claim.
|
||||
@@ -86,12 +103,65 @@ and the following cold Swift/CoreWLAN process missed the beacon. The corrected
|
||||
implementation holds the selected `BleakClient` open through bounded native
|
||||
SSID discovery and the single association call.
|
||||
|
||||
BLE discovery and the selected device action form one host session. A physical
|
||||
run proved that immediately rediscovering the same K1 by its CoreBluetooth UUID
|
||||
can fail even though the preceding scan exposed it. Mission Core retains the
|
||||
non-serializable `BLEDevice` handle process-locally and uses that exact handle
|
||||
for the next selected network action; it never exposes the handle through API
|
||||
state or treats the macOS UUID as durable device identity.
|
||||
One explicit six-second BLE discovery and the later Apply action form one
|
||||
operator intent without a second discovery. A physical run proved that
|
||||
immediately rediscovering the same K1 by its CoreBluetooth UUID can fail even
|
||||
though the preceding scan exposed it. Mission Core retains the non-serializable
|
||||
`BLEDevice` handle process-locally and uses that exact handle for Apply; it never
|
||||
exposes the handle through API state or treats the macOS UUID as durable device
|
||||
identity. UI row selection itself performs no GATT or backend I/O.
|
||||
|
||||
The operator-visible candidate list, local selected draft and admitted active
|
||||
session are separate contracts. Every explicit scan replaces the candidate
|
||||
set. Selection only binds a local form to one result from the latest admitted
|
||||
generation. Wall-clock age does not remove that generation while the operator
|
||||
completes the form. Apply admits only its exact retained handle and live GATT
|
||||
validation may create the active session; a remembered UUID is never mutation
|
||||
authority. Proven disconnect, explicit stop, app/backend restart, another
|
||||
explicit Scan, or a committed mode transition revokes the applicable candidate
|
||||
or live session. Rediscovery never auto-connects.
|
||||
|
||||
Quick Connect to Bridge is an explicit topology transition, not another scan
|
||||
heuristic. Selecting Bridge — or choosing another K1 while Bridge is already
|
||||
selected — sends one idempotent local `reset_scenario` CAS. It seals retained
|
||||
receiver/camera/control ownership, invalidates candidates and credentials and
|
||||
retires old physical lineage truthfully, while sending no device command, BLE,
|
||||
host-network write or automatic Scan. The next explicit Scan starts the clean
|
||||
discovery flow, while Apply remains the topology and device-mutation boundary.
|
||||
The operator selects one discovered K1, sees the Bridge credentials immediately
|
||||
and submits once. The new GATT
|
||||
session reads internal baseline `7f02` and emits exactly one reviewed 99-byte
|
||||
station write. A connect failure ends that attempt. An ambiguous post-write
|
||||
failure is terminal `outcome-unknown` audit, not a permanent cross-session
|
||||
fence. There is no automatic BLE or network-write retry.
|
||||
|
||||
Likewise, an exact REST `network_applied` result with unready/unknown control
|
||||
spends that Apply and its credentials without making the read-model attempt a
|
||||
permanent topology lock. Recommended Verify is pinned to the backend
|
||||
current/configured target. A separately explicit new intent still requires
|
||||
current server policy: Bridge prepares `select-device` before a later fresh
|
||||
scan; Quick and Direct run an admitted fresh scan, select only its latest row
|
||||
and create a new idempotency Apply; a mode or same-mode new-device transition
|
||||
uses one idempotent local-only `reset_scenario` before that fresh scan. None of
|
||||
these new-intent UI paths reuses the old
|
||||
intent or runs as a hidden frontend/mutating continuation. The service-owned
|
||||
same-intent read-only bootstrap declared below is the sole post-ACK exception.
|
||||
|
||||
The Apply REST call returns as soon as the exact durable
|
||||
`network_applied` proof is available. The service may continue the same
|
||||
intent's supervised control bootstrap read-only after that ACK. This performs
|
||||
no BLE/host mutation or retry and creates no frontend Verify, Scan, Apply or
|
||||
blocking Apply loader. While the exact child is accepted/running, the UI may
|
||||
show only a passive **Сеть настроена · подтверждаем управление** indicator and
|
||||
must keep every recovery action disabled. Later control state arrives only as
|
||||
backend presentation convergence; terminal unready/unknown state then exposes
|
||||
the explicit server-policy recovery choices.
|
||||
|
||||
Bridge and Quick Connect were physically accepted as separate paths before
|
||||
this amendment. The combined Quick Connect to Bridge transition has automated
|
||||
contract coverage but remains a distinct physical acceptance gate; it must not
|
||||
be reported as field-accepted until one redacted live run records both sides of
|
||||
the transition.
|
||||
|
||||
The corrected host boundary derives a non-secret, device-scoped profile ID from
|
||||
the selected SSID. The reviewed client contains per-device `WiFiAP_SSID` and
|
||||
@@ -110,13 +180,40 @@ that opaque source before any BLE write. A missing provider fails closed. The
|
||||
browser, API, argv, logs, manifests and evidence never receive the secret; the
|
||||
importer's short-lived mutable buffer is zeroized after the Keychain handoff.
|
||||
|
||||
The 2026-08-06 field regression established that process identity is part of
|
||||
this prepared-host contract. Runtime `swiftc` compilation produced an ad-hoc
|
||||
helper with an unstable designated identity. macOS then requested Keychain
|
||||
authorization repeatedly and the same process context failed to expose the
|
||||
exact K1 SSID through CoreWLAN even after K1 had acknowledged AP-ready. That
|
||||
runtime-compiled route is rejected. The laboratory adapter uses the previously
|
||||
physically accepted Apple-signed interpreter path,
|
||||
`/usr/bin/xcrun swift <reviewed-source>`, and validates the source path before
|
||||
launch. A portable product implementation still requires a packaged,
|
||||
precompiled and properly signed helper with a stable bundle identifier,
|
||||
designated requirement, Location/CoreWLAN authorization and Keychain ACL; the
|
||||
current prepared-host path does not claim that packaging work is complete.
|
||||
|
||||
Before any BLE write, the helper's preflight is non-interactive. It first checks
|
||||
Keychain item existence through metadata, then validates the selected profile's
|
||||
SSID and `exact-firmware-profile` provenance inside the helper without returning
|
||||
secret data. Provider material is also read with interaction disabled if a
|
||||
missing device profile must be materialized. The association phase accepts only
|
||||
that already materialized exact profile. It never
|
||||
falls back to the system Wi-Fi Keychain, rewrites a profile opportunistically,
|
||||
or opens a password/authorization dialog after K1 has changed network state.
|
||||
An unavailable or unauthorized profile therefore fails closed with a precise
|
||||
reason code and no automatic device retry.
|
||||
|
||||
The host-network boundary, rather than the XGRIDS frontend, owns platform
|
||||
association. Browsers expose no Wi-Fi join API, and Apple's iOS
|
||||
`NEHotspotConfiguration` consent flow is unavailable on macOS. The current
|
||||
implementation therefore uses a short-lived Swift/CoreWLAN + macOS Keychain
|
||||
helper; Windows Credential Manager and Linux Secret Service adapters remain
|
||||
separate platform work. The helper performs repeated read-only exact-SSID scans
|
||||
inside one 15-second discovery window and at most one association. It never
|
||||
inside one 30-second discovery window and at most one association. The larger
|
||||
window covers the physically observed 18.142-second beacon-discovery case;
|
||||
AP-ready confirms K1 state but does not prove that macOS has already observed
|
||||
the RF beacon. It never
|
||||
repeats the BLE command, guesses a password or treats `7f01` as a credential-read
|
||||
command. The credential-bearing 99-byte station-provisioning frame and fixed
|
||||
100-byte AP-enable frame are separate reviewed payloads.
|
||||
@@ -128,12 +225,13 @@ The owner also observed no explicit device/account pairing in the normal
|
||||
LixelGo onboarding flow; this is consistent with a firmware-defined AP secret,
|
||||
but does not establish account-wide authorization for arbitrary scanners.
|
||||
|
||||
Connection verification refreshes the session-scoped lease with the same
|
||||
read-only BLE status operation. It does not write a characteristic, re-provision
|
||||
Apply may read BLE baseline internally while establishing a new selected
|
||||
session. Selection never does so, and the normal flow has no mandatory or hidden
|
||||
"verify without write" recovery step. The baseline read does not re-provision
|
||||
Wi-Fi, scan the subnet, change a host route, or touch VPN configuration. The
|
||||
later canonical MQTT session supplies the real data-plane connection and live
|
||||
`DeviceInfo` identity check. A BLE lease observation is therefore not by itself
|
||||
a claim that MQTT/RTSP is reachable.
|
||||
later canonical MQTT
|
||||
session supplies the real data-plane connection and live `DeviceInfo` identity
|
||||
check; BLE status alone is not a claim that MQTT/RTSP is reachable.
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -148,6 +246,10 @@ a claim that MQTT/RTSP is reachable.
|
||||
therefore not scheduled for this Quick Connect path.
|
||||
- Direct Connect requires an already-running hotspot and a controller route;
|
||||
Mission Core does not create or manage that hotspot.
|
||||
- Discovery never auto-connects devices. Mode, selection and input are local
|
||||
only. App restart, disconnect, explicit stop and mode transition require a
|
||||
fresh explicit scan-select-Apply session. Apply performs no hidden rescan or
|
||||
Verify and may cross at most one device-mutation boundary.
|
||||
- The application-control, START/STOP and raw-first acquisition protocol is
|
||||
unchanged after a target address is admitted.
|
||||
- Direct Connect remains explicitly pending one owner-operated physical
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
# ADR 0014: long-lived macOS host-association observer
|
||||
|
||||
Status: planned production boundary; software contract may be developed behind
|
||||
a disabled feature flag.
|
||||
|
||||
Related acceptance item: `CONN-66` in
|
||||
[`../20_K1_CONNECTION_SUPERVISION_CANON.md`](../20_K1_CONNECTION_SUPERVISION_CANON.md).
|
||||
|
||||
## Context
|
||||
|
||||
Mission Core must distinguish a K1 that is configured for a network from a Mac
|
||||
that is currently attached to the same network. Route, TCP, DeviceInfo,
|
||||
control and data evidence are bound to a host-path epoch; a Wi-Fi switch,
|
||||
sleep/wake cycle or observer restart must invalidate that epoch before any late
|
||||
TCP/MQTT result can restore command authority.
|
||||
|
||||
The current laboratory implementation is fail-closed but not a production
|
||||
observer. One normal connection-monitor poll samples the host path before and
|
||||
after its TCP probe. Each sample synchronously invokes:
|
||||
|
||||
```text
|
||||
/usr/bin/xcrun swift plugins/xgrids-k1/macos/associate_wifi.swift
|
||||
```
|
||||
|
||||
under one process-local lock with a 30-second timeout. At the one-second
|
||||
monitor interval this can launch two Swift processes per second. A failed
|
||||
cycle can occupy the lock for roughly sixty seconds, and cancellation of the
|
||||
Python `asyncio.to_thread()` waiter does not terminate the native process or
|
||||
thread. Physical-command validation shares this observation path. Shorter
|
||||
timeouts, cached shell output or automatic fallback would hide rather than
|
||||
remove the lifecycle defect.
|
||||
|
||||
## Decision
|
||||
|
||||
Production host-association evidence will come from one signed, long-lived,
|
||||
read-only agent in the user's macOS login session.
|
||||
|
||||
- The agent owns one `CWWiFiClient` for its process lifetime.
|
||||
- It observes CoreWLAN link/association/power events and macOS sleep/wake.
|
||||
- It never scans BLE, changes Wi-Fi, reads K1 credentials, reconnects MQTT or
|
||||
sends START/STOP.
|
||||
- It is packaged in a minimal container app and registered with `SMAppService`;
|
||||
it is not a `LaunchDaemon` and is not launched through `xcrun` at runtime.
|
||||
- The required Wi-Fi event entitlement and Location authorization are checked
|
||||
before K1 network mutation is offered. Missing authorization produces
|
||||
explicit unavailable evidence, not a crash loop or guessed association.
|
||||
- The existing Wi-Fi mutator remains a separate component under the exclusive
|
||||
network process lease. Observer authority and mutation authority are never
|
||||
combined.
|
||||
|
||||
The backend communicates with the observer through bounded local IPC. Each
|
||||
backend session supplies a random HMAC key. SSID and BSSID remain inside the
|
||||
agent; only an opaque continuity token is returned and it cannot be correlated
|
||||
between backend processes. The token material is interface plus BSSID; SSID is
|
||||
used only to report evidence quality. This keeps one AP identity stable when
|
||||
macOS alternates between `ssid+bssid` and `bssid-only` disclosure.
|
||||
|
||||
## Observer contract
|
||||
|
||||
```text
|
||||
schema_version: missioncore.macos-host-association/v2
|
||||
agent_instance_id: random 128-bit process instance
|
||||
sequence: uint64
|
||||
association_epoch: uint64
|
||||
interface_name: string | null
|
||||
wifi_interface: true | false | null
|
||||
state: associated | not-associated | inactive | not-wifi | unavailable
|
||||
evidence_quality: ssid+bssid | bssid-only | not-wifi | unavailable
|
||||
continuity_token: 64 lowercase hex | null
|
||||
reason_code: string | null
|
||||
observed_monotonic_ns: uint64
|
||||
sample_age_ms: uint32
|
||||
cause: initial | link-change | association-change | power-change |
|
||||
permission-change | will-sleep | did-wake | poll-correction |
|
||||
observer-restart
|
||||
```
|
||||
|
||||
`sequence` changes for every event or heartbeat. `association_epoch` changes
|
||||
when interface, power, state, SSID or BSSID changes. Sleep and wake each create
|
||||
a barrier even if the visible network looks unchanged afterward. A new agent
|
||||
instance, IPC reconnect, sequence rollback/gap, malformed frame or timeout is
|
||||
also a discontinuity.
|
||||
|
||||
The backend adds its own `observer_session_epoch`; the effective host-route
|
||||
fingerprint includes the agent instance, observer session, association epoch
|
||||
and opaque token. A response from an old session or sequence is discarded.
|
||||
Unknown schema/state or incomplete evidence is `unavailable` and immediately
|
||||
revokes host authority.
|
||||
|
||||
## Timing and failure semantics
|
||||
|
||||
- Heartbeat: 1 second.
|
||||
- Maximum cached-snapshot age: 750 ms.
|
||||
- Snapshot RPC deadline: 250 ms.
|
||||
- Initial handshake deadline: 2 seconds.
|
||||
- Two missed heartbeats or one invalid IPC frame revoke authority immediately.
|
||||
- Reconnect backoff: 250 ms, 500 ms, 1 s, 2 s, then at most 5 s.
|
||||
- There is no automatic fallback to the Swift source runner.
|
||||
- Agent loss affects only read-only host evidence. It never triggers a K1
|
||||
network write, MQTT reconnect or physical command.
|
||||
- A discontinuity first marks the supervisor host path unavailable and rotates
|
||||
its epoch. Recovery then requires fresh route, TCP and DeviceInfo/control
|
||||
evidence in that order.
|
||||
|
||||
## Delivery phases
|
||||
|
||||
Phase A is safe without signing or a physical K1:
|
||||
|
||||
1. Define the Python observer protocol and validate the v2 schema.
|
||||
2. Add a fake/in-memory transport and backend session/sequence validator.
|
||||
3. Implement immediate epoch invalidation and bounded cached lookup.
|
||||
4. Inject the observer into the monitor behind a disabled feature flag.
|
||||
5. Implement the Swift reducer and local transport as a testable Swift package.
|
||||
6. Test sleep/wake, timeout, event gaps, delayed replies, crash/restart and
|
||||
manual network changes.
|
||||
7. Expose secret-free observer health and next action to the UI.
|
||||
8. Prove 10,000 samples launch no child process and cause no lock starvation.
|
||||
|
||||
Phase B requires the actual Mac signing and permission environment:
|
||||
|
||||
1. Package and register the user-session agent.
|
||||
2. Obtain the Wi-Fi events entitlement and complete Location onboarding.
|
||||
3. Run the observer in shadow mode beside the current fail-closed probe.
|
||||
4. Cut over only after the physical fault matrix and an eight-hour soak show no
|
||||
unexplained divergence.
|
||||
|
||||
## Acceptance gate
|
||||
|
||||
- No `xcrun`, `swift` or `swiftc` occurs on the observer path.
|
||||
- One agent and one CoreWLAN client serve one login session.
|
||||
- Snapshot p99 is below 50 ms, hard deadline 250 ms, monitor-cycle p99 below
|
||||
1.5 seconds.
|
||||
- No mutex is held across native or IPC calls.
|
||||
- Sleep, wake, agent restart, sequence gap and timeout always invalidate the
|
||||
effective host epoch.
|
||||
- Late TCP/DeviceInfo evidence from an old epoch is rejected.
|
||||
- SSID, BSSID and credentials never enter IPC logs, API state or artifacts.
|
||||
- Quick-to-Bridge, Bridge-to-Quick, manual Wi-Fi switch, Wi-Fi off/on,
|
||||
router loss/return with the same SSID/IP, backend restart and Location denial
|
||||
all revoke control authority within two seconds and recover only through
|
||||
fresh route, TCP and DeviceInfo evidence.
|
||||
|
||||
Until this gate passes, the current association probe remains explicitly a
|
||||
laboratory implementation and `CONN-66` remains open.
|
||||
|
||||
## Laboratory containment while Location evidence is hidden
|
||||
|
||||
The source-runner helper can return `association-identity-unavailable` on a
|
||||
connected Mac when macOS privacy rules hide SSID and BSSID from the CLI child
|
||||
process. Rotating a random fallback token on every one-second poll made a
|
||||
stable route and a successful TCP probe mutually impossible: every following
|
||||
sample revoked the preceding endpoint result as a fictitious network switch.
|
||||
|
||||
Until the signed observer above replaces the source runner, the laboratory
|
||||
probe uses one random, process-scoped token for the same interface and
|
||||
unavailable-evidence scope. This is not promoted to association evidence:
|
||||
|
||||
- the public evidence quality remains `unavailable`;
|
||||
- interface, source address, kernel route, availability, a proven different
|
||||
BSSID and process restart remain epoch barriers;
|
||||
- endpoint reachability alone remains `configured-unverified`;
|
||||
- only fresh exact DeviceInfo/control evidence can grant control authority;
|
||||
- `CONN-66`, sleep/wake and same-subnet network-switch acceptance remain open.
|
||||
|
||||
For an already reachable lease whose exact DeviceInfo identity and control
|
||||
session remain healthy, a temporary helper timeout or privacy-limited
|
||||
association sample may retain the preceding proven association fingerprint
|
||||
only while the kernel route fingerprint, interface, source, intent and target
|
||||
are unchanged. That retained sample still performs TCP contact and a second
|
||||
kernel-route check, refreshing only route/TCP observation TTLs. Endpoint loss,
|
||||
control loss, control-proof expiry, target/intent change, a proven association
|
||||
identity change or any raw route change revokes immediately. A
|
||||
`configured-unverified` path does not receive this bridge and remains bounded
|
||||
by the existing technical-failure debounce and transport TTL.
|
||||
|
||||
This containment removes the false per-poll epoch churn observed on the field
|
||||
Mac without claiming that the planned production observer has been delivered.
|
||||
@@ -0,0 +1,309 @@
|
||||
# ADR 0015: explicit K1 recovery beside the one-intent connection flow
|
||||
|
||||
Status: accepted product, recovery and presentation contract; executable
|
||||
coverage and remaining hardware acceptance are tracked in
|
||||
`docs/k1-connection-acceptance.manifest.json`.
|
||||
|
||||
Related acceptance items: `CONN-16` through `CONN-19`, `CONN-28`, `CONN-29`,
|
||||
`CONN-65`, and `CONN-68` through `CONN-78` in
|
||||
[`../20_K1_CONNECTION_SUPERVISION_CANON.md`](../20_K1_CONNECTION_SUPERVISION_CANON.md).
|
||||
|
||||
## Problem
|
||||
|
||||
Loss of K1 power, the router, Mac Wi-Fi, MQTT control or the backend does not
|
||||
prove whether K1 is physically scanning. Retained points, an open TCP port and a
|
||||
historical START are insufficient. Replaying START or STOP after an ambiguous
|
||||
dispatch boundary can create a second physical edge.
|
||||
|
||||
The durable physical-command ledger, exact read-only classification and
|
||||
fail-closed supervisor must remain. They must not make ordinary connection slow
|
||||
or surprising. In particular, selecting a device must not secretly connect,
|
||||
Verify, retire/reopen history or delay network credentials.
|
||||
|
||||
## Decision
|
||||
|
||||
### Existing product surface
|
||||
|
||||
K1 connection stays in the existing device plugin section headed
|
||||
**Подключение XGRIDS LixelKity K1**. The surrounding job, entity and lifecycle
|
||||
models do not change. This is novelty A: an improvement to an existing product
|
||||
surface. A separate wizard, modal flow and mandatory preflight/recovery surface
|
||||
are rejected.
|
||||
|
||||
The section reuses canonical shared `Button`, `TextField`, `ActivityIndicator`
|
||||
and `StatusBadge`. It creates no shared entity and uses no raw local HTML
|
||||
controls or literal local status colors.
|
||||
|
||||
### One-intent normal flow
|
||||
|
||||
The normal flow is:
|
||||
|
||||
1. choose Bridge, Direct Connect or Quick Connect locally;
|
||||
2. press the explicit Bluetooth search action;
|
||||
3. wait for exactly one six-second discovery;
|
||||
4. press **Выбрать** on one result;
|
||||
5. enter Bridge/Direct credentials immediately, or review the Quick Connect
|
||||
summary;
|
||||
6. press **Применить** once.
|
||||
|
||||
Opening the section, changing mode, selecting a row and every
|
||||
SSID/password keystroke perform zero browser-controller, device or host I/O.
|
||||
They create no backend operation and show no operation loader. An admitted fresh
|
||||
selection retains the selected card and exposes applicable inputs immediately.
|
||||
A candidate without current draft authority is omitted or presented only as
|
||||
non-actionable evidence; it never receives a misleading disabled primary.
|
||||
|
||||
Each explicit search owns exactly one bounded six-second discovery. It performs
|
||||
no connect, Verify, selection or mutation. Results are never auto-selected.
|
||||
|
||||
One Apply owns the normal connection intent. It may commit the local desired-mode
|
||||
draft under backend CAS and may cross at most one reviewed K1 mutation boundary.
|
||||
Its frontend handler performs no hidden Scan, Verify, reconnect, retirement,
|
||||
reopen, candidate substitution or retry. Quick, Bridge and Direct use the same single primary
|
||||
**Применить** action; credentials are required only for Bridge and Direct.
|
||||
|
||||
Ordinary Bridge Apply never opts into changing the controlling Mac's Wi-Fi
|
||||
association. Host switching is a separate future consequential operator action,
|
||||
not an Apply substep. K1 provisioning can therefore succeed as
|
||||
`network_applied` while control is `control_not_ready`. That result must not
|
||||
repeat that intent's BLE write. Recommended separately explicit read-only
|
||||
Verify/recovery may establish route, endpoint and DeviceInfo/control evidence
|
||||
for the applied topology; a new intent remains separately policy-gated.
|
||||
|
||||
The exact REST response owns completion of the Apply mutation. A snapshot with
|
||||
`connection_attempt.phase=network_applied` is accepted immediately when
|
||||
`control_state` is `control_not_ready` or `unknown`; the controller does not
|
||||
wait for WebSocket/poll convergence or call the full connection-ready
|
||||
requirement. The service may continue supervised same-intent control bootstrap
|
||||
after this fast durable ACK, but only read-only: no BLE/host mutation, mutation
|
||||
retry, new UI action or second Apply. This is not a hidden frontend Scan or
|
||||
Verify. Exact connection-ready remains mandatory before control or physical
|
||||
START. This separation spends the old intent before a delayed state channel
|
||||
could invite its duplicate replay. `connection_attempt` is a read model, not
|
||||
permanent lifecycle authority; current server policy may admit a separately
|
||||
explicit new intent.
|
||||
|
||||
While the exact service-owned bootstrap child is `accepted` or `running` and
|
||||
projects `safe_next_action=wait-for-current-attempt`, the UI shows only one
|
||||
passive **Сеть настроена · подтверждаем управление** indicator. It enables no
|
||||
Verify, mode change, Scan, row or Apply action. Terminal unready/unknown child
|
||||
state then exposes the separately explicit policy-gated recovery choices.
|
||||
|
||||
`network_applied` plus unready or unknown control spends the old Apply and gates
|
||||
ordinary mode change, Scan, row selection and Apply. It first waits passively
|
||||
for an exact active service child; after terminal settlement it presents an
|
||||
explicit recovery choice, regardless of browser-local mode. It never authorizes
|
||||
automatic or same-intent replay. Recommended Verify is pinned to the backend
|
||||
`serverBound` current/configured transport and mode; it never falls back to a
|
||||
selected browser row and is not a prerequisite for every new intent.
|
||||
|
||||
Current server policy may admit a distinct, explicit new-intent path. Bridge
|
||||
uses `prepare-select-device`, a local-only CAS with zero device/host I/O; only
|
||||
after its success may the operator initiate a fresh six-second Scan. Quick and
|
||||
Direct use explicit policy-gated `scan-ble`, then the latest fresh row and a new
|
||||
idempotency Apply. A mode change requires backend `mode_selection` authority and
|
||||
then a fresh explicit Scan. No recovery choice performs hidden Scan, selection,
|
||||
Verify, provisioning or continuation of the old Apply, and the browser never
|
||||
manufactures authority.
|
||||
|
||||
### Freshness and outcome semantics
|
||||
|
||||
Apply is admitted only for the exact selected transport, completed discovery
|
||||
generation, backend runtime, desired-mode revision, reconfiguration intent and
|
||||
policy snapshot. Authority drift before dispatch is a terminal, zero-device-I/O
|
||||
`stale` result. The UI keeps the result understandable, labels it explicitly and
|
||||
offers a new explicit six-second search. It never starts that search itself.
|
||||
|
||||
A failure before the reviewed mutation boundary is `not-dispatched` or
|
||||
`failed`, with zero K1 mutation. A lost response, timeout, power failure or
|
||||
process death after dispatch is `outcome-unknown`, with
|
||||
`safe_to_retry=false`. The durable network-attempt ledger prevents replay.
|
||||
Credentials are never reused automatically. A later operator Apply is a new
|
||||
intent and must pass all current gates.
|
||||
|
||||
## Physical safety remains separate
|
||||
|
||||
Network attempts are disposable; physical START/STOP ambiguity is durable:
|
||||
|
||||
- START and STOP never replay automatically;
|
||||
- control loss does not prove scanning stopped;
|
||||
- local receiver/camera/ingress cleanup is not physical STOP;
|
||||
- a wrong K1/transport/profile/project cannot reconcile the record;
|
||||
- READY records cessation without rewriting historical command outcome;
|
||||
- exact same-project SCANNING may mint one single-use confirmed STOP permit on
|
||||
the still-open exact control binding;
|
||||
- accepted STOP without READY or SCAN_STOPPING by the backend deadline closes
|
||||
only host-owned resources, records `timed_out` / `standby-unknown`, preserves
|
||||
the unresolved ledger and keeps every mutation fenced.
|
||||
|
||||
The composite supervisor and physical-command ledger can disable Apply before
|
||||
device I/O. Their denial does not turn mode, selection or input into recovery.
|
||||
|
||||
### Explicit read-only recovery
|
||||
|
||||
Recovery is a distinct, explicitly requested exceptional action. It is never a
|
||||
continuation of row selection or Apply. The browser supplies neither endpoint,
|
||||
substitute transport nor ledger authority. The backend pins the durable record's
|
||||
exact transport, identity/profile, operation/revision, acquisition/project,
|
||||
topology revision and host epoch.
|
||||
|
||||
The non-reconnecting observation is:
|
||||
|
||||
```text
|
||||
topology-probed
|
||||
-> pre-start-control-opened
|
||||
-> device-info-requested (ordinal 1; exactly one publish)
|
||||
-> device-info-verified
|
||||
-> awaiting-passive-fresh-status
|
||||
-> cessation | active-same-project | foreign-active | inconclusive | failed
|
||||
```
|
||||
|
||||
It publishes exactly one canonical DeviceInfo request and then accepts only a
|
||||
fresh non-retained DeviceStatus from the same socket generation after that
|
||||
barrier. It publishes no status solicitation, DeviceConfig, time sync,
|
||||
workspace, project, START or STOP; it never scans, reconnects, provisions or
|
||||
continues into Apply.
|
||||
|
||||
Canonical READY records cessation/standby. Initialized SCANNING may rebind only
|
||||
when operation/acquisition, identity/profile, transport, host epoch and project
|
||||
all match; it exposes one separate single-use confirmed STOP checkpoint.
|
||||
Foreign, stale or inconclusive evidence changes no topology or authority.
|
||||
|
||||
### Explicit retirement and reopen
|
||||
|
||||
`physical-command.retire-unavailable` is a separately confirmed local durable
|
||||
recovery action for one unresolved target that is truly unavailable or replaced.
|
||||
Admission requires stable idempotency identity and exact backend runtime,
|
||||
operation, ledger revision and transport CAS plus safe lifecycle ownership. It
|
||||
preserves the original unknown outcome, activates the exact-transport deny,
|
||||
performs zero device/host I/O and starts no discovery.
|
||||
|
||||
`physical-command.reopen-retired-reconciliation` is also separately confirmed.
|
||||
It requires an exact fresh same-transport candidate, stable `reopening_id`, exact
|
||||
runtime/revision/retirement/transport/discovery CAS and safe lifecycle ownership.
|
||||
It preserves retirement audit, removes only that retirement's active deny and
|
||||
performs zero device/host I/O. The explicit recovery intent may then run one
|
||||
exact read-only observation. **Выбрать** never invokes retirement, reopen or
|
||||
Verify. The only Apply exception is an internal, request-bound local reopen
|
||||
checkpoint for an explicit scenario reset plus its exact successor Scan. It is
|
||||
ordered after network PREPARED and before the sole dispatch edge, remains
|
||||
invisible in the wizard and grants no command authority. The same applied
|
||||
intent may then settle it read-only from fresh DeviceInfo plus non-retained
|
||||
READY/SCANNING evidence.
|
||||
|
||||
FW 3.0.2 BLE `7f02` contains no stable DeviceInfo identity. Mission Core cannot
|
||||
prove during BLE-only discovery that the same physical unit has a new
|
||||
CoreBluetooth UUID. This remains an explicit protocol/hardware gap.
|
||||
|
||||
### Bounded durable audit rollover
|
||||
|
||||
An explicit local scenario reset must not become unavailable merely because
|
||||
closed retire/reopen history filled the 64 KiB hot ledger. Before a transition
|
||||
would exceed that bound, Mission Core durably publishes the complete previous
|
||||
ledger as a private, owner-only, content-addressed archive segment and then
|
||||
atomically publishes a compact v4 main record. The main record retains every
|
||||
active retirement deny, the newest lost-response retire/reopen checkpoint, and
|
||||
all reconciliation/confirmation proof required by the current physical
|
||||
operation. Compaction never changes a device outcome and performs no device,
|
||||
network or host I/O.
|
||||
|
||||
Archive segments form a predecessor hash chain with exact sequence and byte
|
||||
accounting. Reload verifies directory and file ownership/mode, rejects symlink
|
||||
traversal, bounds total segments and bytes, reparses every embedded ledger and
|
||||
fails closed for a missing, replayed, reordered or tampered segment. Operation,
|
||||
reconciliation, verification, confirmation, retirement and reopening identities
|
||||
remain globally one-use across the hot record and archive. The archive segment
|
||||
is fsynced before the main-file replace: a crash may leave only an inert orphan,
|
||||
while retry of the same CAS reuses identical bytes and cannot duplicate the
|
||||
referenced chain.
|
||||
|
||||
Scenario reset asks the ledger to build the exact prospective retirement or
|
||||
prepared→not-dispatched plan before closing any local receiver, camera,
|
||||
control-session or network ownership. That shared planner applies the same hot
|
||||
serialization, compaction, segment, count and total-byte bounds as commit. When
|
||||
rollover is required, preflight may idempotently prepublish only the immutable
|
||||
content-addressed predecessor; the main revision/CAS and physical disposition
|
||||
remain unchanged. This also proves owner/mode, symlink and content-collision
|
||||
conditions before teardown.
|
||||
|
||||
Archive publication is restart-safe at the hard-link boundary. A process death
|
||||
after destination link and directory fsync but before temporary-name unlink may
|
||||
leave exactly two private names for one inode. Retry removes only a strictly
|
||||
named, owner-only temporary alias whose bytes and inode exactly match the
|
||||
expected destination and whose link count is exactly two, fsyncs that cleanup,
|
||||
then reuses the destination. Any unrelated hard link, extra temporary, symlink,
|
||||
metadata mismatch or byte mismatch remains a fail-closed corruption condition.
|
||||
|
||||
## Failure and restart semantics
|
||||
|
||||
- UI entry, mode, selection, input, polling, refresh and layout changes
|
||||
start no device operation.
|
||||
- Search starts only when pressed, runs once for six seconds and terminalizes.
|
||||
- Apply starts only when pressed, uses one exact fresh candidate and may perform
|
||||
at most one K1 mutation.
|
||||
- Candidate/runtime/intent drift is explicit stale, never hidden rescan.
|
||||
- Post-dispatch uncertainty is explicit outcome-unknown, never automatic replay.
|
||||
- K1 power loss revokes the active session without inventing standby.
|
||||
- Wi-Fi loss and WAN loss are distinct: local LAN control may survive WAN loss;
|
||||
route/association loss revokes only dependent host/control evidence.
|
||||
- Browser refresh restores no live local selection and causes no I/O.
|
||||
- Backend restart restores durable audit and safety ledgers, but no live BLE,
|
||||
control or operator intent.
|
||||
- Mac sleep/restart rotates host/runtime authority and rejects late work.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Mode, selection and input result in zero controller calls.
|
||||
- Each Search click issues exactly one scan with duration `6`; no effect, timer,
|
||||
selection or Apply path calls Scan.
|
||||
- Every result keeps the same ordinary **Выбрать** action. Selection retains
|
||||
the card, shows applicable inputs immediately, shows no loader and calls no
|
||||
controller. After an explicit committed scenario reset and its successfully
|
||||
completed successor Scan, this includes the exact UUID used by the retired
|
||||
prior scenario; the row never exposes a reconnect/reopen/Verify CTA.
|
||||
- During unresolved physical recovery, a completed explicit Scan still renders
|
||||
passive BLE evidence but cannot substitute a foreign target for the durable
|
||||
recovery record. Exact recovery remains a separate established-session
|
||||
action outside the cold result list; ordinary **Выбрать** never invokes its
|
||||
reopen or read-only Verify. A new network flow first requires explicit reset
|
||||
and a successor Scan.
|
||||
- Bridge/Direct show SSID/password; Quick Connect does not.
|
||||
- Exactly one primary **Применить** owns the connection request. Its frontend
|
||||
handler calls no Scan/Verify/reopen helper and it permits at most one device
|
||||
mutation. For an exact reset-owned retired UUID, the backend may append only
|
||||
the internal local settlement checkpoint described above before dispatch.
|
||||
A later SCANNING settlement grants only explicit STOP authority and never
|
||||
restarts the reset-owned receiver, camera, writer or acquisition.
|
||||
- Stale/pre-dispatch and unknown/post-dispatch outcomes are visibly distinct.
|
||||
- Applied-but-unready/unknown spends the old Apply and gates ordinary mode,
|
||||
Scan, selection and Apply behind an explicit recovery choice; recommended
|
||||
Verify has only a server-bound backend target and no browser fallback.
|
||||
- A new intent remains possible only through current backend policy. Bridge
|
||||
uses explicit local-only `prepare-select-device`; Quick/Direct use an explicit
|
||||
admitted Scan and latest fresh row; mode change requires `mode_selection`.
|
||||
Each route starts no hidden frontend or mutating continuation and ends in a
|
||||
later fresh Scan/new idempotency Apply. The declared service-owned
|
||||
same-intent read-only bootstrap after the durable ACK is the sole continuation
|
||||
exception and creates no UI action.
|
||||
- The exact Apply REST snapshot with `phase=network_applied` completes the
|
||||
network intent for both `control_not_ready` and `unknown`, without requiring
|
||||
connection-ready or waiting for WebSocket/poll convergence.
|
||||
- A service-owned supervised control bootstrap may continue read-only after
|
||||
that ACK. It performs no BLE/host mutation or retry and creates no frontend
|
||||
Scan/Verify/new-Apply action or blocking Apply loader. Its exact
|
||||
accepted/running state may own one passive settling indicator only.
|
||||
- Operator error copy comes only from an allowlisted public error-code mapping;
|
||||
unknown/raw messages use a canonical secret-free fallback and never render
|
||||
credentials, SSIDs, payloads or stack traces.
|
||||
- No timeout, disconnect, refresh, restart or state update starts a continuation
|
||||
or replays an ended action.
|
||||
- Supervisor, identity pin, network-attempt ledger, physical-command ledger,
|
||||
process/BLE lease and one-use recovery STOP remain authoritative.
|
||||
- The plugin uses shared `Button`, `TextField`, `ActivityIndicator` and
|
||||
`StatusBadge`; contract tests reject raw local controls and literal colors.
|
||||
- Geometry and long-copy tests keep all actions reachable without overlap.
|
||||
- Bridge and Quick Connect retain separate real-hardware acceptance.
|
||||
|
||||
This ADR does not itself declare hardware coverage. The manifest may mark a
|
||||
scenario software-covered only when named executable tests cover the software
|
||||
invariant; remaining K1/macOS/router and Quick Connect gaps stay explicit.
|
||||
Reference in New Issue
Block a user