feat(plugin-sdk): many more initial impl

This commit is contained in:
Neko Ayaka
2026-02-07 03:31:43 +08:00
parent dd92fb8f1c
commit 795e12f13b
30 changed files with 867 additions and 3 deletions
@@ -0,0 +1,59 @@
# {title}
{toc}
## Summary
{summary}
## Background
{background}
## Goals
{goals}
## Non-goals
{non-goals}
## Proposal
{proposal}
## Design Detials
{tagline of design details}
### {optional sub-sections}
{sub-sections}
### {optional sub-sections}
{sub-sections}
## Verify & Test
### Criteria
{how it can be seens as completed, or could be moved to next phase}
### Test & QA
{how to test}
## Progress
{multiple sections of progress}
## Reviews
### Q&A
{multiple Q&A for explaining what possible questions or challenges could have or being asked}
### Related Documentations
- [{title of the related documentation}]({related path to the related documentation})
@@ -0,0 +1,230 @@
---
title: AIRI Plugin Platform
description: Architecture for plugins, bridges, and multi-device orchestration
---
# AIRI Plugin Platform
- [Summary](#summary)
- [Background](#background)
- [Goals](#goals)
- [Non-goals](#non-goals)
- [Proposal](#proposal)
- [Design Detials](#design-detials)
- [Control And Data Planes](#control-and-data-planes)
- [Plugin Host And Viewers](#plugin-host-and-viewers)
- [Plugin Lifecycle Overview](#plugin-lifecycle-overview)
- [Bridges And Remote Plugins](#bridges-and-remote-plugins)
- [Transport Abstraction](#transport-abstraction)
- [Capability Model](#capability-model)
- [Deployment Modes](#deployment-modes)
- [Manifest And Entrypoints](#manifest-and-entrypoints)
- [Verify & Test](#verify--test)
- [Criteria](#criteria)
- [Test & QA](#test--qa)
- [Progress](#progress)
- [Status](#status)
- [Next Steps](#next-steps)
- [Reviews](#reviews)
- [Q&A](#qa)
- [Related Documentations](#related-documentations)
## Summary
AIRI is a multi-node system where plugins, bridges, and viewers communicate over Eventa transports. A Plugin Host loads plugins, provides a single API surface, and routes control and data across devices. The platform separates the control plane from the data plane, supports local and remote plugins, and enables multi-device orchestration without changing plugin APIs.
## Background
AIRI needs to run across desktop, web, and mobile while keeping one clean API surface. Plugins must be able to register UI, declare capabilities, and exchange data with device-specific bridges. To keep the system scalable, high-rate streams must be separated from lifecycle and configuration traffic.
## Goals
- Provide a single plugin API surface across runtimes.
- Separate lifecycle and configuration traffic from high-rate streams.
- Support local plugins and remote plugins with the same protocol.
- Allow multiple viewers and bridges to coordinate through a shared control plane.
- Keep deployment flexible: embedded, external, or remote Plugin Host.
## Non-goals
- Defining the full plugin lifecycle state machine in this document.
- Specifying detailed UI layouts or viewer implementations.
- Implementing a new transport beyond Eventa adapters.
## Proposal
- Use Eventa for all control and data traffic.
- Use a dedicated control plane for configuration, permissions, UI, and routing.
- Use a data plane for high-volume streams like audio, vision, and telemetry.
- Run plugins inside a Plugin Host that loads plugin entrypoints and exposes the SDK.
- Treat bridges as device-specific integrations that only provide data and actions.
## Design Detials
The platform design focuses on consistent APIs, transport-agnostic integration, and multi-device orchestration.
### Control And Data Planes
Control plane purpose: lifecycle, configuration, routing policy, permissions, and UI contributions.
Typical control messages:
- control:hello
- control:announce
- control:plugin:register
- control:plugin:config:get
- control:plugin:config:set
- control:capability:grant
- control:capability:revoke
- control:ui:register
Data plane purpose: real-time and high-throughput streams.
Typical data messages:
- data:context:update
- data:vision:frame
- data:audio:stream
- data:transcript
- data:character:output
Both planes use Eventa messages. Transport options:
- Two WebSocket endpoints
- One multiplexed connection with namespaces
### Plugin Host And Viewers
The Plugin Host is a Node process that:
- Loads plugin entrypoints.
- Exposes the AIRI SDK.
- Registers UI contributions.
- Negotiates capabilities.
- Connects to control and data planes.
Viewers render UI and character output. Examples:
- Electron Stage with Configurator features.
- Web Configurator client.
- Pocket Stage client.
### Plugin Lifecycle Overview
The lifecycle below mirrors the detailed lifecycle comment in
`packages/plugin-sdk/src/plugin-host/index.ts` and focuses on the module
announcement, configuration, and capability phases.
```mermaid
flowchart TD
A[Connect to control plane] --> B[Authenticate]
B --> C[Host sends registry:modules:sync]
C --> D[Module emits module:announce]
D --> E[Module declares deps + initial config]
E --> F{Dependencies resolved?}
F -- no --> G[module:status emitted]
F -- yes --> H[module:prepared]
H --> I[module:configuration:needed]
I --> J[validate/plan/commit config]
J --> K[module:configuration:configured]
K --> L[Offer capabilities]
L --> M[Capability configuration phase]
M --> N[module:status ready]
```
### Bridges And Remote Plugins
Bridges connect external devices and services to AIRI. They do not own UI; they only provide data and actions. Examples:
- VS Code extension for editor context and commands.
- Browser extension for page context.
- Minecraft service for game events and commands.
Remote plugins are services in any language that connect over Eventa and register capabilities. They are preferred for server integrations and non-JS/TS stacks.
### Transport Abstraction
All SDK calls are transport-agnostic. The host controls whether communication is local IPC or remote RPC without changing plugin APIs.
### Capability Model
Each node announces capabilities on registration. The control plane grants or denies permissions and routes requests based on policy.
Example capabilities:
- context.read
- context.write
- ui.panel
- ui.widget
- vision.capture
- vision.stream
- device.mobile.sensors
### Deployment Modes
The Plugin Host can run in three modes:
1. Embedded in Electron main for install-and-go.
2. External Node process for hot reload and isolation.
3. Remote server for cross-device continuity.
### Manifest And Entrypoints
Plugins declare metadata in a manifest file and provide runtime entrypoints.
Example:
```json
{
"id": "airi.vscode",
"name": "AIRI VS Code",
"version": "1.0.0",
"capabilities": ["context.read", "ui.panel", "commands"],
"entrypoints": {
"node": "./dist/node/index.js"
}
}
```
## Verify & Test
### Criteria
- Plugins can be loaded by the host and register UI and capabilities.
- Bridges can connect and be discovered by the control plane.
- Data plane streams stay isolated from control plane traffic.
- The same plugin API surface works across desktop, web, and mobile.
### Test & QA
- Integration test: host + viewer + bridge with control plane routing.
- Integration test: data plane streaming with a high-rate source.
- Compatibility test: same plugin entrypoints across multiple runtimes.
## Progress
### Status
Active design.
### Next Steps
- Align runtime docs with updated plugin context and transport strategy.
- Expand remote plugin examples by language.
## Reviews
### Q&A
- Q: Why split control and data planes?
A: Lifecycle traffic and high-rate streams have different reliability and QoS needs.
- Q: Do bridges render UI?
A: No. UI is contributed to viewers through the control plane.
- Q: Can remote plugins be written without JS or npm?
A: Yes. They only need to speak the Eventa protocol over WebSocket.
### Related Documentations
- [Multi-Transport Plugin Contexts](../../../../packages/plugin-sdk/docs/design/multi-transport.md)
@@ -0,0 +1,176 @@
# Multi-Transport Plugin Contexts
- [Summary](#summary)
- [Background](#background)
- [Goals](#goals)
- [Non-goals](#non-goals)
- [Proposal](#proposal)
- [Design Detials](#design-detials)
- [Context And Transport Model](#context-and-transport-model)
- [Lifecycle Placement](#lifecycle-placement)
- [Host Runtime Layout](#host-runtime-layout)
- [API Binding Strategy](#api-binding-strategy)
- [Local Vs Remote Plugins](#local-vs-remote-plugins)
- [Multi-Plugin Isolation](#multi-plugin-isolation)
- [Verify & Test](#verify--test)
- [Criteria](#criteria)
- [Test & QA](#test--qa)
- [Progress](#progress)
- [Status](#status)
- [Next Steps](#next-steps)
- [Reviews](#reviews)
- [Q&A](#qa)
- [Related Documentations](#related-documentations)
## Summary
Introduce a host-side transport-aware context factory that provides one Eventa context per plugin instance. Plugin SDK APIs become context-bound factories, allowing local (in-memory/worker) and remote (WebSocket) plugins to share the same API surface while using different transports. This enables multiple plugins within a single Plugin Host without cross-talk or global channel coupling.
## Background
`plugin-sdk` currently exposes APIs (for example `providers.listProviders`) that call `defineInvoke` on a globally imported channel. This couples plugins to a single shared context and prevents the host from isolating multiple plugins or using different transports per plugin. We also need a path to support local plugins (in-process or worker) and remote plugins (WebSocket) with consistent ergonomics.
Eventa is context-oriented: contexts are created per transport (in-memory, WebSocket, worker, electron) and the invoke/handler APIs attach to that context. Multiple contexts can co-exist in the same process.
## Goals
- Provide one context per plugin instance, scoped by transport.
- Allow the same API surface to work for local and remote plugins.
- Keep transport selection under Plugin Host control, not plugin control.
- Support multiple plugins within one host without channel conflicts.
- Keep the API ergonomics for plugin authors simple and explicit.
## Non-goals
- Designing the full plugin lifecycle orchestration (phase transitions, capability config, etc.).
- Implementing a new transport stack beyond Eventa adapters (unless required by runtime gaps).
- Defining plugin packaging or distribution formats beyond `ManifestV1` entrypoints.
## Proposal
1. Introduce a host-side `createPluginContext(transport)` factory that returns an Eventa context bound to the plugin's transport.
2. Convert plugin SDK APIs to context-bound factories (`createApis(ctx)`), replacing global channel usage.
3. Resolve transport per plugin instance during host setup and pass the created context into plugin `init()`.
4. Add runtime-specific implementations under `plugin-host/runtimes/node` and `plugin-host/runtimes/web` to handle different transport adapters.
5. Optional: introduce shared reliable WebSocket helpers if needed, but prefer Eventa adapters first.
## Design Detials
Transport-aware contexts for isolated multi-plugin hosts.
### Context And Transport Model
Define a small transport config type owned by the Plugin Host:
```ts
export type PluginTransport
= | { kind: 'in-memory' }
| { kind: 'websocket', url: string, protocols?: string[] }
| { kind: 'web-worker', worker: Worker }
| { kind: 'node-worker', worker: import('node:worker_threads').Worker }
| { kind: 'electron', target: 'main' | 'renderer', webContentsId?: number }
```
`createPluginContext(transport)` creates and returns an Eventa context based on the transport adapter (in-memory, WebSocket, worker, electron).
### Lifecycle Placement
Context creation happens during host setup, before any plugin lifecycle method is called.
1. Load plugin module (FileSystemLoader / UrlLoader).
2. Resolve transport for the plugin (manifest + host config).
3. Create context via `createPluginContext(transport)`.
4. Bind APIs with `createApis(ctx)`.
5. Call `plugin.init({ host: ctx, apis })`.
### Host Runtime Layout
- `packages/plugin-sdk/src/plugin-host/transports/`:
- transport type definitions and helpers
- `packages/plugin-sdk/src/plugin-host/runtimes/node/`:
- in-memory, node-worker, websocket implementations
- `packages/plugin-sdk/src/plugin-host/runtimes/web/`:
- web-worker, websocket implementations
- `packages/plugin-sdk/src/plugin-host/index.ts`:
- exports the runtime-appropriate `createPluginContext` via conditional exports
### API Binding Strategy
Replace direct channel usage with context-bound factories:
```ts
export function createProviders(ctx: EventaContext) {
return {
listProviders() {
return defineInvoke(ctx, protocolListProviders)()
},
}
}
export function createApis(ctx: EventaContext) {
return { providers: createProviders(ctx) }
}
```
Plugins call `createApis(ctx)` provided by the host instead of importing global singletons.
### Local Vs Remote Plugins
- Local plugins:
- `in-memory` for simplest case
- `node-worker` or `web-worker` for isolation
- Remote plugins:
- `websocket` transport bound to a specific URL or connection
Transport selection is a host concern; plugins are transport-agnostic.
### Multi-Plugin Isolation
Each plugin has its own context and transport. APIs are bound to that context, preventing cross-talk. The host keeps a registry mapping plugin ID to its context, transport, and loaded module for lifecycle management.
## Verify & Test
### Criteria
- Multiple plugins can be loaded in one host without shared global channels.
- Local plugin calls use in-memory or worker contexts without manual wiring in plugin code.
- Remote plugin calls use WebSocket contexts and do not affect local plugins.
- Existing plugin tests can be updated to pass by injecting a context into APIs.
### Test & QA
- Unit test: create two plugin contexts in the same process, verify isolated invoke/handler pairs.
- Unit test: FileSystemLoader + in-memory context binds correctly to `createApis(ctx)`.
- Integration test (optional): WebSocket adapter roundtrip using a stub server.
## Progress
### Status
Planned.
### Next Steps
- Implement `createApis(ctx)` and migrate current API modules.
- Implement `createPluginContext` in node runtime (in-memory + websocket).
- Update tests to construct APIs with a provided context.
## Reviews
### Q&A
- Q: Why not keep global channels and just switch the active channel?
A: Global channels make multi-plugin isolation impossible and require global state mutation. Context-per-plugin avoids cross-talk and matches Eventa's design.
- Q: Do plugins need to know about transports?
A: No. The host injects the context and APIs; plugins remain transport-agnostic.
- Q: Should we build a shared reliable WebSocket package?
A: Only if we need custom reconnection/heartbeat logic across multiple packages. Start with Eventa adapters; factor out shared logic later if required.
- Q: Can workers be used for plugin isolation?
A: Yes. Use Eventa web-worker or node-worker adapters to bridge a per-plugin context to the worker.
### Related Documentations
- [Plugin Lifecycle](./plugin-lifecycle.md)