feat(k1): add offline control execution gate

This commit is contained in:
DCCONSTRUCTIONS
2026-07-18 14:14:30 +03:00
parent d7749208a7
commit ea811ff370
17 changed files with 916 additions and 44 deletions
@@ -0,0 +1,118 @@
from __future__ import annotations
import platform
import shutil
import subprocess
from dataclasses import dataclass
from typing import Protocol
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
ApplicationBootstrapError,
ApplicationControlAuthority,
)
KEYCHAIN_SERVICE = "NODEDC Mission Core XGRIDS K1 OpenAPI"
KEYCHAIN_ACCOUNT = "lixelgo-application-fw-3.0.2"
KEYCHAIN_TIMEOUT_SECONDS = 5.0
class ApplicationAuthorityLoadError(RuntimeError):
"""The private application authority could not be loaded safely."""
class CommandRunner(Protocol):
def __call__(
self,
args: list[str],
*,
capture_output: bool,
check: bool,
timeout: float,
) -> subprocess.CompletedProcess[bytes]: ...
@dataclass(frozen=True, slots=True)
class ApplicationAuthoritySourceSnapshot:
provider: str = "macos-keychain"
service: str = KEYCHAIN_SERVICE
account: str = KEYCHAIN_ACCOUNT
secret_cached: bool = False
secret_exportable: bool = False
file_fallback_enabled: bool = False
environment_fallback_enabled: bool = False
def as_dict(self) -> dict[str, object]:
return {
"provider": self.provider,
"service": self.service,
"account": self.account,
"secret_cached": self.secret_cached,
"secret_exportable": self.secret_exportable,
"file_fallback_enabled": self.file_fallback_enabled,
"environment_fallback_enabled": self.environment_fallback_enabled,
}
class MacOSKeychainApplicationAuthorityLoader:
"""Read the exact-profile authority from the current user's Keychain.
There is deliberately no environment, plaintext-file, browser or API
fallback. The secret is requested through the absolute ``security`` binary
and is never interpolated into an exception, repr or subprocess argument.
"""
def __init__(self, *, runner: CommandRunner = subprocess.run) -> None:
self._runner = runner
def snapshot(self) -> ApplicationAuthoritySourceSnapshot:
return ApplicationAuthoritySourceSnapshot()
def load(self) -> ApplicationControlAuthority:
if platform.system() != "Darwin":
raise ApplicationAuthorityLoadError(
"application authority loading is supported only through macOS Keychain"
)
security = shutil.which("security")
if security != "/usr/bin/security":
raise ApplicationAuthorityLoadError("trusted macOS security binary is unavailable")
try:
completed = self._runner(
[
security,
"find-generic-password",
"-s",
KEYCHAIN_SERVICE,
"-a",
KEYCHAIN_ACCOUNT,
"-w",
],
capture_output=True,
check=False,
timeout=KEYCHAIN_TIMEOUT_SECONDS,
)
except (OSError, subprocess.SubprocessError) as exc:
raise ApplicationAuthorityLoadError("macOS Keychain authority lookup failed") from exc
if completed.returncode != 0:
raise ApplicationAuthorityLoadError("macOS Keychain authority is unavailable")
secret_buffer = bytearray(completed.stdout)
try:
while secret_buffer.endswith((b"\n", b"\r")):
secret_buffer.pop()
try:
secret = secret_buffer.decode("ascii")
except UnicodeDecodeError as exc:
raise ApplicationAuthorityLoadError(
"macOS Keychain authority has an invalid encoding"
) from exc
try:
return ApplicationControlAuthority(openapi_key=secret)
except ApplicationBootstrapError as exc:
raise ApplicationAuthorityLoadError(
"macOS Keychain authority does not match the reviewed profile"
) from exc
finally:
for index in range(len(secret_buffer)):
secret_buffer[index] = 0
@@ -3,6 +3,7 @@ from __future__ import annotations
import hashlib
import hmac
import re
import threading
from dataclasses import dataclass, field
from typing import Literal
@@ -62,6 +63,10 @@ class ApplicationControlAuthority:
"application authority does not match the reviewed K1 profile"
)
_validate_ascii_identity(self.openapi_key, "openapi_key")
if len(self.openapi_key) != 36:
raise ApplicationBootstrapError(
"openapi_key must match the exact 36-byte reviewed application profile"
)
@dataclass(frozen=True, slots=True)
@@ -133,6 +138,9 @@ class EncodedApplicationRequest:
payload_bytes: int
mutates_device: bool
requires_live_binding: bool
response_required: bool
session_id: str = field(repr=False)
vendor_device_id: str | None = field(repr=False)
qos: int = 2
retain: bool = False
@@ -147,6 +155,7 @@ class EncodedApplicationRequest:
"payload_bytes": self.payload_bytes,
"mutates_device": self.mutates_device,
"requires_live_binding": self.requires_live_binding,
"response_required": self.response_required,
"qos": self.qos,
"retain": self.retain,
}
@@ -181,6 +190,39 @@ class DeviceInfoResponse:
result_code: int
@dataclass(frozen=True, slots=True)
class ApplicationResponse:
session_id: str = field(repr=False)
vendor_device_id: str = field(repr=False)
openapi_key: str = field(repr=False)
result_code: int
@dataclass(frozen=True, slots=True)
class ApplicationBootstrapSnapshot:
current_batch: int
total_batches: int
batch_issued: bool
pending_response_topics: tuple[str, ...]
live_binding_observed: bool
bootstrap_complete: bool
executable: bool = False
vendor_writes_enabled: bool = False
def as_dict(self) -> dict[str, object]:
return {
"mode": "shadow-only",
"current_batch": self.current_batch,
"total_batches": self.total_batches,
"batch_issued": self.batch_issued,
"pending_response_topics": list(self.pending_response_topics),
"live_binding_observed": self.live_binding_observed,
"bootstrap_complete": self.bootstrap_complete,
"executable": self.executable,
"vendor_writes_enabled": self.vendor_writes_enabled,
}
def build_shadow_bootstrap(
authority: ApplicationControlAuthority,
binding: LiveDeviceControlBinding,
@@ -213,6 +255,7 @@ def build_shadow_bootstrap(
DEVICE_INFO_RESPONSE_TOPIC,
None,
False,
True,
),
(
"identity-discovery",
@@ -221,6 +264,7 @@ def build_shadow_bootstrap(
MODELING_STATUS_RESPONSE_TOPIC,
None,
False,
False,
),
(
"identity-discovery",
@@ -229,6 +273,7 @@ def build_shadow_bootstrap(
GET_RTK_ADVANCE_RESPONSE_TOPIC,
None,
False,
True,
),
(
"bound-preparation",
@@ -237,6 +282,7 @@ def build_shadow_bootstrap(
DEVICE_CONFIG_RESPONSE_TOPIC,
DEVICE_CONFIG_TIME_CONTEXT,
True,
True,
),
(
"bound-preparation",
@@ -245,6 +291,7 @@ def build_shadow_bootstrap(
DEVICE_INFO_RESPONSE_TOPIC,
None,
False,
True,
),
(
"bound-preparation",
@@ -253,6 +300,7 @@ def build_shadow_bootstrap(
GET_RTK_ADVANCE_RESPONSE_TOPIC,
None,
False,
True,
),
(
"bound-preparation",
@@ -261,6 +309,7 @@ def build_shadow_bootstrap(
GET_NTRIP_PROFILE_RESPONSE_TOPIC,
None,
False,
True,
),
(
"bound-preparation",
@@ -269,6 +318,7 @@ def build_shadow_bootstrap(
GET_CLOUD_CONFIG_RESPONSE_TOPIC,
None,
False,
True,
),
(
"bound-preparation",
@@ -277,6 +327,7 @@ def build_shadow_bootstrap(
GET_RTK_ADVANCE_RESPONSE_TOPIC,
None,
False,
True,
),
(
"bound-preparation",
@@ -285,12 +336,19 @@ def build_shadow_bootstrap(
DEVICE_INFO_RESPONSE_TOPIC,
None,
False,
True,
),
)
requests: list[EncodedApplicationRequest] = []
for ordinal, (phase, message_type, topic, response_topic, context, mutates) in enumerate(
specs, start=1
):
for ordinal, (
phase,
message_type,
topic,
response_topic,
context,
mutates,
response_required,
) in enumerate(specs, start=1):
request_binding = None if ordinal <= 3 else binding
header = ApplicationRequestHeader(
message_type=message_type,
@@ -316,6 +374,11 @@ def build_shadow_bootstrap(
payload_bytes=len(payload),
mutates_device=mutates,
requires_live_binding=ordinal > 3,
response_required=response_required,
session_id=header.session_id,
vendor_device_id=(
request_binding.vendor_device_id if request_binding is not None else None
),
)
)
return ShadowApplicationBootstrap(tuple(requests))
@@ -324,6 +387,9 @@ def build_shadow_bootstrap(
def decode_and_bind_device_info_response(
payload: bytes,
authority: ApplicationControlAuthority,
*,
expected_session_id: str = ":DeviceInfoRequest",
expected_vendor_device_id: str | None = None,
) -> DeviceInfoResponse:
"""Correlate the bootstrap response and derive a live per-device binding."""
@@ -333,8 +399,12 @@ def decode_and_bind_device_info_response(
device_id = _identity_field(header, 4, "header.device_id")
session_id = _identity_field(header, 5, "header.session_id")
openapi_key = _identity_field(header, 6, "header.openapi_key")
if session_id != ":DeviceInfoRequest":
if not hmac.compare_digest(session_id, expected_session_id):
raise ApplicationBootstrapError("DeviceInfoResponse session correlation failed")
if expected_vendor_device_id is not None and not hmac.compare_digest(
device_id, expected_vendor_device_id
):
raise ApplicationBootstrapError("DeviceInfoResponse vendor identity mismatch")
if not hmac.compare_digest(openapi_key, authority.openapi_key):
raise ApplicationBootstrapError("DeviceInfoResponse application authority mismatch")
@@ -371,6 +441,221 @@ def decode_and_bind_device_info_response(
return DeviceInfoResponse(binding, session_id, openapi_key, result_code)
def correlate_application_response(
payload: bytes,
expected: EncodedApplicationRequest,
authority: ApplicationControlAuthority,
*,
live_binding: LiveDeviceControlBinding | None = None,
) -> ApplicationResponse:
"""Correlate a non-DeviceInfo response to an exact emitted request."""
expected_device_id = expected.vendor_device_id
if expected_device_id is None and live_binding is not None:
expected_device_id = live_binding.vendor_device_id
if expected_device_id is None:
raise ApplicationBootstrapError(
"generic response correlation requires live DeviceInfo identity"
)
_check_payload(payload, "application response")
top = _selected_unique_fields(
payload,
"application response",
max_fields=64,
selected={1, 15},
)
header = _selected_unique_fields(
_required_bytes(top, 1, "response.header"),
"header",
max_fields=32,
selected={4, 5, 6},
)
device_id = _identity_field(header, 4, "header.device_id")
session_id = _identity_field(header, 5, "header.session_id")
openapi_key = _identity_field(header, 6, "header.openapi_key")
if not hmac.compare_digest(device_id, expected_device_id):
raise ApplicationBootstrapError("application response vendor identity mismatch")
if not hmac.compare_digest(session_id, expected.session_id):
raise ApplicationBootstrapError("application response session correlation failed")
if not hmac.compare_digest(openapi_key, authority.openapi_key):
raise ApplicationBootstrapError("application response authority mismatch")
error = _selected_unique_fields(
_required_bytes(top, 15, "response.error"),
"error",
max_fields=8,
selected={1, 2},
)
result_code = _required_uint(error, 1, "response.error.code")
if result_code != OPENAPI_SUCCESS:
raise ApplicationBootstrapError(
f"application response rejected with result code {result_code}"
)
return ApplicationResponse(session_id, device_id, openapi_key, result_code)
class ShadowApplicationBootstrapOrchestrator:
"""Advance the retained dialogue only across observed response barriers.
The clean cycle emitted five batches. ``ModelingStatusRequest`` in the
second batch had no required synchronous response before preparation
continued; readiness remains a separate live DeviceStatus gate.
"""
_BATCH_ORDINALS = ((1,), (2, 3), (4, 5, 6), (7,), (8, 9, 10))
def __init__(
self,
authority: ApplicationControlAuthority,
*,
epoch_seconds: int,
timezone_name: str,
) -> None:
self._lock = threading.Lock()
self._authority = authority
self._epoch_seconds = epoch_seconds
self._timezone_name = timezone_name
self._binding: LiveDeviceControlBinding | None = None
self._plan: ShadowApplicationBootstrap | None = None
self._batch_index = 0
self._batch_issued = False
self._pending: dict[str, EncodedApplicationRequest] = {}
self._complete = False
self._initial_request = _build_initial_device_info_request(authority)
def snapshot(self) -> ApplicationBootstrapSnapshot:
with self._lock:
return ApplicationBootstrapSnapshot(
current_batch=min(self._batch_index + 1, len(self._BATCH_ORDINALS)),
total_batches=len(self._BATCH_ORDINALS),
batch_issued=self._batch_issued,
pending_response_topics=tuple(sorted(self._pending)),
live_binding_observed=self._binding is not None,
bootstrap_complete=self._complete,
)
@property
def binding(self) -> LiveDeviceControlBinding | None:
with self._lock:
return self._binding
def next_batch(self) -> tuple[EncodedApplicationRequest, ...]:
"""Return the next batch once; repeated access fails closed."""
with self._lock:
if self._complete:
raise ApplicationBootstrapError("application bootstrap is already complete")
if self._batch_issued:
raise ApplicationBootstrapError(
"current application bootstrap batch was already issued"
)
requests: tuple[EncodedApplicationRequest, ...]
if self._batch_index == 0:
requests = (self._initial_request,)
else:
if self._plan is None:
raise ApplicationBootstrapError("live DeviceInfo binding is missing")
requests = tuple(
self._plan.requests[ordinal - 1]
for ordinal in self._BATCH_ORDINALS[self._batch_index]
)
pending = {
request.response_topic: request for request in requests if request.response_required
}
if len(pending) != sum(request.response_required for request in requests):
raise ApplicationBootstrapError(
"bootstrap batch contains ambiguous required response topics"
)
if not pending:
raise ApplicationBootstrapError("bootstrap batch has no recovered response barrier")
self._pending = pending
self._batch_issued = True
return requests
def accept_response(self, topic: str, payload: bytes) -> None:
"""Correlate one required response and unlock only the next batch."""
with self._lock:
if not self._batch_issued:
raise ApplicationBootstrapError("no application bootstrap batch is in flight")
expected = self._pending.get(topic)
if expected is None:
raise ApplicationBootstrapError(
"application response is not required by the current batch"
)
if expected.ordinal == 1:
response = decode_and_bind_device_info_response(payload, self._authority)
binding = response.binding
plan = build_shadow_bootstrap(
self._authority,
binding,
epoch_seconds=self._epoch_seconds,
timezone_name=self._timezone_name,
)
if plan.requests[0].payload != self._initial_request.payload:
raise ApplicationBootstrapError(
"bound bootstrap plan changed the emitted identity request"
)
self._binding = binding
self._plan = plan
else:
if self._binding is None:
raise ApplicationBootstrapError("live DeviceInfo binding is missing")
if expected.message_type == "DeviceInfoRequest":
response = decode_and_bind_device_info_response(
payload,
self._authority,
expected_session_id=expected.session_id,
expected_vendor_device_id=self._binding.vendor_device_id,
)
if response.binding != self._binding:
raise ApplicationBootstrapError(
"live DeviceInfo facts changed during bootstrap"
)
else:
correlate_application_response(
payload,
expected,
self._authority,
live_binding=self._binding,
)
del self._pending[topic]
if self._pending:
return
self._batch_issued = False
self._batch_index += 1
if self._batch_index == len(self._BATCH_ORDINALS):
self._complete = True
def _build_initial_device_info_request(
authority: ApplicationControlAuthority,
) -> EncodedApplicationRequest:
header = ApplicationRequestHeader(
message_type="DeviceInfoRequest",
authority=authority,
)
payload = _bytes_field(1, _encode_header(header))
_check_payload(payload, "application request")
return EncodedApplicationRequest(
ordinal=1,
phase="identity-discovery",
message_type="DeviceInfoRequest",
topic=DEVICE_INFO_REQUEST_TOPIC,
response_topic=DEVICE_INFO_RESPONSE_TOPIC,
payload=payload,
payload_sha256=hashlib.sha256(payload).hexdigest(),
payload_bytes=len(payload),
mutates_device=False,
requires_live_binding=False,
response_required=True,
session_id=header.session_id,
vendor_device_id=None,
)
def _encode_header(header: ApplicationRequestHeader) -> bytes:
parts: list[bytes] = []
if header.binding is not None:
@@ -438,6 +723,26 @@ def _unique_fields(
return result
def _selected_unique_fields(
payload: bytes,
name: str,
*,
max_fields: int,
selected: set[int],
) -> dict[int, ProtoField]:
result: dict[int, ProtoField] = {}
try:
for item in iter_fields(payload, max_fields=max_fields):
if item.number not in selected:
continue
if item.number in result:
raise ApplicationBootstrapError(f"{name} field {item.number} is duplicated")
result[item.number] = item
except ProtobufWireError as exc:
raise ApplicationBootstrapError(f"invalid {name}: {exc}") from exc
return result
def _required_bytes(fields: dict[int, ProtoField], number: int, name: str) -> bytes:
item = fields.get(number)
if item is None or item.wire_type != 2 or not isinstance(item.value, bytes):
@@ -0,0 +1,128 @@
from __future__ import annotations
import hashlib
import threading
from dataclasses import dataclass, field
from typing import Protocol
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
EncodedApplicationRequest,
)
from k1link.device_plugins.xgrids_k1.protocol.modeling_safety import (
ShadowModelingCommand,
)
class VendorWritesDisabledError(RuntimeError):
"""The active compatibility profile cannot touch a publish transport."""
class ApplicationPublishSink(Protocol):
def publish(
self,
*,
topic: str,
payload: bytes,
qos: int,
retain: bool,
) -> None: ...
@dataclass(frozen=True, slots=True)
class OneShotPublishEnvelope:
topic: str
payload: bytes = field(repr=False)
payload_sha256: str
payload_bytes: int
qos: int
retain: bool
automatic_retry: bool = False
@classmethod
def from_bootstrap_request(
cls,
request: EncodedApplicationRequest,
) -> OneShotPublishEnvelope:
return cls(
topic=request.topic,
payload=request.payload,
payload_sha256=request.payload_sha256,
payload_bytes=request.payload_bytes,
qos=request.qos,
retain=request.retain,
)
@classmethod
def from_modeling_command(
cls,
command: ShadowModelingCommand,
) -> OneShotPublishEnvelope:
return cls(
topic=command.topic,
payload=command.command.payload,
payload_sha256=command.payload_sha256,
payload_bytes=command.payload_bytes,
qos=command.qos,
retain=command.retain,
)
def __post_init__(self) -> None:
if self.qos != 2 or self.retain or self.automatic_retry:
raise ValueError("publish envelope differs from the reviewed one-shot contract")
if self.payload_bytes != len(self.payload):
raise ValueError("publish envelope payload length mismatch")
if not hashlib.sha256(self.payload).hexdigest() == self.payload_sha256:
raise ValueError("publish envelope payload digest mismatch")
def as_dict(self) -> dict[str, object]:
return {
"topic": self.topic,
"payload_sha256": self.payload_sha256,
"payload_bytes": self.payload_bytes,
"qos": self.qos,
"retain": self.retain,
"automatic_retry": self.automatic_retry,
}
@dataclass(frozen=True, slots=True)
class WriteDisabledPublisherSnapshot:
denied_attempts: int
transport_calls: int = 0
vendor_writes_enabled: bool = False
publisher_armed: bool = False
automatic_retry: bool = False
def as_dict(self) -> dict[str, object]:
return {
"mode": "write-disabled",
"denied_attempts": self.denied_attempts,
"transport_calls": self.transport_calls,
"vendor_writes_enabled": self.vendor_writes_enabled,
"publisher_armed": self.publisher_armed,
"automatic_retry": self.automatic_retry,
}
class WriteDisabledOneShotPublisher:
"""Current-profile publish boundary that cannot call its injected sink.
A future write-capable implementation must be a separate reviewed type. It
cannot be enabled by flipping a runtime boolean on this class.
"""
def __init__(self, sink: ApplicationPublishSink) -> None:
self._sink = sink
self._lock = threading.Lock()
self._denied_attempts = 0
def publish_once(self, _envelope: OneShotPublishEnvelope) -> None:
with self._lock:
self._denied_attempts += 1
raise VendorWritesDisabledError(
"active K1 compatibility profile cannot publish vendor requests"
)
def snapshot(self) -> WriteDisabledPublisherSnapshot:
with self._lock:
return WriteDisabledPublisherSnapshot(denied_attempts=self._denied_attempts)