feat(k1): wire dormant control lease

This commit is contained in:
DCCONSTRUCTIONS
2026-07-18 14:37:02 +03:00
parent ea811ff370
commit a4accf0fb6
21 changed files with 704 additions and 38 deletions
+8
View File
@@ -31,6 +31,8 @@ The plugin owns:
- K1-specific operator instructions and compatibility tests.
- the scoped React `device.connection` contribution and its BLE/Wi-Fi and
acquisition pipeline UI.
- the dormant application-control facade boundary: explicit shadow arm/disarm,
a 15300 second Keychain-backed in-memory authority lease, and redacted state.
The plugin does not own:
@@ -79,6 +81,12 @@ device I/O with:
uv run python plugins/xgrids-k1/profile_loader.py
```
Plugin v0.4.0 does not widen device authority. Its application-control
coordinator has no request-emission method, no live MQTT sink and no UI control;
`vendor_writes_enabled` remains false. Keychain item provisioning and a separate
review of any future write-capable transport are still required before a
physical command can be considered.
The optional owner-controlled iPhone/LixelGO observation tool lives under
[`lab/iphone-capture/`](lab/iphone-capture/). It pins `pymobiledevice3` in a
separate `uv` environment, writes only ignored evidence sessions, and remains a
+2
View File
@@ -12,6 +12,8 @@ The contribution contains:
start/stop and compatibility file replay;
- plugin-local diagnostics, metrics, API state, lifecycle mapping,
observation-source mapping and scoped styles;
- typed v0.4.0 shadow application-control state/arm/disarm contracts. No button
is rendered because the plugin has no live command transport;
- `plugin.ts`, which binds the manifest `device.connection` component key to
the runtime provider and connection view.
+43
View File
@@ -82,6 +82,28 @@ export interface XgridsModelingControlSafety {
publisher_installed: false;
}
export interface XgridsApplicationControlExecution {
mode: "dormant-write-disabled";
state: "disarmed" | "armed-shadow-only" | "expired" | "closed";
lease?: {
state: "armed" | "expired" | "closed";
remaining_seconds: number;
authority_cached: boolean;
exportable: false;
} | null;
orchestrator?: Record<string, unknown> | null;
publisher: {
mode: "write-disabled";
denied_attempts: number;
transport_calls: 0;
vendor_writes_enabled: false;
publisher_armed: false;
automatic_retry: false;
};
live_transport_installed: false;
can_emit_requests: false;
}
export interface XgridsAcquisition {
schema_version?: string;
acquisition_id: string;
@@ -205,6 +227,7 @@ export interface XgridsK1State {
metrics?: XgridsK1Metrics;
compatibility?: XgridsCompatibilityState | null;
modeling_control_safety?: XgridsModelingControlSafety | null;
application_control_execution?: XgridsApplicationControlExecution | null;
device_ref?: XgridsDeviceRef | null;
device_session?: XgridsDeviceSession | null;
acquisition?: XgridsAcquisition | null;
@@ -306,6 +329,12 @@ export interface StopCameraPreviewRequest {
generation: number;
}
export interface ShadowApplicationControlArmRequest {
operator_confirmed: true;
lease_seconds?: number;
timezone_name: string;
}
export class ApiError extends Error {
readonly status: number;
@@ -453,6 +482,20 @@ export const xgridsK1Api = {
updateViewerSettings(body: ViewerSettings): Promise<XgridsK1State> {
return invokeState(xgridsK1Actions.viewerSettingsUpdate, body);
},
getShadowApplicationControlState(): Promise<XgridsK1State> {
return invokeState(xgridsK1Actions.applicationControlShadowState);
},
armShadowApplicationControl(
body: ShadowApplicationControlArmRequest,
): Promise<XgridsK1State> {
return invokeState(xgridsK1Actions.applicationControlShadowArm, body);
},
disarmShadowApplicationControl(): Promise<XgridsK1State> {
return invokeState(xgridsK1Actions.applicationControlShadowDisarm);
},
};
export type EventSocketStatus = "connecting" | "open" | "closed" | "error";
@@ -30,4 +30,16 @@ export const xgridsK1Actions = Object.freeze({
cameraPreviewSelect: requirePluginAction(xgridsK1Manifest, "camera.preview.select"),
cameraPreviewStop: requirePluginAction(xgridsK1Manifest, "camera.preview.stop"),
viewerSettingsUpdate: requirePluginAction(xgridsK1Manifest, "viewer.settings.update"),
applicationControlShadowState: requirePluginAction(
xgridsK1Manifest,
"application-control.shadow-state",
),
applicationControlShadowArm: requirePluginAction(
xgridsK1Manifest,
"application-control.shadow-arm",
),
applicationControlShadowDisarm: requirePluginAction(
xgridsK1Manifest,
"application-control.shadow-disarm",
),
});
+7 -3
View File
@@ -3,7 +3,7 @@
"kind": "DevicePlugin",
"metadata": {
"id": "nodedc.device.xgrids-lixelkity-k1",
"version": "0.3.0",
"version": "0.4.0",
"displayName": "XGRIDS K1 Integration"
},
"spec": {
@@ -25,7 +25,8 @@
"network.mqtt.subscribe-private-lan",
"network.rtsp.read-private-lan",
"media.publish-local-browser",
"evidence.write-session-artifacts"
"evidence.write-session-artifacts",
"credential.keychain.read-local"
],
"actions": [
{ "id": "state.read", "mutating": false, "secretFields": [] },
@@ -45,7 +46,10 @@
{ "id": "stream.stop", "mutating": true, "secretFields": [] },
{ "id": "camera.preview.select", "mutating": true, "secretFields": [] },
{ "id": "camera.preview.stop", "mutating": true, "secretFields": [] },
{ "id": "viewer.settings.update", "mutating": true, "secretFields": [] }
{ "id": "viewer.settings.update", "mutating": true, "secretFields": [] },
{ "id": "application-control.shadow-state", "mutating": false, "secretFields": [] },
{ "id": "application-control.shadow-arm", "mutating": true, "secretFields": [] },
{ "id": "application-control.shadow-disarm", "mutating": true, "secretFields": [] }
],
"models": [
{
+5 -4
View File
@@ -39,10 +39,11 @@ capture verifies the `ModelingRequest` topic, action values, field layout,
literal `{device_id}:ModelingRequest` session relation, retained start settings
and numeric success code. Retained client/wire evidence identifies OpenAPI as
one private application-level value rather than a per-scanner profile. A fixed
read-only macOS Keychain loader and offline response orchestrator now exist;
operator-owned provisioning/execution-lease wiring, durable save completion,
timeout and rollback behavior remain unresolved. Acquisition therefore
stays `operator-manual` through the verified physical double-click.
read-only macOS Keychain loader, bounded in-memory lease and dormant facade
orchestrator now exist. No request-emission method or live MQTT sink exists;
operator-owned Keychain item provisioning, reviewed emission, durable save
completion, timeout and rollback behavior remain unresolved. Acquisition
therefore stays `operator-manual` through the verified physical double-click.
The standalone encoder models the recovered wire schema, including enum values
outside the retained request. It is not an authorization policy: any future
publisher must enforce the exact profile mapping (`2/1/0`, omitted
@@ -359,7 +359,7 @@
},
"success_result_code": 302252033,
"required_unresolved_context": [
"operator-owned Keychain authority provisioning and execution-lease wiring",
"operator-owned Keychain item provisioning and reviewed request emission",
"authorization policy for any setting outside the retained request",
"timeout, rejection and rollback contract"
],
@@ -406,7 +406,7 @@
"request_fields": {},
"success_result_code": 302252033,
"required_unresolved_context": [
"operator-owned Keychain authority provisioning and execution-lease wiring",
"operator-owned Keychain item provisioning and reviewed request emission",
"save-completion and final-standby state mapping",
"timeout and rollback contract"
],