10 KiB
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 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.
- Mission Core never substitutes its inventory UUID, BLE UUID or serial for the vendor request identity.
- One private application-level OpenAPI authority is loaded outside Git, Ops, browser state, APIs and logs. It is not copied into scanner profiles.
- 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.
- 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. 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.
- 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.
- 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.
- 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.
- No firmware, activation, account, update or vendor-cloud mutation belongs to
this path.
GetCloudServerConfigis a local K1 read. - STOP completion has separate gates: correlated result, local stream quiescence/evidence sealing, READY plus steady green, then independent native project verification.
Recovered START sequence
The clean cycle contains:
- unbound
DeviceInfoRequest; - unbound
ModelingStatusRequest; - unbound
GetRtkAdvanceRequest; - bound
DeviceConfigRequesttime/timezone sync; - bound
DeviceInfoRequest; - bound
GetRtkAdvanceRequest; - bound
GetNtripProfileRequest; - bound
GetCloudServerConfigRequest; - bound
GetRtkAdvanceRequest; - bound
DeviceInfoRequest; - bound
ModelingRequestSTART. - bound
ModelingStatusRequest; - bound
DeviceInfoRequestafter static initialization; - bound
ModelingStatusRequestafter 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 five-batch response-barrier orchestrator. 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 adds the dormant facade 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. Plugin v0.4.0 exposes 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 proved that five response batches alone are insufficient, the collapsed
bootstrap entry point and standalone START/STOP were disabled. The uninstalled
replacement 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 READY plus operator-confirmed
standby. 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 and steady-green
confirmation remain separate operator actions. Preparing local reception happens before the
START event is released. After STOP, local capture remains owned until live
unbound READY and explicit visual confirmation. 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 uninstalled transport:
- provision the reviewed private authority into the fixed local Keychain item;
- confirm battery/storage and select one K1 over BLE;
- run the exact staged response-gated preparation and attest activated FW 3.0.2;
- confirm READY identity against DeviceInfo and review shadow metadata;
- send one START without retry and correlate its response;
- preserve the same control MQTT connection, issue the retained post-START reads and observe calibration plus first point/pose/camera data;
- send one STOP without retry and correlate its response;
- seal local evidence while accepting the bounded stream tail;
- wait for READY and steady green;
- 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.