fix(k1): record redacted acceptance failures
This commit is contained in:
parent
eddc09008e
commit
a29b38a0d7
|
|
@ -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
|
It consumes operation keys before publish and poisons unknown outcomes without
|
||||||
retry. An interactive Keychain administration command provisions through
|
retry. An interactive Keychain administration command provisions through
|
||||||
Apple's hidden prompt without putting the authority in argv. Neither component
|
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
|
is wired to facade/UI; `vendor_writes_enabled` remains false. The first
|
||||||
command has been sent.
|
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
|
This locked bootstrap is repeatable in the current workspace, not yet a
|
||||||
standalone release install. The frontend consumes sibling `file:` packages from
|
standalone release install. The frontend consumes sibling `file:` packages from
|
||||||
|
|
|
||||||
|
|
@ -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 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 |
|
| 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 |
|
| 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 |
|
| 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
|
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.
|
application retry are forbidden, and any unknown outcome poisons the transport.
|
||||||
The one-time `k1link authority provision` command delegates secret entry to the
|
The one-time `k1link authority provision` command delegates secret entry to the
|
||||||
macOS Keychain TTY prompt and validates through the production loader without
|
macOS Keychain TTY prompt and validates through the production loader without
|
||||||
receiving the value in argv/environment/file/browser state. The command has not
|
receiving the value in argv/environment/file/browser state. During the first
|
||||||
been run. Physical START/STOP remain untested; the button is the safe fallback.
|
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 1–6 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
|
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;
|
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;
|
- firmware or internal-Linux analysis;
|
||||||
- physical end-to-end shared-timeline playback for a newly archived session
|
- physical end-to-end shared-timeline playback for a newly archived session
|
||||||
containing point cloud plus one selected K1 camera;
|
containing point cloud plus one selected K1 camera;
|
||||||
- operator-owned Keychain item provisioning, operator-present physical use of
|
- OS-independent plugin/edge authority provider, redacted live-response
|
||||||
the reviewed acceptance transport and stable-artifact proof after STOP;
|
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;
|
- long-running large-session WebViewer/WASM memory telemetry;
|
||||||
- production retention, replication, encryption and cross-platform packaging.
|
- production retention, replication, encryption and cross-platform packaging.
|
||||||
|
|
|
||||||
|
|
@ -225,13 +225,25 @@ post-publish outcome. A 15–120 second permit requires explicit operator-presen
|
||||||
owner-controlled-device, LixelGO-closed, battery/storage and expected-state
|
owner-controlled-device, LixelGO-closed, battery/storage and expected-state
|
||||||
confirmations. START additionally requires the complete five-batch bootstrap;
|
confirmations. START additionally requires the complete five-batch bootstrap;
|
||||||
STOP requires its own separate permit. This code is not installed as a plugin
|
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 1–6. 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
|
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
|
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
|
accepts it through CLI argv and validates the resulting fixed item through the
|
||||||
production loader. The command has not yet been run. The complete
|
production loader. The macOS CLI approval path timed out during the first lab
|
||||||
stop/save sequence has not physically proved a durable vendor project. A
|
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
|
correlated STOP success, stream quiescence, return to
|
||||||
`READY`/steady-green hardware state and appearance of a reusable native project
|
`READY`/steady-green hardware state and appearance of a reusable native project
|
||||||
are separate gates. The verified physical double-click remains the acquisition
|
are separate gates. The verified physical double-click remains the acquisition
|
||||||
|
|
|
||||||
|
|
@ -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
|
transport now implements exact subscriptions, QoS2 completion, response
|
||||||
barriers and no-retry poisoning. The admin CLI can provision the fixed Keychain
|
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.
|
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
|
Neither path is imported by the facade. The first physical attempt emitted only
|
||||||
physical START/STOP acceptance are still required.
|
bootstrap ordinals 1–6 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
|
The optional owner-controlled iPhone/LixelGO observation tool lives under
|
||||||
[`lab/iphone-capture/`](lab/iphone-capture/). It pins `pymobiledevice3` in a
|
[`lab/iphone-capture/`](lab/iphone-capture/). It pins `pymobiledevice3` in a
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import math
|
import math
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
@ -8,6 +9,7 @@ from dataclasses import dataclass
|
||||||
from typing import Literal, Protocol
|
from typing import Literal, Protocol
|
||||||
|
|
||||||
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
|
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
|
||||||
|
ApplicationBootstrapError,
|
||||||
LiveDeviceControlBinding,
|
LiveDeviceControlBinding,
|
||||||
ShadowApplicationBootstrapOrchestrator,
|
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 (
|
from k1link.device_plugins.xgrids_k1.protocol.modeling_control import (
|
||||||
ModelingAction,
|
ModelingAction,
|
||||||
|
ModelingProtocolError,
|
||||||
ModelingResponse,
|
ModelingResponse,
|
||||||
correlate_modeling_response,
|
correlate_modeling_response,
|
||||||
)
|
)
|
||||||
|
|
@ -130,6 +133,8 @@ class PhysicalAcceptanceDialogueExecutor:
|
||||||
self._permit = permit
|
self._permit = permit
|
||||||
self._bootstrap_complete = False
|
self._bootstrap_complete = False
|
||||||
self._command_complete = False
|
self._command_complete = False
|
||||||
|
self._response_evidence: list[dict[str, object]] = []
|
||||||
|
self._correlation_failure: dict[str, object] | None = None
|
||||||
|
|
||||||
def run_bootstrap(
|
def run_bootstrap(
|
||||||
self,
|
self,
|
||||||
|
|
@ -151,10 +156,30 @@ class PhysicalAcceptanceDialogueExecutor:
|
||||||
)
|
)
|
||||||
for request in batch:
|
for request in batch:
|
||||||
if request.response_required:
|
if request.response_required:
|
||||||
orchestrator.accept_response(
|
payload = responses[request.response_topic]
|
||||||
request.response_topic,
|
self._record_response_evidence(
|
||||||
responses[request.response_topic],
|
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
|
binding = orchestrator.binding
|
||||||
if binding is None:
|
if binding is None:
|
||||||
|
|
@ -173,10 +198,25 @@ class PhysicalAcceptanceDialogueExecutor:
|
||||||
[OneShotPublishEnvelope.from_modeling_command(command)],
|
[OneShotPublishEnvelope.from_modeling_command(command)],
|
||||||
required_response_topics={MODELING_RESPONSE_TOPIC},
|
required_response_topics={MODELING_RESPONSE_TOPIC},
|
||||||
)
|
)
|
||||||
return correlate_modeling_response(
|
payload = responses[MODELING_RESPONSE_TOPIC]
|
||||||
responses[MODELING_RESPONSE_TOPIC],
|
self._record_response_evidence(
|
||||||
command.command,
|
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]:
|
def snapshot(self) -> dict[str, object]:
|
||||||
return {
|
return {
|
||||||
|
|
@ -184,5 +224,44 @@ class PhysicalAcceptanceDialogueExecutor:
|
||||||
"bootstrap_complete": self._bootstrap_complete,
|
"bootstrap_complete": self._bootstrap_complete,
|
||||||
"command_complete": self._command_complete,
|
"command_complete": self._command_complete,
|
||||||
"permit": self._permit.snapshot(),
|
"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,
|
"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,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
from collections.abc import Collection, Sequence
|
from collections.abc import Collection, Sequence
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -11,6 +12,7 @@ from k1link.device_plugins.xgrids_k1.protocol.application_acceptance import (
|
||||||
PhysicalAcceptancePermit,
|
PhysicalAcceptancePermit,
|
||||||
)
|
)
|
||||||
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
|
from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
|
||||||
|
DEVICE_CONFIG_RESPONSE_TOPIC,
|
||||||
ApplicationControlAuthority,
|
ApplicationControlAuthority,
|
||||||
ShadowApplicationBootstrapOrchestrator,
|
ShadowApplicationBootstrapOrchestrator,
|
||||||
)
|
)
|
||||||
|
|
@ -186,10 +188,71 @@ def test_start_acceptance_requires_full_bootstrap_and_consumes_one_short_permit(
|
||||||
assert permit.snapshot()["consumed"] is True
|
assert permit.snapshot()["consumed"] is True
|
||||||
assert executor.snapshot()["bootstrap_complete"] is True
|
assert executor.snapshot()["bootstrap_complete"] is True
|
||||||
assert executor.snapshot()["command_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"):
|
with pytest.raises(ApplicationAcceptanceError, match="already attempted"):
|
||||||
executor.execute_modeling(command)
|
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:
|
def test_start_cannot_skip_bootstrap_and_stop_uses_a_separate_action_permit() -> None:
|
||||||
transport = SyntheticAcceptanceTransport()
|
transport = SyntheticAcceptanceTransport()
|
||||||
start = PhysicalAcceptanceDialogueExecutor(
|
start = PhysicalAcceptanceDialogueExecutor(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue