feat(plugins): add runtime handshake boundary

This commit is contained in:
DCCONSTRUCTIONS
2026-07-17 19:50:01 +03:00
parent 24a47318f2
commit 9d51080d2e
22 changed files with 856 additions and 138 deletions
@@ -26,11 +26,12 @@ navigation, workspaces, and generic runtime code do not change. Runtime discover
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.
Startup fails unless every catalog manifest produces exactly one runtime with
the same plugin ID, version, supported host API and action set. ADR 0011 adds a
versioned descriptor/handshake before the runtime becomes ready. 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:
@@ -76,20 +77,23 @@ firmware-specific phases remain inside the XGRIDS plugin adapter.
`GET /api/v1/device-models`.
- Host-owned action dispatcher:
`POST /api/v1/device-plugins/{pluginId}/actions/{actionId}`.
- Runtime lifecycle health:
`GET /api/v1/device-plugin-runtimes`.
- 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.
- Existing K1 action endpoints are compatibility shims over the same admitted
runtime transport, 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.
separately built or signed npm package. Backend composition is manifest-driven
and performs its control-plane handshake; frontend/backend installed-set parity
is still a reviewed build-time responsibility until the host gains signed
plugin bundles and frontend loading policy.
## Required dependency rules
@@ -102,7 +106,7 @@ until the host gains signed plugin bundles and a startup compatibility handshake
- 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.
- Manifest/runtime ID, version, host API 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.
@@ -217,6 +217,11 @@ 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.
ADR 0011 later adds runtime-owned descriptor, handshake and lifecycle-health
contracts plus a transport seam around this action path. Its current
implementation remains in-process and does not revise the device/acquisition
semantics accepted here.
## Deliberately deferred
The following are not complete and must not be inferred from this ADR:
@@ -87,6 +87,11 @@ operator workflows remain compatible. The `k1link` distribution name and CLI
command remain transitional compatibility names; their implementation is now
plugin-owned.
ADR 0011 subsequently places the action control plane behind a versioned
descriptor/handshake/health transport seam. Observation discovery and export
remain host-called plugin contributions and are not yet portable process
transport contracts.
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
@@ -81,6 +81,7 @@ contribution and one reviewed composition import, not edits to `App`,
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.
inside the control-plane process. ADR 0011 adds a fail-closed backend runtime
descriptor/handshake and replaceable control-plane transport seam, but not a
subprocess. Signed bundles, frontend loading/realm policy and process/restart
containment remain later deployment milestones.
@@ -0,0 +1,103 @@
# ADR 0011: laboratory plugin runtime handshake and transport seam
- Status: accepted
- Date: 2026-07-17
- Extends: ADR 0003, ADR 0004, ADR 0009 and ADR 0010
## Context
Backend and frontend vendor code are physically plugin-owned, but the backend
dispatcher still received a raw in-process action adapter. Manifest parity was
checked through Python attributes rather than a runtime-owned wire contract.
Replacing that adapter with a later subprocess transport would therefore have
changed the dispatcher and compatibility routes as well as composition.
Mission Core is still a development-source laboratory sandbox. The final Edge,
Control Station and equipment-composition architecture is not known. Complex
hardware packs will probably combine separately useful components, but there is
not yet enough evidence to define pack roles, lifecycle or compatibility
matrices. The next step must preserve the proven plugin boundary without
pretending that production supervision is already designed.
## Decision
The executable control plane gains one minimal, versioned runtime protocol:
```text
missioncore.nodedc/plugin-runtime/v0alpha1
```
Plugin SDK v0alpha2 defines closed immutable contracts for:
- `RuntimePluginDescriptor` — plugin ID/version, supported host APIs and the
exact action set owned by the runtime;
- `RuntimeHandshakeRequest` and `RuntimeHandshakeResult` — an activation
challenge correlated by handshake ID and runtime instance ID;
- `RuntimeHealthSnapshot` — lifecycle health only: `starting`, `ready` or
`stopped`;
- the existing `RuntimeActionInvocation` and `RuntimeActionResult` envelopes.
The generic host depends on `DevicePluginRuntimeTransport`, whose control-plane
surface is `handshake`, `health`, `invoke` and `close`. A plugin factory returns
this transport inside `DevicePluginRuntimeContribution`; it no longer exposes a
raw action adapter to composition or the dispatcher.
Before admitting a plugin, composition validates all of the following:
1. manifest ID equals runtime descriptor ID;
2. manifest version equals runtime descriptor version;
3. the manifest API is explicitly supported by the runtime;
4. manifest and runtime action sets are exactly equal;
5. handshake response correlates to the request and descriptor;
6. runtime lifecycle health is `ready`.
Any failure closes the runtime and aborts composition. The dispatcher addresses
only admitted transports and rejects an action result whose invocation, plugin
or action correlation differs from the request.
`InProcessDevicePluginRuntime` implements the protocol for the current lab. It
wraps the XGRIDS action adapter, performs no serialization and provides no crash
boundary. The deprecated XGRIDS action routes now use the same admitted
transport instead of invoking the adapter directly. The plugin-local camera
gateway remains a same-process data-plane route; portable stream/media IPC is a
separate future gate.
`GET /api/v1/device-plugin-runtimes` exposes the small lifecycle snapshots. The
main health response reports ready/total runtime counts. These values do not
claim device connectivity, sensor health, process containment or durable
supervision.
## Acceptance
The automated gate proves that:
- two unrelated synthetic plugin runtimes complete independent handshakes and
dispatch through the unchanged generic host;
- actions are rejected before successful activation;
- an unsupported host API fails closed without changing runtime health to
ready;
- uncorrelated action results are rejected;
- close is idempotent and lifecycle health becomes stopped;
- SDK handshake contracts remain closed, immutable and exported as JSON Schema.
## Consequences
The generic action path now has a transport-replaceable seam. A later
subprocess implementation can satisfy the same host protocol while moving
serialization, heartbeat, restart and resource policy behind the transport.
This is useful architectural order even while the product topology is still
being discovered.
This ADR does **not** implement or promise:
- a subprocess supervisor, crash containment, heartbeat timeout or restart;
- CPU, memory, device-permission or filesystem limits;
- dynamic plugin discovery, quarantine or hot reload;
- bundle signing, frontend realm isolation, installers or updates;
- durable operation recovery or portable stream/EvidenceStore transport;
- multi-device routing or hardware-pack composition semantics.
The plugin remains the smallest canonical vendor-isolation unit. Whether a
future hardware pack is a host-owned composition of component plugins, a
deployable profile or another domain object is intentionally deferred until
real additional equipment makes that choice testable.