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
+110
View File
@@ -0,0 +1,110 @@
from __future__ import annotations
import subprocess
from unittest.mock import patch
import pytest
from k1link.device_plugins.xgrids_k1.protocol.application_authority import (
KEYCHAIN_ACCOUNT,
KEYCHAIN_SERVICE,
ApplicationAuthorityLoadError,
MacOSKeychainApplicationAuthorityLoader,
)
PRIVATE_AUTHORITY = b"11111111-2222-3333-4444-555555555555\n"
@patch("platform.system", return_value="Darwin")
@patch("shutil.which", return_value="/usr/bin/security")
def test_authority_loads_only_from_fixed_macos_keychain_item(
_which: object,
_system: object,
) -> None:
calls: list[list[str]] = []
def runner(
args: list[str],
*,
capture_output: bool,
check: bool,
timeout: float,
) -> subprocess.CompletedProcess[bytes]:
calls.append(args)
assert capture_output
assert check is False
assert timeout == 5.0
return subprocess.CompletedProcess(args, 0, stdout=PRIVATE_AUTHORITY, stderr=b"")
loader = MacOSKeychainApplicationAuthorityLoader(runner=runner)
authority = loader.load()
assert calls == [
[
"/usr/bin/security",
"find-generic-password",
"-s",
KEYCHAIN_SERVICE,
"-a",
KEYCHAIN_ACCOUNT,
"-w",
]
]
assert PRIVATE_AUTHORITY.decode().strip() not in repr(authority)
assert loader.snapshot().as_dict() == {
"provider": "macos-keychain",
"service": KEYCHAIN_SERVICE,
"account": KEYCHAIN_ACCOUNT,
"secret_cached": False,
"secret_exportable": False,
"file_fallback_enabled": False,
"environment_fallback_enabled": False,
}
@patch("platform.system", return_value="Linux")
def test_authority_loader_has_no_non_keychain_fallback(_system: object) -> None:
loader = MacOSKeychainApplicationAuthorityLoader()
with pytest.raises(ApplicationAuthorityLoadError, match="only through macOS Keychain"):
loader.load()
@patch("platform.system", return_value="Darwin")
@patch("shutil.which", return_value="/usr/bin/security")
def test_authority_loader_redacts_keychain_failures(
_which: object,
_system: object,
) -> None:
private_error = b"private-keychain-diagnostic"
def runner(
args: list[str],
**_kwargs: object,
) -> subprocess.CompletedProcess[bytes]:
return subprocess.CompletedProcess(args, 44, stdout=b"", stderr=private_error)
with pytest.raises(ApplicationAuthorityLoadError) as error:
MacOSKeychainApplicationAuthorityLoader(runner=runner).load()
assert private_error.decode() not in str(error.value)
@patch("platform.system", return_value="Darwin")
@patch("shutil.which", return_value="/usr/bin/security")
def test_authority_loader_rejects_wrong_profile_length_without_reflection(
_which: object,
_system: object,
) -> None:
invalid_secret = b"too-short-private-value"
def runner(
args: list[str],
**_kwargs: object,
) -> subprocess.CompletedProcess[bytes]:
return subprocess.CompletedProcess(args, 0, stdout=invalid_secret, stderr=b"")
with pytest.raises(ApplicationAuthorityLoadError) as error:
MacOSKeychainApplicationAuthorityLoader(runner=runner).load()
assert invalid_secret.decode() not in str(error.value)
@@ -6,6 +6,7 @@ from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
ApplicationBootstrapError,
ApplicationControlAuthority,
LiveDeviceControlBinding,
ShadowApplicationBootstrapOrchestrator,
build_shadow_bootstrap,
decode_and_bind_device_info_response,
)
@@ -75,6 +76,13 @@ def _device_info_response(*, session_id: str = ":DeviceInfoRequest") -> bytes:
return _bytes(1, header) + _bytes(2, device_info) + _bytes(15, error)
def _generic_response(session_id: str) -> bytes:
header = _text(4, VENDOR_DEVICE_ID) + _text(5, session_id) + _text(6, APPLICATION_KEY)
error = _uint(1, OPENAPI_SUCCESS) + _text(2, "success")
repeated_vendor_body = _text(3, "opaque-a") + _text(3, "opaque-b")
return _bytes(1, header) + repeated_vendor_body + _bytes(15, error)
def test_exact_clean_cycle_pre_start_order_and_wire_lengths() -> None:
plan = build_shadow_bootstrap(
_authority(),
@@ -165,3 +173,89 @@ def test_device_info_response_correlation_and_profile_attestation_fail_closed()
epoch_seconds=1_752_680_000,
timezone_name="Europe/Moscow",
)
def test_response_barrier_orchestrator_emits_each_retained_batch_once() -> None:
orchestrator = ShadowApplicationBootstrapOrchestrator(
_authority(),
epoch_seconds=1_752_680_000,
timezone_name="Europe/Moscow",
)
identity = orchestrator.next_batch()
assert [request.ordinal for request in identity] == [1]
with pytest.raises(ApplicationBootstrapError, match="already issued"):
orchestrator.next_batch()
orchestrator.accept_response(identity[0].response_topic, _device_info_response())
initial_reads = orchestrator.next_batch()
assert [request.ordinal for request in initial_reads] == [2, 3]
assert [request.response_required for request in initial_reads] == [False, True]
orchestrator.accept_response(
initial_reads[1].response_topic,
_generic_response(initial_reads[1].session_id),
)
preparation = orchestrator.next_batch()
assert [request.ordinal for request in preparation] == [4, 5, 6]
device_info = preparation[1]
orchestrator.accept_response(
device_info.response_topic,
_device_info_response(session_id=device_info.session_id),
)
with pytest.raises(ApplicationBootstrapError, match="already issued"):
orchestrator.next_batch()
for request in (preparation[2], preparation[0]):
orchestrator.accept_response(
request.response_topic,
_generic_response(request.session_id),
)
ntrip = orchestrator.next_batch()
assert [request.ordinal for request in ntrip] == [7]
orchestrator.accept_response(
ntrip[0].response_topic,
_generic_response(ntrip[0].session_id),
)
final_reads = orchestrator.next_batch()
assert [request.ordinal for request in final_reads] == [8, 9, 10]
for request in final_reads:
payload = (
_device_info_response(session_id=request.session_id)
if request.message_type == "DeviceInfoRequest"
else _generic_response(request.session_id)
)
orchestrator.accept_response(request.response_topic, payload)
snapshot = orchestrator.snapshot()
assert snapshot.bootstrap_complete
assert snapshot.live_binding_observed
assert snapshot.pending_response_topics == ()
assert snapshot.executable is False
assert snapshot.vendor_writes_enabled is False
with pytest.raises(ApplicationBootstrapError, match="already complete"):
orchestrator.next_batch()
def test_orchestrator_rejects_unexpected_response_without_advancing() -> None:
orchestrator = ShadowApplicationBootstrapOrchestrator(
_authority(),
epoch_seconds=1_752_680_000,
timezone_name="Europe/Moscow",
)
orchestrator.next_batch()
with pytest.raises(ApplicationBootstrapError, match="not required"):
orchestrator.accept_response(
"lixel/application/response/get_rtk_advance",
_generic_response(":GetRtkAdvanceRequest"),
)
assert orchestrator.snapshot().current_batch == 1
assert orchestrator.snapshot().batch_issued
def test_application_authority_requires_exact_captured_profile_length() -> None:
with pytest.raises(ApplicationBootstrapError, match="36-byte"):
ApplicationControlAuthority(openapi_key="too-short")
+88
View File
@@ -0,0 +1,88 @@
from __future__ import annotations
import pytest
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
ApplicationControlAuthority,
LiveDeviceControlBinding,
build_shadow_bootstrap,
)
from k1link.device_plugins.xgrids_k1.protocol.application_publish import (
OneShotPublishEnvelope,
VendorWritesDisabledError,
WriteDisabledOneShotPublisher,
)
PRIVATE_AUTHORITY = "11111111-2222-3333-4444-555555555555"
class SpySink:
def __init__(self) -> None:
self.calls = 0
def publish(
self,
*,
topic: str,
payload: bytes,
qos: int,
retain: bool,
) -> None:
del topic, payload, qos, retain
self.calls += 1
def _envelope() -> OneShotPublishEnvelope:
authority = ApplicationControlAuthority(openapi_key=PRIVATE_AUTHORITY)
binding = LiveDeviceControlBinding(
vendor_device_id="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
device_serial="K1SERIAL01",
software_version="V3.0.2-build.1",
system_version="V3.0.2",
device_model="LixelKity K1",
device_type="K1",
is_activated=True,
)
request = build_shadow_bootstrap(
authority,
binding,
epoch_seconds=1_752_680_000,
timezone_name="Europe/Moscow",
).requests[0]
return OneShotPublishEnvelope.from_bootstrap_request(request)
def test_write_disabled_publisher_never_touches_injected_transport() -> None:
sink = SpySink()
publisher = WriteDisabledOneShotPublisher(sink)
envelope = _envelope()
for _ in range(2):
with pytest.raises(VendorWritesDisabledError, match="cannot publish"):
publisher.publish_once(envelope)
assert sink.calls == 0
assert publisher.snapshot().as_dict() == {
"mode": "write-disabled",
"denied_attempts": 2,
"transport_calls": 0,
"vendor_writes_enabled": False,
"publisher_armed": False,
"automatic_retry": False,
}
def test_publish_envelope_redacts_payload_and_rejects_retry_semantics() -> None:
envelope = _envelope()
assert PRIVATE_AUTHORITY not in repr(envelope)
assert PRIVATE_AUTHORITY not in str(envelope.as_dict())
with pytest.raises(ValueError, match="one-shot contract"):
OneShotPublishEnvelope(
topic=envelope.topic,
payload=envelope.payload,
payload_sha256=envelope.payload_sha256,
payload_bytes=envelope.payload_bytes,
qos=1,
retain=False,
)
+3 -3
View File
@@ -65,7 +65,7 @@ def _status_message(
def _authority() -> ApplicationControlAuthority:
return ApplicationControlAuthority(openapi_key="application-private-key")
return ApplicationControlAuthority(openapi_key="11111111-2222-3333-4444-555555555555")
def _binding(**changes: object) -> LiveDeviceControlBinding:
@@ -120,8 +120,8 @@ def test_start_shadow_plan_is_exact_bounded_and_never_executable() -> None:
assert plan.executable is False
assert plan.blockers == ("vendor-writes-disabled", "publisher-not-installed")
assert plan.retry_policy == "never-automatic"
assert "application-private-key" not in repr(plan)
assert "application-private-key" not in str(plan.as_dict())
assert "11111111-2222-3333-4444-555555555555" not in repr(plan)
assert "11111111-2222-3333-4444-555555555555" not in str(plan.as_dict())
def test_stop_shadow_plan_requires_same_live_device_and_active_project() -> None: