feat(plugins): add runtime handshake boundary
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user