fix(k1): record redacted acceptance failures

This commit is contained in:
DCCONSTRUCTIONS 2026-07-18 16:11:29 +03:00
parent eddc09008e
commit a29b38a0d7
6 changed files with 187 additions and 18 deletions

View File

@ -157,8 +157,11 @@ response subscriptions, QoS2 completion and the five response-gated batches.
It consumes operation keys before publish and poisons unknown outcomes without
retry. An interactive Keychain administration command provisions through
Apple's hidden prompt without putting the authority in argv. Neither component
is wired to facade/UI; `vendor_writes_enabled` remains false and no application
command has been sent.
is wired to facade/UI; `vendor_writes_enabled` remains false. The first
operator-present physical attempt on 2026-07-18 emitted only the first six
bootstrap requests, then failed closed while correlating the third response
batch. The K1 remained `READY`; the `ModelingRequest` START was never emitted.
The temporary laboratory Keychain item was deleted after the attempt.
This locked bootstrap is repeatable in the current workspace, not yet a
standalone release install. The frontend consumes sibling `file:` packages from

View File

@ -21,7 +21,7 @@ Each gate produces evidence and an explicit GO, PAUSE or BLOCKED result.
| Stage 7 observation archive | GO (point/pose/telemetry contract) — durable catalog, recovery, capture-clock-bounded RRD preparation, archived metric time series, saved-session timeline and atomic playback are implemented |
| Stage 7 recorded cameras | GO (contract), acceptance pending — acquisition-owned fMP4 archive and player are implemented/tested; one real archived K1 camera plus point-cloud session has not passed playback yet |
| Plugin isolation | GO (laboratory control plane) — vendor backend/frontend and optional scene controls are plugin-owned; manifest/runtime descriptor parity, versioned handshake, lifecycle health and transport correlation fail closed while execution remains in-process |
| K1 application control | PAUSE (physical-acceptance software gate complete) — plugin v0.4.0 runtime remains dormant/write-disabled; a separate uninstalled transport now enforces the retained MQTT session, exact response subscriptions, QoS2 one-shot operation keys, five response-gated batches and poison-on-unknown behavior; Keychain provisioning and physical START/STOP remain unexecuted |
| K1 application control | PAUSE (first physical bootstrap failed closed before START) — plugin v0.4.0 runtime remains dormant/write-disabled; the uninstalled acceptance transport completed one connection, six QoS2 bootstrap publishes and five correlated responses, then rejected one live batch-3 response; K1 remained READY, START was not emitted, retry is forbidden and the temporary Keychain item was deleted |
| Stage 8 product storage | PAUSE — retention, replication, encryption, capacity monitoring and long-run browser/WASM stress remain deployment gates |
USB project copying remains optional ground truth rather than a blocker for the
@ -53,8 +53,15 @@ Each operation key is consumed before publish, automatic reconnect and
application retry are forbidden, and any unknown outcome poisons the transport.
The one-time `k1link authority provision` command delegates secret entry to the
macOS Keychain TTY prompt and validates through the production loader without
receiving the value in argv/environment/file/browser state. The command has not
been run. Physical START/STOP remain untested; the button is the safe fallback.
receiving the value in argv/environment/file/browser state. During the first
physical attempt that CLI loader did not receive macOS approval, so a temporary
process-local Security.framework lab adapter supplied the already reviewed
value. The transport emitted bootstrap ordinals 16 and received all five
required responses through batch 3, but the orchestrator raised
`ApplicationBootstrapError` while correlating that batch. No START was emitted;
live DeviceStatus stayed READY and the local point/pose counts remained zero.
The local capture was sealed and the temporary Keychain item was deleted.
Physical START/STOP therefore remain untested; the button is the safe fallback.
The Stage 6 live path uses a bounded raw-first bridge: loss in the visualization
queue cannot discard MQTT evidence. The queue holds four preview messages;
@ -273,7 +280,8 @@ useful stream is decoded or structurally identified.
- firmware or internal-Linux analysis;
- physical end-to-end shared-timeline playback for a newly archived session
containing point cloud plus one selected K1 camera;
- operator-owned Keychain item provisioning, operator-present physical use of
the reviewed acceptance transport and stable-artifact proof after STOP;
- OS-independent plugin/edge authority provider, redacted live-response
diagnostics for bootstrap batch 3, a newly permitted operator-present full
START/STOP attempt and stable-artifact proof after STOP;
- long-running large-session WebViewer/WASM memory telemetry;
- production retention, replication, encryption and cross-platform packaging.

View File

@ -225,13 +225,25 @@ post-publish outcome. A 15120 second permit requires explicit operator-presen
owner-controlled-device, LixelGO-closed, battery/storage and expected-state
confirmations. START additionally requires the complete five-batch bootstrap;
STOP requires its own separate permit. This code is not installed as a plugin
action and has never emitted to the K1.
action. On 2026-07-18 its first operator-present physical attempt made one
connection and emitted bootstrap ordinals 16. All six QoS 2 publishes completed
and five required responses were collected, but exact correlation rejected one
response in batch 3. The executor stopped before building or publishing START;
K1 remained `READY`, project binding stayed absent and point/pose remained zero.
No automatic retry was attempted.
The one-time Keychain administration command uses `security -w` as the final
argument so Apple's own TTY prompt receives the authority. Mission Core never
accepts it through CLI argv and validates the resulting fixed item through the
production loader. The command has not yet been run. The complete
stop/save sequence has not physically proved a durable vendor project. A
production loader. The macOS CLI approval path timed out during the first lab
attempt; a temporary Security.framework-backed item was used only inside the
one-shot process and deleted immediately afterward. It is not the product
authority architecture. The acceptance executor now records only ordinal,
message type, response topic, payload size, SHA-256 and a safe correlation
reason; it never exposes response bytes or authority in diagnostics. The
retained clean-cycle still completes all nine bootstrap barriers offline, so
the open issue is a live batch-3 response divergence. The complete stop/save
sequence has not physically proved a durable vendor project. A
correlated STOP success, stream quiescence, return to
`READY`/steady-green hardware state and appearance of a reusable native project
are separate gates. The verified physical double-click remains the acquisition

View File

@ -87,8 +87,12 @@ coordinator has no request-emission method, no live MQTT sink and no UI control;
transport now implements exact subscriptions, QoS2 completion, response
barriers and no-retry poisoning. The admin CLI can provision the fixed Keychain
item through Apple's hidden prompt without accepting the value as an argument.
Neither path is imported by the facade; Keychain provisioning and a controlled
physical START/STOP acceptance are still required.
Neither path is imported by the facade. The first physical attempt emitted only
bootstrap ordinals 16 and failed closed on live batch-3 response correlation;
START was not emitted and the K1 remained READY. The temporary lab Keychain item
was deleted. An OS-independent plugin/edge authority provider, redacted live
response comparison and a newly permitted full START/STOP acceptance remain
required.
The optional owner-controlled iPhone/LixelGO observation tool lives under
[`lab/iphone-capture/`](lab/iphone-capture/). It pins `pymobiledevice3` in a

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import hashlib
import math
import threading
import time
@ -8,6 +9,7 @@ from dataclasses import dataclass
from typing import Literal, Protocol
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
ApplicationBootstrapError,
LiveDeviceControlBinding,
ShadowApplicationBootstrapOrchestrator,
)
@ -19,6 +21,7 @@ from k1link.device_plugins.xgrids_k1.protocol.application_publish import (
)
from k1link.device_plugins.xgrids_k1.protocol.modeling_control import (
ModelingAction,
ModelingProtocolError,
ModelingResponse,
correlate_modeling_response,
)
@ -130,6 +133,8 @@ class PhysicalAcceptanceDialogueExecutor:
self._permit = permit
self._bootstrap_complete = False
self._command_complete = False
self._response_evidence: list[dict[str, object]] = []
self._correlation_failure: dict[str, object] | None = None
def run_bootstrap(
self,
@ -151,10 +156,30 @@ class PhysicalAcceptanceDialogueExecutor:
)
for request in batch:
if request.response_required:
orchestrator.accept_response(
request.response_topic,
responses[request.response_topic],
payload = responses[request.response_topic]
self._record_response_evidence(
phase="bootstrap",
operation_key=(
f"bootstrap:{request.ordinal}:{request.message_type}"
),
response_topic=request.response_topic,
payload=payload,
)
try:
orchestrator.accept_response(request.response_topic, payload)
except ApplicationBootstrapError as exc:
self._record_correlation_failure(
phase="bootstrap",
operation_key=(
f"bootstrap:{request.ordinal}:{request.message_type}"
),
response_topic=request.response_topic,
reason=str(exc),
)
raise ApplicationAcceptanceError(
"bootstrap response correlation failed for "
f"ordinal {request.ordinal} ({request.message_type}): {exc}"
) from exc
binding = orchestrator.binding
if binding is None:
@ -173,10 +198,25 @@ class PhysicalAcceptanceDialogueExecutor:
[OneShotPublishEnvelope.from_modeling_command(command)],
required_response_topics={MODELING_RESPONSE_TOPIC},
)
return correlate_modeling_response(
responses[MODELING_RESPONSE_TOPIC],
command.command,
payload = responses[MODELING_RESPONSE_TOPIC]
self._record_response_evidence(
phase="modeling",
operation_key=f"modeling:{command.action.name.casefold()}",
response_topic=MODELING_RESPONSE_TOPIC,
payload=payload,
)
try:
return correlate_modeling_response(payload, command.command)
except ModelingProtocolError as exc:
self._record_correlation_failure(
phase="modeling",
operation_key=f"modeling:{command.action.name.casefold()}",
response_topic=MODELING_RESPONSE_TOPIC,
reason=str(exc),
)
raise ApplicationAcceptanceError(
f"modeling response correlation failed for {command.action.name}: {exc}"
) from exc
def snapshot(self) -> dict[str, object]:
return {
@ -184,5 +224,44 @@ class PhysicalAcceptanceDialogueExecutor:
"bootstrap_complete": self._bootstrap_complete,
"command_complete": self._command_complete,
"permit": self._permit.snapshot(),
"response_evidence": tuple(dict(item) for item in self._response_evidence),
"correlation_failure": (
dict(self._correlation_failure)
if self._correlation_failure is not None
else None
),
"automatic_retry": False,
}
def _record_response_evidence(
self,
*,
phase: Literal["bootstrap", "modeling"],
operation_key: str,
response_topic: str,
payload: bytes,
) -> None:
self._response_evidence.append(
{
"phase": phase,
"operation_key": operation_key,
"response_topic": response_topic,
"payload_bytes": len(payload),
"payload_sha256": hashlib.sha256(payload).hexdigest(),
}
)
def _record_correlation_failure(
self,
*,
phase: Literal["bootstrap", "modeling"],
operation_key: str,
response_topic: str,
reason: str,
) -> None:
self._correlation_failure = {
"phase": phase,
"operation_key": operation_key,
"response_topic": response_topic,
"reason": reason,
}

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import hashlib
from collections.abc import Collection, Sequence
import pytest
@ -11,6 +12,7 @@ from k1link.device_plugins.xgrids_k1.protocol.application_acceptance import (
PhysicalAcceptancePermit,
)
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
DEVICE_CONFIG_RESPONSE_TOPIC,
ApplicationControlAuthority,
ShadowApplicationBootstrapOrchestrator,
)
@ -186,10 +188,71 @@ def test_start_acceptance_requires_full_bootstrap_and_consumes_one_short_permit(
assert permit.snapshot()["consumed"] is True
assert executor.snapshot()["bootstrap_complete"] is True
assert executor.snapshot()["command_complete"] is True
response_evidence = executor.snapshot()["response_evidence"]
assert isinstance(response_evidence, tuple)
assert len(response_evidence) == 10
assert response_evidence[0]["operation_key"] == "bootstrap:1:DeviceInfoRequest"
assert response_evidence[-1]["operation_key"] == "modeling:start"
assert all("payload" not in item for item in response_evidence)
assert APPLICATION_KEY not in str(executor.snapshot())
assert VENDOR_DEVICE_ID not in str(executor.snapshot())
with pytest.raises(ApplicationAcceptanceError, match="already attempted"):
executor.execute_modeling(command)
def test_bootstrap_correlation_failure_records_only_redacted_response_evidence() -> None:
class CorruptDeviceConfigTransport(SyntheticAcceptanceTransport):
def exchange_batch_once(
self,
envelopes: Sequence[OneShotPublishEnvelope],
*,
required_response_topics: Collection[str],
) -> dict[str, bytes]:
responses = super().exchange_batch_once(
envelopes,
required_response_topics=required_response_topics,
)
if DEVICE_CONFIG_RESPONSE_TOPIC in responses:
responses[DEVICE_CONFIG_RESPONSE_TOPIC] = _generic_response(
f"{VENDOR_DEVICE_ID}:wrong-session"
)
return responses
executor = PhysicalAcceptanceDialogueExecutor(
CorruptDeviceConfigTransport(),
PhysicalAcceptancePermit(_checklist(ModelingAction.START)),
)
orchestrator = ShadowApplicationBootstrapOrchestrator(
ApplicationControlAuthority(openapi_key=APPLICATION_KEY),
epoch_seconds=1_752_680_000,
timezone_name="Europe/Moscow",
)
with pytest.raises(
ApplicationAcceptanceError,
match=r"ordinal 4 \(DeviceConfigRequest\).*session correlation",
):
executor.run_bootstrap(orchestrator)
snapshot = executor.snapshot()
failure = snapshot["correlation_failure"]
assert failure == {
"phase": "bootstrap",
"operation_key": "bootstrap:4:DeviceConfigRequest",
"response_topic": DEVICE_CONFIG_RESPONSE_TOPIC,
"reason": "application response session correlation failed",
}
evidence = snapshot["response_evidence"]
assert isinstance(evidence, tuple)
assert evidence[-1]["payload_sha256"] == hashlib.sha256(
_generic_response(f"{VENDOR_DEVICE_ID}:wrong-session")
).hexdigest()
assert evidence[-1]["payload_bytes"] > 0
assert all("payload" not in item for item in evidence)
assert APPLICATION_KEY not in str(snapshot)
assert VENDOR_DEVICE_ID not in str(snapshot)
def test_start_cannot_skip_bootstrap_and_stop_uses_a_separate_action_permit() -> None:
transport = SyntheticAcceptanceTransport()
start = PhysicalAcceptanceDialogueExecutor(