# ADR 0012: live-bound K1 control with application-level authority - Status: accepted - Date: 2026-07-18 - Extends: ADR 0004, ADR 0005, ADR 0009 and ADR 0010 ## Context Owner-controlled LixelGO traffic proves the K1 preparation and modeling dialogue for one activated K1 running the exact `xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1` profile. Offline regression proves that Mission Core reproduces all ten requests before START, plus START and STOP, byte-for-byte. This describes the current device dialogue but does not authorize writes to any K1. The profile identifier above records the accepted v0.5.0 boundary. ADR 0013 supersedes the active runtime selection with `xgrids.lixelkity-k1.fw-3.0.2.local-network.v2`; it does not change the command dialogue or retroactively broaden this acceptance evidence. The protocol exposes identities with different ownership. Mission Core's inventory UUID and the macOS CoreBluetooth UUID are local transport identities. The vendor device ID and serial identify the live scanner. The OpenAPI value is private application material: retained traffic uses one value across every request type, and decompiled LixelGO uses its embedded application value before the serial-derived fallback. It is therefore not a manual per-scanner profile. The selected BLE peripheral returns the LAN address of that same unit during the reviewed Wi-Fi provisioning exchange. On MQTT, LixelGO first sends `DeviceInfoRequest` without a device ID. `DeviceInfoResponse` supplies vendor ID, serial, model, activation and version facts; subsequent headers bind to that identity. Native project durability remains separate from command acceptance. ## Decision K1 application control is exact-profile, live-bound and fail-closed. 1. Mission Core never substitutes its inventory UUID, BLE UUID or serial for the vendor request identity. 2. One private application-level OpenAPI authority is loaded outside Git, Ops, browser state, APIs and logs. It is not copied into scanner profiles. 3. The BLE-selected transport is followed by an unbound DeviceInfo exchange. Its live vendor ID, serial, activation and FW 3.0.2 facts form the transient device binding; a saved profile is optional metadata, not protocol authority. 4. The complete retained START lifecycle keeps its 14-operation order, stage boundaries and one continuous control MQTT session. The first ten operations are pre-START; operation 11 is START; operation 12 is a bound ModelingStatus read; operations 13–14 are the post-initialization DeviceInfo/ModelingStatus refresh. Operation 12 is response-free in both retained captures; the sole later ModelingStatus response follows and belongs to required operation 14. The only preparatory mutation is time/timezone sync. No stage is collapsed, skipped or automatically retried. Captured wall-clock gaps are operator/UI timing, not protocol delays. Stage progression uses explicit UI actions and live K1 state reports. 5. Live DeviceInfo binding must agree with the live DeviceStatus stream before START/STOP planning. Identity drift, malformed data, inactive equipment, profile mismatch or an unexpected lifecycle state fails closed. 6. START retains record-and-calculate mode 2, LCC mode 1, handheld mount 0 and the validated project name. STOP contains only bound header and action. 7. MQTT application semantics remain QoS 2, retain false. An unknown outcome is reconciled from correlated response, status and physical evidence before any new operator-authorized attempt. 8. No firmware, activation, account, update or vendor-cloud mutation belongs to this path. `GetCloudServerConfig` is a local K1 read. 9. STOP completion has separate gates: correlated result, protocol-reported unbound READY, then local stream quiescence/evidence sealing and independent native-project verification. Stable green is useful physical corroboration, not a second operator acknowledgement in the application lifecycle. ## Recovered START sequence The clean cycle contains: 1. unbound `DeviceInfoRequest`; 2. unbound `ModelingStatusRequest`; 3. unbound `GetRtkAdvanceRequest`; 4. bound `DeviceConfigRequest` time/timezone sync; 5. bound `DeviceInfoRequest`; 6. bound `GetRtkAdvanceRequest`; 7. bound `GetNtripProfileRequest`; 8. bound `GetCloudServerConfigRequest`; 9. bound `GetRtkAdvanceRequest`; 10. bound `DeviceInfoRequest`; 11. bound `ModelingRequest` START. 12. bound `ModelingStatusRequest`; 13. bound `DeviceInfoRequest` after static initialization; 14. bound `ModelingStatusRequest` after static initialization. Normal sessions are `${device_id_or_empty}:${MessageType}`. Time sync uses the captured special relation `${device_id}:DeviceConfigRequest:Publish_Proto_DeviceConfig_SetTime`. ## Current implementation boundary `application_bootstrap.py` provides DeviceInfo decoding, the exact ten-request shadow plan and a four-window response-barrier orchestrator. The initial DeviceInfo response binds identity; ordinals 2–6 then remain ordered in one window so ordinal 3 does not create a barrier absent from the captures. Its private retained regression completes with 10/10 request payload/topic matches and correlated required responses. `ModelingStatusRequest` remains non-blocking exactly where the capture had no synchronous response. `application_authority.py` reads the exact 36-byte authority only from a fixed macOS Keychain item and offers no plaintext fallback. `LiveModelingControlSafety` independently binds DeviceInfo identity to live status and creates non-executable START/STOP plans. Public state contains only booleans, counts and wire metadata. Private identity, authority and payload bytes stay out of repr/API output. `application_publish.py` defines reviewed one-shot envelopes plus a current write-disabled publisher type. That type has no enable switch and raises before its injected transport sink can be called. Both shadow paths remain blocked by `vendor-writes-disabled` and `publisher-not-installed`; no MQTT publisher or automatic retry path exists. `application_execution.py` defines the legacy dormant shadow boundary. An operator-confirmed arm may retain authority for 15–300 seconds in a non-exportable process-memory lease and construct the shadow orchestrator. The lease and orchestrator are dropped together on expiry/disarm and also before reprovision, acquisition preparation or shutdown. The prior plugin v0.4.0 exposed only shadow arm/disarm/state actions and redacted snapshots. The coordinator has no request-emission method and the installed sink is explicitly uninstalled; this runtime wiring does not widen write authority. `application_mqtt.py` and `application_acceptance.py` provide the reviewed physical-acceptance boundary. The MQTT type repeats the retained 3.1.1 session (`clean_session=false`, keepalive 60), subscribes only to exact known response topics, admits only reviewed request topics and QoS2/retain-false envelopes, and consumes explicit bootstrap ordinals/modeling actions before the first publish call. It performs one connect attempt with no reconnect. PUBCOMP and every required application response are separate barriers; stale, duplicate, unexpected, oversized or timed-out responses poison the transport. An unknown post-publish outcome can only be reconciled physically/status-wise; the same operation key can never be attempted again by that transport. The acceptance executor requires a 15–120 second single-action permit backed by five explicit operator confirmations. After the failed 2026-07-18 physical attempt exposed topic-only response routing and an invented ordinal-3 barrier, the collapsed bootstrap entry point and standalone START/STOP were disabled. Their staged replacement, installed in plugin v0.5.0, binds requests 1–6 to connection, request 7 to scan-workspace entry, requests 8–10 to project-prompt entry and START to a separate operator permit. There is no separate project-name mutation; the validated name is carried by START. Operations 13–14 are gated by bound `SCANNING`, project presence and `init_ready`, not by the observed 24–25-second duration. The same owner services MQTT through explicit STOP and waits for live unbound READY. Fault/status observations are redacted and any fault forbids automatic continuation. The socket remains serviced during operator/UI waits, and each short action permit is created at its button confirmation rather than at connection time. Plugin v0.5.0 installs this boundary through `application_session.py`. One background owner is the only thread allowed to touch the control MQTT client. The standard UI turns one explicit launch intent into response-gated REST actions for session open (1–6), workspace entry (7), project preparation (8–10) and START. This is control compression, not protocol compression: each next action waits for the exact server/device phase, and local polling never retries or times a K1 write. STOP remains a separate explicit operator action; the subsequent READY/unbound transition completes without a second click. Preparing local reception happens before the START event is released. After STOP, local capture remains owned until that live protocol state. Page close, model switching, polling and service state reads cannot emit START/STOP; model switching is blocked while the socket is open. Repository tests replace the transport with in-memory fakes and perform no equipment I/O. `MacOSKeychainApplicationAuthorityProvisioner` invokes Apple's interactive `security add-generic-password ... -w` prompt with `-w` last. The private value therefore never enters Mission Core argv, environment, files, logs or browser state, and the resulting item is immediately validated by the fixed loader. The repository CLI exposes this as an explicit one-time admin command, but the command has not been executed in this milestone. ## Promotion gate Promotion now requires operator-present physical acceptance of the installed, explicitly staged transport: 1. provision the reviewed private authority into the fixed local Keychain item; 2. confirm battery/storage and select one K1 over BLE; 3. run the exact staged response-gated preparation and attest activated FW 3.0.2; 4. confirm READY identity against DeviceInfo and review shadow metadata; 5. send one START without retry and correlate its response; 6. preserve the same control MQTT connection, issue the retained post-START reads and observe calibration plus first point/pose/camera data; 7. send one STOP without retry and correlate its response; 8. seal local evidence while accepting the bounded stream tail; 9. wait for protocol-reported unbound READY and record stable green as physical corroboration; 10. verify the native project through the vendor-supported workflow. Any unknown response, identity change, fault, low battery/storage or unexpected transition pauses the test. It never triggers a guessed recovery command. ## Consequences Adding another identical K1 means selecting it over BLE and deriving a fresh live DeviceInfo binding, not creating or copying a command profile. A second physical K1 remains a useful portability acceptance, but another packet capture is not required before implementing the reviewed current profile. The design preserves native record-and-calculate behavior while keeping vendor control in the XGRIDS plugin and out of generic Mission Core.