feat(plugins): add runtime handshake boundary
This commit is contained in:
@@ -36,8 +36,9 @@ The separate SDK v0alpha2 package establishes executable contracts for:
|
||||
- independently revisioned enrollment, connectivity, and acquisition states;
|
||||
- operation policy, request, acknowledgement, progress, completion, failure,
|
||||
timeout, cancellation, secret reference, and idempotency boundaries;
|
||||
- immutable pre-session runtime-action invocation and result envelopes used by
|
||||
the active backend dispatcher;
|
||||
- immutable runtime descriptor, activation handshake, lifecycle health,
|
||||
action-invocation and action-result envelopes used by the active backend
|
||||
dispatcher;
|
||||
- canonical point cloud, pose, image, encoded video, and device-status streams;
|
||||
- immutable evidence handles, raw transport records, lineage, and store
|
||||
protocol;
|
||||
@@ -57,18 +58,21 @@ Current host implementation references:
|
||||
that frontend surface;
|
||||
- `apps/control-station/src/core/runtime/` — normalized runtime envelope;
|
||||
- `src/k1link/web/plugin_catalog.py` — strict backend manifest validation;
|
||||
- `src/k1link/web/plugin_runtime.py` — host-owned allowlisted SDK-envelope action dispatcher;
|
||||
- `src/k1link/web/plugin_runtime.py` — transport protocol, laboratory in-process
|
||||
implementation and host-owned SDK-envelope action dispatcher;
|
||||
- `src/k1link/web/device_plugin_composition.py` — manifest factory loader and
|
||||
startup parity checks;
|
||||
- `docs/adr/0003-device-plugin-ui-and-runtime-boundary.md` — accepted boundary
|
||||
and extraction sequence.
|
||||
fail-closed descriptor/handshake checks;
|
||||
- `docs/adr/0011-laboratory-plugin-runtime-handshake-and-transport-seam.md` —
|
||||
accepted control-plane seam and its explicit laboratory limits.
|
||||
|
||||
The existing lifecycle is fail-closed: inactive provider shells perform no
|
||||
I/O, events are scoped by plugin ID, and selection cannot change until the
|
||||
active plugin confirms teardown. Process isolation remains a later host
|
||||
milestone. v0alpha2 models secret references and operation policy, but host
|
||||
authorization and the actual secret vault remain separate implementation
|
||||
responsibilities.
|
||||
The existing lifecycle is fail-closed: a backend runtime stays `starting` until
|
||||
its ID, version, host API and exact action set pass the handshake; inactive
|
||||
provider shells perform no I/O; events are scoped by plugin ID; and selection
|
||||
cannot change until the active plugin confirms teardown. `ready` health means
|
||||
only that this control-plane contract is admitted. Process isolation remains a
|
||||
later host milestone. v0alpha2 models secret references and operation policy,
|
||||
but host authorization and the actual secret vault remain separate
|
||||
implementation responsibilities.
|
||||
|
||||
The React surface is not yet an independently published npm package. Its alias
|
||||
is intentionally narrow so plugin source cannot import Control Station
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -47,7 +47,15 @@ from .operations import (
|
||||
validate_operation_request,
|
||||
)
|
||||
from .payloads import InlinePayload, PayloadHandle, ReferencedPayload
|
||||
from .runtime import RuntimeActionInvocation, RuntimeActionResult
|
||||
from .runtime import (
|
||||
RUNTIME_PROTOCOL_VERSION,
|
||||
RuntimeActionInvocation,
|
||||
RuntimeActionResult,
|
||||
RuntimeHandshakeRequest,
|
||||
RuntimeHandshakeResult,
|
||||
RuntimeHealthSnapshot,
|
||||
RuntimePluginDescriptor,
|
||||
)
|
||||
from .schema import contract_json_schemas
|
||||
from .session import (
|
||||
AcquisitionState,
|
||||
@@ -121,8 +129,13 @@ __all__ = [
|
||||
"Quaternion",
|
||||
"RawTransportRecord",
|
||||
"RedactionState",
|
||||
"RUNTIME_PROTOCOL_VERSION",
|
||||
"RuntimeActionInvocation",
|
||||
"RuntimeActionResult",
|
||||
"RuntimeHandshakeRequest",
|
||||
"RuntimeHandshakeResult",
|
||||
"RuntimeHealthSnapshot",
|
||||
"RuntimePluginDescriptor",
|
||||
"ReferencedPayload",
|
||||
"RuleOutcome",
|
||||
"SecretReference",
|
||||
|
||||
@@ -1,12 +1,92 @@
|
||||
"""Minimal pre-session action envelopes for the executable plugin runtime."""
|
||||
"""Portable control-plane envelopes for an executable plugin runtime."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import AwareDatetime, Field
|
||||
from pydantic import AwareDatetime, Field, model_validator
|
||||
|
||||
from .common import API_VERSION, ApiVersion, ContractModel, Identifier, JsonObject
|
||||
from .common import (
|
||||
API_VERSION,
|
||||
ApiVersion,
|
||||
ContractModel,
|
||||
Identifier,
|
||||
JsonObject,
|
||||
ShortText,
|
||||
)
|
||||
|
||||
RuntimeProtocolVersion = Literal["missioncore.nodedc/plugin-runtime/v0alpha1"]
|
||||
RUNTIME_PROTOCOL_VERSION: RuntimeProtocolVersion = (
|
||||
"missioncore.nodedc/plugin-runtime/v0alpha1"
|
||||
)
|
||||
|
||||
|
||||
class RuntimePluginDescriptor(ContractModel):
|
||||
"""Plugin-owned declaration used before the host admits any action."""
|
||||
|
||||
api_version: ApiVersion = API_VERSION
|
||||
kind: Literal["RuntimePluginDescriptor"] = "RuntimePluginDescriptor"
|
||||
runtime_protocol_version: RuntimeProtocolVersion = RUNTIME_PROTOCOL_VERSION
|
||||
plugin_id: Identifier
|
||||
plugin_version: ShortText
|
||||
supported_host_api_versions: tuple[Identifier, ...] = Field(min_length=1)
|
||||
action_ids: tuple[Identifier, ...] = Field(min_length=1)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def declarations_are_unique(self) -> RuntimePluginDescriptor:
|
||||
if len(self.supported_host_api_versions) != len(
|
||||
set(self.supported_host_api_versions)
|
||||
):
|
||||
raise ValueError("supported host API versions must be unique")
|
||||
if len(self.action_ids) != len(set(self.action_ids)):
|
||||
raise ValueError("runtime action ids must be unique")
|
||||
return self
|
||||
|
||||
|
||||
class RuntimeHandshakeRequest(ContractModel):
|
||||
"""Host compatibility challenge sent before a runtime becomes ready."""
|
||||
|
||||
api_version: ApiVersion = API_VERSION
|
||||
kind: Literal["RuntimeHandshakeRequest"] = "RuntimeHandshakeRequest"
|
||||
runtime_protocol_version: RuntimeProtocolVersion = RUNTIME_PROTOCOL_VERSION
|
||||
handshake_id: Identifier
|
||||
plugin_id: Identifier
|
||||
plugin_version: ShortText
|
||||
host_api_version: Identifier
|
||||
requested_at: AwareDatetime
|
||||
required_action_ids: tuple[Identifier, ...] = Field(min_length=1)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def required_actions_are_unique(self) -> RuntimeHandshakeRequest:
|
||||
if len(self.required_action_ids) != len(set(self.required_action_ids)):
|
||||
raise ValueError("required runtime action ids must be unique")
|
||||
return self
|
||||
|
||||
|
||||
class RuntimeHandshakeResult(ContractModel):
|
||||
"""Runtime response proving identity, protocol and admitted host API."""
|
||||
|
||||
api_version: ApiVersion = API_VERSION
|
||||
kind: Literal["RuntimeHandshakeResult"] = "RuntimeHandshakeResult"
|
||||
handshake_id: Identifier
|
||||
runtime_instance_id: Identifier
|
||||
accepted_host_api_version: Identifier
|
||||
descriptor: RuntimePluginDescriptor
|
||||
ready_at: AwareDatetime
|
||||
|
||||
|
||||
class RuntimeHealthSnapshot(ContractModel):
|
||||
"""Small transport-neutral liveness snapshot for laboratory supervision."""
|
||||
|
||||
api_version: ApiVersion = API_VERSION
|
||||
kind: Literal["RuntimeHealthSnapshot"] = "RuntimeHealthSnapshot"
|
||||
runtime_instance_id: Identifier
|
||||
plugin_id: Identifier
|
||||
plugin_version: ShortText
|
||||
runtime_protocol_version: RuntimeProtocolVersion = RUNTIME_PROTOCOL_VERSION
|
||||
status: Literal["starting", "ready", "stopped"]
|
||||
observed_at: AwareDatetime
|
||||
detail_code: Identifier | None = None
|
||||
|
||||
|
||||
class RuntimeActionInvocation(ContractModel):
|
||||
|
||||
@@ -8,7 +8,14 @@ from .compatibility import CompatibilityAssessment
|
||||
from .evidence import EvidenceRecord, RawTransportRecord
|
||||
from .identity import DeviceInstanceRef
|
||||
from .operations import OperationEvent, OperationPolicy, OperationRequest
|
||||
from .runtime import RuntimeActionInvocation, RuntimeActionResult
|
||||
from .runtime import (
|
||||
RuntimeActionInvocation,
|
||||
RuntimeActionResult,
|
||||
RuntimeHandshakeRequest,
|
||||
RuntimeHandshakeResult,
|
||||
RuntimeHealthSnapshot,
|
||||
RuntimePluginDescriptor,
|
||||
)
|
||||
from .session import DeviceSessionContext, DeviceSessionSnapshot
|
||||
from .streams import CanonicalStreamEnvelope
|
||||
|
||||
@@ -25,6 +32,10 @@ def contract_json_schemas() -> dict[str, dict[str, object]]:
|
||||
"OperationEvent": OperationEvent,
|
||||
"RuntimeActionInvocation": RuntimeActionInvocation,
|
||||
"RuntimeActionResult": RuntimeActionResult,
|
||||
"RuntimePluginDescriptor": RuntimePluginDescriptor,
|
||||
"RuntimeHandshakeRequest": RuntimeHandshakeRequest,
|
||||
"RuntimeHandshakeResult": RuntimeHandshakeResult,
|
||||
"RuntimeHealthSnapshot": RuntimeHealthSnapshot,
|
||||
"CanonicalStreamEnvelope": CanonicalStreamEnvelope,
|
||||
"EvidenceRecord": EvidenceRecord,
|
||||
"RawTransportRecord": RawTransportRecord,
|
||||
|
||||
Reference in New Issue
Block a user