feat: introduce device plugin runtime boundary
This commit is contained in:
@@ -42,7 +42,8 @@ The rename does not alter these proven interfaces:
|
||||
- native `.k1mqtt` framing and `K1MQTT` magic;
|
||||
- provisioning profile `xgrids-k1-fw3-wifi-v1`;
|
||||
- K1 MQTT topics and firmware-scoped protobuf/LZ4 codecs;
|
||||
- current `k1_ip`, `K1Metrics`, and `useK1Console` compatibility fields;
|
||||
- wire-level `k1_ip` and metric values (their frontend type/hook names may move
|
||||
behind the plugin boundary; see ADR 0003);
|
||||
- schema v1 `$id`, which remains an immutable legacy identifier.
|
||||
|
||||
Those names move or change only during the plugin-extraction milestone with
|
||||
@@ -55,5 +56,8 @@ explicit migration and regression tests.
|
||||
- The existing K1 implementation remains operational while contracts are
|
||||
extracted incrementally.
|
||||
- Core must eventually boot without the XGRIDS plugin.
|
||||
- Plugin failure must not terminate the Mission Core host or raw evidence plane.
|
||||
- An invalid installed manifest, missing factory, or manifest/runtime drift fails
|
||||
startup before the host accepts traffic. After successful startup, an action
|
||||
failure is isolated to that namespaced plugin request and must not terminate
|
||||
the Mission Core host or raw evidence plane.
|
||||
- A future repository split remains possible after Plugin SDK v1 is stable.
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
# ADR 0003: device plugin UI and runtime boundary
|
||||
|
||||
- Status: accepted
|
||||
- Date: 2026-07-16
|
||||
|
||||
## Context
|
||||
|
||||
The verified XGRIDS K1 workflow originally lived directly in the Mission Core
|
||||
React shell and local FastAPI service. That preserved experimental speed, but it
|
||||
made BLE, Wi-Fi, K1 state fields, and stream actions look like platform concepts.
|
||||
Adding a second device that way would spread vendor branches through navigation,
|
||||
runtime state, and visualization.
|
||||
|
||||
## Decision
|
||||
|
||||
Mission Core uses an allowlisted device-plugin registry. Each installed plugin
|
||||
provides a versioned manifest, exactly one device model in v1alpha1, a runtime
|
||||
provider, and UI contributions for declared host slots. The one-model limit
|
||||
matches the current one-runtime-adapter lifecycle; multi-model plugins require
|
||||
explicit device-session and model routing in the next SDK version.
|
||||
|
||||
The only import of a concrete device plugin from platform code is the composition
|
||||
root `apps/control-station/src/composition/devicePlugins.ts`. Shipping another
|
||||
frontend plugin therefore requires one reviewed composition-root import; `App`,
|
||||
navigation, workspaces, and generic runtime code do not change. Runtime discovery
|
||||
of arbitrary JavaScript is deliberately not supported.
|
||||
|
||||
Backend factories are loaded only from validated, repository-local manifests.
|
||||
Startup fails unless every catalog manifest produces exactly one adapter with
|
||||
the same plugin ID and action set. The catalog is then frozen for the life of
|
||||
the process, so a backend-catalog model cannot appear without an executable
|
||||
runtime. Frontend availability remains the separate reviewed static-composition
|
||||
step described below.
|
||||
|
||||
The local-device UX is:
|
||||
|
||||
```text
|
||||
no model selected
|
||||
-> model catalog from registered manifests
|
||||
-> operator selects a model
|
||||
-> host mounts that plugin's device.connection contribution
|
||||
-> plugin performs its own setup and stream workflow
|
||||
-> plugin normalizes status, source, and metrics into MissionRuntimeState
|
||||
```
|
||||
|
||||
Provider shells remain structurally mounted to keep the Mission Core React tree
|
||||
stable, but their contract requires them to be inert while `activeModel` is
|
||||
`null`. Before a model is selected, no device API is polled and no discovery,
|
||||
credentials, stream controls, or metrics are shown. The selected custom view is
|
||||
mounted only for its manifest `componentKey`.
|
||||
|
||||
Changing the model is an asynchronous lifecycle transition. The active plugin
|
||||
must confirm deactivation; the XGRIDS implementation always issues `stream.stop`
|
||||
before its UI is removed. A failed or still-running teardown prevents the switch.
|
||||
|
||||
The shared runtime envelope contains only:
|
||||
|
||||
- generic lifecycle phase and message;
|
||||
- active device identity and an opaque endpoint label;
|
||||
- selected spatial source;
|
||||
- generic stream mode and metrics;
|
||||
- viewer settings.
|
||||
|
||||
Vendor fields such as `k1_ip`, `likely_k1`, BLE candidates, MQTT topics, and
|
||||
firmware-specific phases remain inside the XGRIDS plugin adapter.
|
||||
|
||||
## Current implementation
|
||||
|
||||
- Canonical manifest: `plugins/xgrids-k1/plugin.manifest.json`.
|
||||
- Host contracts and registry: `apps/control-station/src/core/device-plugins/`.
|
||||
- Generic runtime envelope: `apps/control-station/src/core/runtime/`.
|
||||
- XGRIDS UI/runtime adapter: `apps/control-station/src/device-plugins/xgrids-k1/`.
|
||||
- Read-only backend catalog: `GET /api/v1/device-plugins` and
|
||||
`GET /api/v1/device-models`.
|
||||
- Host-owned action dispatcher:
|
||||
`POST /api/v1/device-plugins/{pluginId}/actions/{actionId}`.
|
||||
- Plugin-scoped event stream:
|
||||
`WS /api/v1/device-plugins/{pluginId}/events`, with plugin ID and sequence.
|
||||
- Manifest factory composition:
|
||||
`src/k1link/web/device_plugin_composition.py`.
|
||||
- Existing K1 action endpoints are compatibility shims over that dispatcher, so
|
||||
the proven physical implementation is unchanged.
|
||||
|
||||
The XGRIDS frontend code is statically linked into the Control Station during
|
||||
this v1alpha milestone. It is isolated by imports and contracts but is not yet a
|
||||
separately built npm workspace package. Backend composition is manifest-driven;
|
||||
frontend/backend installed-set parity is a reviewed build-time responsibility
|
||||
until the host gains signed plugin bundles and a startup compatibility handshake.
|
||||
|
||||
## Required dependency rules
|
||||
|
||||
- Core cannot import a concrete plugin outside the composition root.
|
||||
- Core cannot inspect opaque plugin state or branch on a model ID.
|
||||
- Plugins render only inside declared slots and cannot own global navigation or
|
||||
global CSS.
|
||||
- Plugins publish canonical spatial sources; they do not control the scene
|
||||
engine or route names.
|
||||
- Passwords and other secrets cannot enter manifests, runtime snapshots, event
|
||||
payloads, or browser persistence.
|
||||
- Invalid or duplicate plugin/model IDs fail registry construction.
|
||||
- Manifest/runtime ID or action drift fails backend startup.
|
||||
- Every v1alpha1 plugin declares exactly one model and a non-mutating,
|
||||
secret-free `state.read` action.
|
||||
- A plugin must stop or cancel active work before model selection can change.
|
||||
|
||||
Manifest `permissions`, action `mutating`, and `secretFields` are validated and
|
||||
exposed as declarative contract metadata in v1alpha1. They do not yet implement
|
||||
operator authorization, policy enforcement, or secret-vault substitution; the
|
||||
dispatcher enforces only installed plugin IDs and declared action IDs. Until the
|
||||
next SDK milestone, plugin adapters remain responsible for request validation
|
||||
and for keeping secret values out of state, events, logs, and browser storage.
|
||||
|
||||
## Lifecycle model
|
||||
|
||||
Enrollment, connectivity, and streaming are the target independent state
|
||||
machines. The v1alpha shared runtime currently exposes an aggregate `phase`
|
||||
plus `sourceMode`; plugins must keep their detailed state private until the
|
||||
three fields are added to the stable SDK:
|
||||
|
||||
```text
|
||||
Enrollment: empty -> selecting_model -> setup_in_progress -> enrolled
|
||||
Connectivity: unknown/offline -> connecting -> connected -> degraded/offline
|
||||
Stream: idle -> starting -> streaming -> stopping -> idle
|
||||
\-> failed -> idle
|
||||
```
|
||||
|
||||
The current XGRIDS contribution maps its proven internal workflow into those
|
||||
platform states without changing the wire protocol:
|
||||
|
||||
```text
|
||||
confirm power -> scan BLE -> select candidate -> enter Wi-Fi
|
||||
-> provision once -> receive LAN address -> start source
|
||||
-> wait for first point frame -> streaming
|
||||
```
|
||||
|
||||
## Consequences and next extraction
|
||||
|
||||
The shell can now boot with no selected device and contains no K1 wire fields.
|
||||
Adding another statically reviewed UI plugin does not require changing `App` or
|
||||
the generic local-device workspace.
|
||||
|
||||
Backend execution supports only reviewed `transitional-in-process` plugins in
|
||||
v1alpha1, but already enters through the manifest
|
||||
factory, allowlisted XGRIDS facade and host-owned dispatcher. Synchronous
|
||||
capture/runtime work is moved off the API event loop. The next milestone adds
|
||||
independent device-session IDs, operation IDs, cancellation, timeouts, audited
|
||||
secret references, and process isolation. Only after replay parity and a
|
||||
physical regression may BLE, MQTT, codecs, and raw evidence code move out of
|
||||
the compatibility package. Rerun must ultimately consume canonical
|
||||
PointCloud/Pose envelopes instead of K1 topics.
|
||||
|
||||
This ADR supersedes the compatibility names listed in ADR 0002: `K1Metrics`,
|
||||
`useK1Console`, and `ConsoleService` were renamed and isolated inside the
|
||||
XGRIDS adapter without changing the proven transport algorithms.
|
||||
Reference in New Issue
Block a user