158 lines
7.4 KiB
Markdown
158 lines
7.4 KiB
Markdown
# 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/`.
|
|
- Public frontend host surface:
|
|
`apps/control-station/src/core/device-plugins/frontendSdk.ts`.
|
|
- XGRIDS UI/runtime adapter: `plugins/xgrids-k1/frontend/src/`.
|
|
- 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 physically plugin-owned and statically linked into
|
|
the Control Station during this v1alpha milestone. Its provisioning,
|
|
acquisition/replay and diagnostics blocks are separate components, and it may
|
|
import the host only through `@mission-core/plugin-sdk`. It is not yet a
|
|
separately built or signed npm 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, cannot own global navigation, and
|
|
may ship only plugin-root-scoped 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 still supports only reviewed `transitional-in-process`
|
|
plugins, but enters through the manifest factory, immutable SDK runtime-action
|
|
envelopes, the allowlisted facade and host-owned dispatcher. Synchronous
|
|
capture/runtime work is moved off the API event loop. ADR 0009 completes the
|
|
physical BLE/MQTT/codec/evidence extraction and plugin-owned observation
|
|
contribution after replay parity and physical regression. The remaining
|
|
milestone is process isolation, durable operation supervision and portable SDK
|
|
stream/evidence transport; the Rerun consumer already contains no 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.
|