feat(plugins): isolate device integrations

This commit is contained in:
DCCONSTRUCTIONS
2026-07-17 19:29:32 +03:00
parent f9ffb7bd1c
commit 24a47318f2
122 changed files with 3304 additions and 1892 deletions
@@ -69,7 +69,9 @@ firmware-specific phases remain inside the XGRIDS plugin adapter.
- 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/`.
- 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:
@@ -81,9 +83,11 @@ firmware-specific phases remain inside the XGRIDS plugin adapter.
- 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;
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.
@@ -91,8 +95,8 @@ until the host gains signed plugin bundles and a startup compatibility handshake
- 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 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
@@ -139,15 +143,14 @@ 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.
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
@@ -208,10 +208,14 @@ This milestone is additive:
- legacy `stream.start-live`, `stream.start-replay`, `stream.stop`, and viewer
settings actions remain compatibility shims for the proven UI and runtime;
- the XGRIDS backend still runs through the reviewed
`transitional-in-process` facade in `src/k1link`.
`transitional-in-process` facade in
`src/k1link/device_plugins/xgrids_k1`;
- every dispatcher and compatibility-route action now crosses immutable SDK
`RuntimeActionInvocation` and `RuntimeActionResult` validation.
SDK v0alpha2 contracts do not imply that every current host endpoint, frontend
state object, or compatibility shim already emits those contracts.
SDK v0alpha2 runtime-action contracts are active in the backend hot path. This
does not imply that frontend state objects, live spatial frames or stored raw
evidence already use every SDK contract.
## Deliberately deferred
@@ -87,5 +87,5 @@ ADR 0008.
- `apps/control-station/src/components/ObservationSources.tsx`
- `apps/control-station/src/components/ObservationTimeline.tsx`
- `apps/control-station/src/workspaces/Workspaces.tsx`
- `apps/control-station/src/device-plugins/xgrids-k1/observationSources.ts`
- `src/k1link/web/xgrids_k1_camera.py`
- `plugins/xgrids-k1/frontend/src/observationSources.ts`
- `src/k1link/device_plugins/xgrids_k1/camera.py`
@@ -89,11 +89,11 @@ timeline once when the recording opens and does not poll or force the cursor.
## Code anchors
- `src/k1link/web/xgrids_k1_camera.py`
- `src/k1link/device_plugins/xgrids_k1/camera.py`
- `src/k1link/web/camera_archive.py`
- `src/k1link/web/xgrids_k1_facade.py`
- `src/k1link/device_plugins/xgrids_k1/facade.py`
- `apps/control-station/src/components/MseFmp4WebSocketPlayer.tsx`
- `apps/control-station/src/components/RecordedFmp4Player.tsx`
- `apps/control-station/src/core/observation/useObservationLayout.ts`
- `apps/control-station/src/device-plugins/xgrids-k1/observationSources.ts`
- `plugins/xgrids-k1/frontend/src/observationSources.ts`
- `tests/test_xgrids_camera_gateway.py`
@@ -134,8 +134,10 @@ The derived RRD writes an actual zero-time anchor at the internal
`/__mission_core/session_origin` entity. Keeping it outside `/world` prevents a
synthetic visualization layer while ensuring that the decoded RRD timeline,
not merely its summary document, begins at session time zero. This changes the
derived payload contract: cache v6 rejects v4/v5 sidecars and performs one
background rebuild instead of silently reusing an archive without that row.
derived payload contract. Cache v7 keeps that anchor and additionally binds the
derived recording to the owning plugin ID, primary artifact and ordered source
artifact set; it rejects v6 and older sidecars and performs one background
rebuild.
Replay v2 exposes two paths to the same pinned generation. `source_url` retains
the strict `If-Match: "sha256:…"` contract for Mission Core clients;
@@ -0,0 +1,93 @@
# ADR 0009: device-plugin observation runtime and K1 extraction
- Status: accepted
- Date: 2026-07-17
- Extends: ADR 0003, ADR 0004 and ADR 0008
## Context
The manifest/action boundary isolated the browser shell, but the host observation
service still imported K1 archive discovery, `.k1mqtt` preparation, MQTT timing
metadata and the K1 normalizer. A second sensor could not provide durable replay
without adding vendor branches to `SessionStore`, `SessionRecordingMaterializer`
and `app.py`.
## Decision
Every installed device plugin may contribute an optional observation runtime:
- one or more `ObservationArchiveSource` values with a plugin ID, archive ID,
confined root, discovery callback and recovery callback;
- exactly one recording exporter for that plugin ID;
- generic `ObservationSessionCandidate`, `SessionSource` and
`ObservationArtifactCandidate` values returned to the host.
The host owns SQLite lifecycle, path confinement, preparation scheduling,
cross-process cache locking, atomic publication, RRD delivery, recorded-media
admission and browser APIs. A plugin owns native evidence discovery, transport
format validation, recovery semantics, timeline-origin extraction and conversion
of its primary artifact into the canonical recorded Rerun artifact.
`ReplayCommand` is now vendor-neutral. It contains an owning `plugin_id`, an
opaque primary artifact ID, an ordered tuple of confined artifacts and a ready
timeline origin. It contains no filename suffix, MQTT topic or sidecar name.
The materializer selects the exporter by `plugin_id` and stages every validated
artifact prefix while preserving only its plugin-owned basename.
Derived RRD cache v7 binds the published RRD to:
- plugin ID and primary artifact ID;
- ordered artifact IDs and media types;
- full file identity and replay boundary for every artifact;
- per-artifact prefix digest;
- derived recording digest and zero-based timeline bounds.
Cache v6 and older sidecars are rebuilt once. The native evidence is not
rewritten.
The concrete implementation is physically located below:
```text
src/k1link/device_plugins/xgrids_k1/
ble/ mqtt/ protocol/ analyze/ net/ usb/
viewer/
facade.py camera.py archive.py observation.py rrd_export.py cli.py
```
The manifest entrypoint is
`k1link.device_plugins.xgrids_k1.facade:build_xgrids_k1_plugin`. Generic
`sessions`, `web/app.py`, `web/session_api.py` and the Rerun consumer do not
import this package or contain K1 formats, topics or identifiers.
Plugin SDK v0alpha2 additionally defines immutable
`RuntimeActionInvocation` and `RuntimeActionResult`. The host dispatcher creates
and validates them for every plugin action, including legacy compatibility
routes, before returning the path-free output document.
## Acceptance gate
The automated gate builds an unrelated synthetic spatial-sensor plugin which:
1. discovers its own native evidence extension;
2. reconciles into the unchanged host SQLite catalog;
3. produces a generic `ReplayCommand`;
4. materializes and reuses an RRD through its plugin-keyed exporter.
A dependency test also fails if generic observation hot-path files regain K1,
XGRIDS, topic, native-format or concrete-plugin imports. Adding a second device
therefore requires a manifest/runtime contribution and a plugin-owned reviewed
UI contribution, not edits to the session API, preparation queue, Rerun consumer
or main application shell. ADR 0010 defines that frontend boundary.
## Consequences
The semantic and physical vendor boundary is complete for the current
single-process architecture. Existing K1 raw evidence, camera archives and
operator workflows remain compatible. The `k1link` distribution name and CLI
command remain transitional compatibility names; their implementation is now
plugin-owned.
This ADR does not claim process isolation, crash containment, a durable operation
journal, signed plugin bundles, multi-device concurrency, portable SDK stream
envelopes or SDK `EvidenceStore` persistence. Those require a supervisor and a
new physical regression gate; they must not be inferred from module extraction.
@@ -0,0 +1,86 @@
# ADR 0010: plugin-owned frontend device workflows
- Status: accepted
- Date: 2026-07-17
- Extends: ADR 0003, ADR 0004 and ADR 0009
## Context
The device UI registry already mounted a model-specific `device.connection`
component, but the XGRIDS implementation still lived below
`apps/control-station/src/device-plugins`. Its 600-line connection view mixed
BLE discovery, Wi-Fi provisioning, acquisition/replay controls, diagnostics and
metrics. The boundary was logical, yet the plugin source and styles were still
physically owned by the generic application tree.
Different device families need different enrollment, connection and acquisition
workflows. Making those workflows generic would either produce a lowest-common-
denominator wizard or restore vendor branches in the Control Station.
## Decision
Device-specific operator workflows are frontend plugin contributions. The
XGRIDS source, API adapter, runtime provider, observation mapping and scoped CSS
are physically located under:
```text
plugins/xgrids-k1/frontend/src/
plugin.ts
XgridsK1Connection.tsx
components/
K1ProvisioningPipeline.tsx
K1AcquisitionPipeline.tsx
K1Diagnostics.tsx
K1Metrics.tsx
```
The generic host exposes one reviewed frontend surface through
`@mission-core/plugin-sdk`, implemented by
`apps/control-station/src/core/device-plugins/frontendSdk.ts`. A plugin may use
manifest/model/slot contracts, the generic Mission Runtime bridge, React and the
NODE.DC UI kit. It may not import Control Station implementation paths.
`apps/control-station/src/composition/devicePlugins.ts` is the only Core file
which imports the concrete XGRIDS frontend. The host owns model selection,
safe deactivation, the `device.connection` slot, navigation, the spatial scene,
saved-session UX and global layout. The plugin owns BLE candidates, exact-profile
attestation, credentials-in-memory form state, K1 endpoint state, connection
instructions and the live/replay acquisition controls.
Plugin CSS is imported with the contribution and scoped below
`.xgrids-k1-plugin`. A plugin cannot add global navigation, route ownership or
unscoped global selectors.
Frontend loading remains static and reviewed at build time. This ADR does not
introduce arbitrary remote JavaScript, an npm marketplace, signatures or
process/browser-realm isolation.
## Acceptance gate
The frontend boundary test builds two unrelated synthetic device plugins with
different connection components and proves that the unchanged registry resolves
the correct component for each model. It also fails when:
- XGRIDS implementation files return to the generic Control Station source;
- generic Core files other than the composition root contain XGRIDS/Lixel/K1
implementation knowledge;
- plugin source imports Control Station implementation paths;
- provisioning, acquisition, diagnostics or scoped-style modules disappear.
The accepted local gate is 114 frontend unit tests, TypeScript strict checking
and a Vite production build. Backend behavior and the physical K1 protocol are
unchanged by this source/UI decomposition.
## Consequences
A second device family can provide a completely different connection pipeline
and UI while reusing the same model catalog, lifecycle transition, observation
scene and saved-session host. Adding it requires its own plugin-owned frontend
contribution and one reviewed composition import, not edits to `App`,
`DeviceWorkspace` or generic runtime state.
The remaining isolation gap is deployment-level: frontend contributions are
still compiled into one signed application build, and backend plugins still run
inside the control-plane process. Signed bundles, compatibility handshake,
dynamic loading policy and process/restart containment belong to the supervisor
milestone.