# 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.