NODEDC_MISSION_CORE/docs/adr/0003-device-plugin-ui-and-r...

7.2 KiB

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:

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:

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:

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.