feat(plugins): isolate device integrations
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user