feat(plugin-runtime): add plugin isolation IPC infrastructure
- Protocol layer: Envelope model with Pydantic schema, MsgPack/JSON codecs, unified error codes - Transport layer: cross-platform IPC abstraction with 4-byte length-prefixed framing (UDS + TCP fallback) - Host: RPC server, policy engine, circuit breaker, capability service, supervisor with hot-reload - Runner: RPC client, plugin loader, process entry point - Tests: 16 passing tests covering protocol, transport, host, and E2E handshake
This commit is contained in:
1
src/plugin_runtime/protocol/__init__.py
Normal file
1
src/plugin_runtime/protocol/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Protocol 层 - RPC 消息模型、编解码、错误码
|
||||
80
src/plugin_runtime/protocol/codec.py
Normal file
80
src/plugin_runtime/protocol/codec.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""MsgPack / JSON 编解码器
|
||||
|
||||
提供统一的消息编解码接口,生产环境默认使用 MsgPack,
|
||||
开发调试模式可切换为 JSON(仅编解码切换,传输层不变)。
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import json
|
||||
|
||||
import msgpack
|
||||
|
||||
from .envelope import Envelope
|
||||
|
||||
|
||||
class Codec:
|
||||
"""消息编解码器基类"""
|
||||
|
||||
def encode_envelope(self, envelope: Envelope) -> bytes:
|
||||
raise NotImplementedError
|
||||
|
||||
def decode_envelope(self, data: bytes) -> Envelope:
|
||||
raise NotImplementedError
|
||||
|
||||
def encode(self, obj: dict[str, Any]) -> bytes:
|
||||
raise NotImplementedError
|
||||
|
||||
def decode(self, data: bytes) -> dict[str, Any]:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class MsgPackCodec(Codec):
|
||||
"""MsgPack 编解码器(生产默认)"""
|
||||
|
||||
def encode(self, obj: dict[str, Any]) -> bytes:
|
||||
return msgpack.packb(obj, use_bin_type=True)
|
||||
|
||||
def decode(self, data: bytes) -> dict[str, Any]:
|
||||
result = msgpack.unpackb(data, raw=False)
|
||||
if not isinstance(result, dict):
|
||||
raise ValueError(f"期望解码为 dict,实际为 {type(result)}")
|
||||
return result
|
||||
|
||||
def encode_envelope(self, envelope: Envelope) -> bytes:
|
||||
return self.encode(envelope.model_dump())
|
||||
|
||||
def decode_envelope(self, data: bytes) -> Envelope:
|
||||
raw = self.decode(data)
|
||||
return Envelope.model_validate(raw)
|
||||
|
||||
|
||||
class JsonCodec(Codec):
|
||||
"""JSON 编解码器(开发调试用)"""
|
||||
|
||||
def encode(self, obj: dict[str, Any]) -> bytes:
|
||||
return json.dumps(obj, ensure_ascii=False).encode("utf-8")
|
||||
|
||||
def decode(self, data: bytes) -> dict[str, Any]:
|
||||
result = json.loads(data.decode("utf-8"))
|
||||
if not isinstance(result, dict):
|
||||
raise ValueError(f"期望解码为 dict,实际为 {type(result)}")
|
||||
return result
|
||||
|
||||
def encode_envelope(self, envelope: Envelope) -> bytes:
|
||||
return self.encode(envelope.model_dump())
|
||||
|
||||
def decode_envelope(self, data: bytes) -> Envelope:
|
||||
raw = self.decode(data)
|
||||
return Envelope.model_validate(raw)
|
||||
|
||||
|
||||
def create_codec(use_json: bool = False) -> Codec:
|
||||
"""创建编解码器实例
|
||||
|
||||
Args:
|
||||
use_json: 是否使用 JSON(开发模式)。默认使用 MsgPack。
|
||||
"""
|
||||
if use_json:
|
||||
return JsonCodec()
|
||||
return MsgPackCodec()
|
||||
187
src/plugin_runtime/protocol/envelope.py
Normal file
187
src/plugin_runtime/protocol/envelope.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""RPC Envelope 消息模型
|
||||
|
||||
定义 Host 与 Runner 之间所有 RPC 消息的统一信封格式。
|
||||
使用 Pydantic 进行 schema 定义与校验。
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
import time
|
||||
|
||||
|
||||
# ─── 协议常量 ──────────────────────────────────────────────────────
|
||||
|
||||
PROTOCOL_VERSION = "1.0"
|
||||
|
||||
# 支持的 SDK 版本范围(Host 在握手时校验)
|
||||
MIN_SDK_VERSION = "1.0.0"
|
||||
MAX_SDK_VERSION = "1.99.99"
|
||||
|
||||
|
||||
# ─── 消息类型 ──────────────────────────────────────────────────────
|
||||
|
||||
class MessageType(str, Enum):
|
||||
"""RPC 消息类型"""
|
||||
REQUEST = "request"
|
||||
RESPONSE = "response"
|
||||
EVENT = "event"
|
||||
|
||||
|
||||
# ─── 请求 ID 生成器 ───────────────────────────────────────────────
|
||||
|
||||
class RequestIdGenerator:
|
||||
"""单调递增 int64 请求 ID 生成器(线程安全由调用方保证或使用 asyncio)"""
|
||||
|
||||
def __init__(self, start: int = 1):
|
||||
self._counter = start
|
||||
|
||||
def next(self) -> int:
|
||||
current = self._counter
|
||||
self._counter += 1
|
||||
return current
|
||||
|
||||
|
||||
# ─── Envelope 模型 ─────────────────────────────────────────────────
|
||||
|
||||
class Envelope(BaseModel):
|
||||
"""RPC 统一信封
|
||||
|
||||
所有 Host <-> Runner 消息均封装为此格式。
|
||||
序列化流程:Envelope -> .model_dump() -> MsgPack encode
|
||||
反序列化流程:MsgPack decode -> Envelope.model_validate(data)
|
||||
"""
|
||||
|
||||
protocol_version: str = Field(default=PROTOCOL_VERSION, description="协议版本")
|
||||
request_id: int = Field(description="单调递增请求 ID")
|
||||
message_type: MessageType = Field(description="消息类型")
|
||||
method: str = Field(default="", description="RPC 方法名")
|
||||
plugin_id: str = Field(default="", description="目标插件 ID")
|
||||
timestamp_ms: int = Field(default_factory=lambda: int(time.time() * 1000), description="发送时间戳(ms)")
|
||||
timeout_ms: int = Field(default=30000, description="相对超时(ms)")
|
||||
generation: int = Field(default=0, description="Runner generation 编号")
|
||||
payload: dict[str, Any] = Field(default_factory=dict, description="业务数据")
|
||||
error: dict[str, Any] | None = Field(default=None, description="错误信息(仅 response)")
|
||||
|
||||
def is_request(self) -> bool:
|
||||
return self.message_type == MessageType.REQUEST
|
||||
|
||||
def is_response(self) -> bool:
|
||||
return self.message_type == MessageType.RESPONSE
|
||||
|
||||
def is_event(self) -> bool:
|
||||
return self.message_type == MessageType.EVENT
|
||||
|
||||
def make_response(self, payload: dict[str, Any] | None = None, error: dict[str, Any] | None = None) -> "Envelope":
|
||||
"""基于当前请求创建对应的响应信封"""
|
||||
return Envelope(
|
||||
protocol_version=self.protocol_version,
|
||||
request_id=self.request_id,
|
||||
message_type=MessageType.RESPONSE,
|
||||
method=self.method,
|
||||
plugin_id=self.plugin_id,
|
||||
generation=self.generation,
|
||||
payload=payload or {},
|
||||
error=error,
|
||||
)
|
||||
|
||||
def make_error_response(self, code: str, message: str = "", details: dict | None = None) -> "Envelope":
|
||||
"""基于当前请求创建错误响应"""
|
||||
return self.make_response(
|
||||
error={
|
||||
"code": code,
|
||||
"message": message,
|
||||
"details": details or {},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ─── 握手消息 ──────────────────────────────────────────────────────
|
||||
|
||||
class HelloPayload(BaseModel):
|
||||
"""runner.hello 握手请求 payload"""
|
||||
runner_id: str = Field(description="Runner 进程唯一标识")
|
||||
sdk_version: str = Field(description="SDK 版本号")
|
||||
session_token: str = Field(description="一次性会话令牌")
|
||||
|
||||
|
||||
class HelloResponsePayload(BaseModel):
|
||||
"""runner.hello 握手响应 payload"""
|
||||
accepted: bool = Field(description="是否接受连接")
|
||||
host_version: str = Field(default="", description="Host 版本号")
|
||||
assigned_generation: int = Field(default=0, description="分配的 generation 编号")
|
||||
reason: str = Field(default="", description="拒绝原因(若 accepted=False)")
|
||||
|
||||
|
||||
# ─── 组件注册消息 ──────────────────────────────────────────────────
|
||||
|
||||
class ComponentDeclaration(BaseModel):
|
||||
"""单个组件声明"""
|
||||
name: str = Field(description="组件名称")
|
||||
component_type: str = Field(description="组件类型: action/command/tool/event_handler")
|
||||
plugin_id: str = Field(description="所属插件 ID")
|
||||
metadata: dict[str, Any] = Field(default_factory=dict, description="组件元数据")
|
||||
|
||||
|
||||
class RegisterComponentsPayload(BaseModel):
|
||||
"""plugin.register_components 请求 payload"""
|
||||
plugin_id: str = Field(description="插件 ID")
|
||||
plugin_version: str = Field(default="1.0.0", description="插件版本")
|
||||
components: list[ComponentDeclaration] = Field(default_factory=list, description="组件列表")
|
||||
capabilities_required: list[str] = Field(default_factory=list, description="所需能力列表")
|
||||
|
||||
|
||||
# ─── 调用消息 ──────────────────────────────────────────────────────
|
||||
|
||||
class InvokePayload(BaseModel):
|
||||
"""plugin.invoke_* 请求 payload"""
|
||||
component_name: str = Field(description="要调用的组件名称")
|
||||
args: dict[str, Any] = Field(default_factory=dict, description="调用参数")
|
||||
|
||||
|
||||
class InvokeResultPayload(BaseModel):
|
||||
"""plugin.invoke_* 响应 payload"""
|
||||
success: bool = Field(description="是否成功")
|
||||
result: Any = Field(default=None, description="返回值")
|
||||
|
||||
|
||||
# ─── 能力调用消息 ──────────────────────────────────────────────────
|
||||
|
||||
class CapabilityRequestPayload(BaseModel):
|
||||
"""cap.* 请求 payload(插件 -> Host 能力调用)"""
|
||||
capability: str = Field(description="能力名称,如 send.text, db.query")
|
||||
args: dict[str, Any] = Field(default_factory=dict, description="调用参数")
|
||||
|
||||
|
||||
class CapabilityResponsePayload(BaseModel):
|
||||
"""cap.* 响应 payload"""
|
||||
success: bool = Field(description="是否成功")
|
||||
result: Any = Field(default=None, description="返回值")
|
||||
|
||||
|
||||
# ─── 健康检查 ──────────────────────────────────────────────────────
|
||||
|
||||
class HealthPayload(BaseModel):
|
||||
"""plugin.health 响应 payload"""
|
||||
healthy: bool = Field(description="是否健康")
|
||||
loaded_plugins: list[str] = Field(default_factory=list, description="已加载的插件列表")
|
||||
uptime_ms: int = Field(default=0, description="运行时长(ms)")
|
||||
|
||||
|
||||
# ─── 配置更新 ──────────────────────────────────────────────────────
|
||||
|
||||
class ConfigUpdatedPayload(BaseModel):
|
||||
"""plugin.config_updated 事件 payload"""
|
||||
plugin_id: str = Field(description="插件 ID")
|
||||
config_version: str = Field(description="新配置版本")
|
||||
config_data: dict[str, Any] = Field(default_factory=dict, description="配置内容")
|
||||
|
||||
|
||||
# ─── 关停 ──────────────────────────────────────────────────────────
|
||||
|
||||
class ShutdownPayload(BaseModel):
|
||||
"""plugin.shutdown / plugin.prepare_shutdown payload"""
|
||||
reason: str = Field(default="normal", description="关停原因")
|
||||
drain_timeout_ms: int = Field(default=5000, description="排空超时(ms)")
|
||||
61
src/plugin_runtime/protocol/errors.py
Normal file
61
src/plugin_runtime/protocol/errors.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""RPC 错误码定义
|
||||
|
||||
所有 Host 与 Runner 之间的 RPC 通信使用统一的错误码体系。
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ErrorCode(str, Enum):
|
||||
"""RPC 错误码枚举"""
|
||||
|
||||
# 通用
|
||||
OK = "OK"
|
||||
E_UNKNOWN = "E_UNKNOWN"
|
||||
|
||||
# 协议层
|
||||
E_TIMEOUT = "E_TIMEOUT"
|
||||
E_BAD_PAYLOAD = "E_BAD_PAYLOAD"
|
||||
E_PROTOCOL_MISMATCH = "E_PROTOCOL_MISMATCH"
|
||||
|
||||
# 权限与策略
|
||||
E_UNAUTHORIZED = "E_UNAUTHORIZED"
|
||||
E_METHOD_NOT_ALLOWED = "E_METHOD_NOT_ALLOWED"
|
||||
E_BACKPRESSURE = "E_BACKPRESSURE"
|
||||
E_HOST_OVERLOADED = "E_HOST_OVERLOADED"
|
||||
|
||||
# 插件生命周期
|
||||
E_PLUGIN_CRASHED = "E_PLUGIN_CRASHED"
|
||||
E_PLUGIN_NOT_FOUND = "E_PLUGIN_NOT_FOUND"
|
||||
E_GENERATION_MISMATCH = "E_GENERATION_MISMATCH"
|
||||
E_RELOAD_IN_PROGRESS = "E_RELOAD_IN_PROGRESS"
|
||||
|
||||
# 能力调用
|
||||
E_CAPABILITY_DENIED = "E_CAPABILITY_DENIED"
|
||||
E_CAPABILITY_FAILED = "E_CAPABILITY_FAILED"
|
||||
|
||||
|
||||
class RPCError(Exception):
|
||||
"""RPC 调用异常"""
|
||||
|
||||
def __init__(self, code: ErrorCode, message: str = "", details: dict | None = None):
|
||||
self.code = code
|
||||
self.message = message or code.value
|
||||
self.details = details or {}
|
||||
super().__init__(f"[{code.value}] {self.message}")
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"code": self.code.value,
|
||||
"message": self.message,
|
||||
"details": self.details,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "RPCError":
|
||||
code = ErrorCode(data.get("code", "E_UNKNOWN"))
|
||||
return cls(
|
||||
code=code,
|
||||
message=data.get("message", ""),
|
||||
details=data.get("details", {}),
|
||||
)
|
||||
Reference in New Issue
Block a user