Plugin SDK
Building Plugins
OpenSkyLab plugins are dynamic libraries (.so on Linux, .dll on Windows) that implement the ProPlugin trait. The SDK is MIT-licensed and designed for fast compilation with zero heavy dependencies.
Overview
The plugin system architecture:
- osl-plugin-sdk — Public crate defining the plugin contract (MIT licensed)
- Plugin Registry — Discovers and loads .so/.dll files at runtime
- ProPanelLoader — Frontend component that renders plugin panels via iframe or embedded React
- PLUGIN_API_VERSION — Version constant ensuring binary compatibility
The SDK has zero internal dependencies — it does not depend on osl-core or osl-api. This keeps compilation fast and prevents circular dependencies.
SDK Structure
lib.rs Re-exports, PLUGIN_API_VERSION, export_plugin! macro traits.rs trait ProPlugin (main contract) context.rs PluginContext, WsEvent, LicenseInfo, LicenseTier metadata.rs PluginMetadata panel.rs PanelDef, PanelPosition route.rs PluginRoute, HttpMethod, RouteHandler
ProPlugin Trait
The core contract every plugin must implement:
#[async_trait]
pub trait ProPlugin: Send + Sync {
/// Plugin metadata (name, version, description, author)
fn metadata(&self) -> PluginMetadata;
/// Called when the plugin is loaded. Initialize state here.
async fn on_load(&mut self, ctx: PluginContext) -> Result<()>;
/// Called when the plugin is unloaded. Clean up resources.
async fn on_unload(&mut self) -> Result<()>;
/// HTTP routes this plugin exposes (mounted under /api/v1/plugins/{id}/)
fn routes(&self) -> Vec<PluginRoute>;
/// UI panels this plugin provides
fn panels(&self) -> Vec<PanelDef>;
/// WebSocket event types this plugin emits
fn event_types(&self) -> Vec<String>;
/// Event types this plugin wants to receive
fn event_hooks(&self) -> Vec<String> { vec![] }
/// Called when a subscribed event occurs
async fn on_event(&self, _event: WsEvent) -> Result<()> {
Ok(())
}
}Plugin Context
Plugins receive a PluginContext on load with everything they need to interact with the system:
pub struct PluginContext {
/// Broadcast channel to send WS events to all clients
pub event_tx: broadcast::Sender<WsEvent>,
/// License information for the current user
pub license: LicenseInfo,
/// Directory for persistent plugin data
pub data_dir: PathBuf,
/// Directory for plugin configuration
pub config_dir: PathBuf,
}
pub struct LicenseInfo {
pub tier: LicenseTier,
pub valid: bool,
pub expires_at: Option<DateTime<Utc>>,
}
pub enum LicenseTier {
Free,
Pro,
Enterprise,
}
/// Duplicated intentionally from osl-core to avoid circular dependency
pub struct WsEvent {
pub event_type: String,
pub ts: u64,
pub data: serde_json::Value,
}Routes & Panels
HTTP Routes
Plugins can expose custom HTTP endpoints. Routes are automatically mounted under /api/v1/plugins/{plugin_id}/:
pub struct PluginRoute {
pub path: String, // e.g. "/analyze"
pub method: HttpMethod, // Get, Post, Put, Delete
pub handler: RouteHandler, // async fn handler
}
pub enum HttpMethod {
Get, Post, Put, Delete,
}
pub type RouteHandler = Box<dyn Fn(Request) -> BoxFuture<Response> + Send + Sync>;UI Panels
Plugins can register panels that appear in the OpenSkyLab sidebar:
pub struct PanelDef {
pub id: String, // unique panel identifier
pub title: String, // display name
pub icon: String, // lucide icon name
pub position: PanelPosition,
pub default_visible: bool,
}
pub enum PanelPosition {
Sidebar, // appears in main sidebar
Settings, // appears in settings panel
Dock, // appears in bottom dock
}Building a Plugin
1. Create a new Rust crate
cargo new my-plugin --lib cd my-plugin
2. Add the SDK dependency
[package]
name = "my-plugin"
version = "0.1.0"
[lib]
crate-type = ["cdylib"] # Dynamic library
[dependencies]
osl-plugin-sdk = { path = "../osl-plugin-sdk" }
async-trait = "0.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"3. Implement ProPlugin
use osl_plugin_sdk::*;
use async_trait::async_trait;
pub struct MyPlugin {
ctx: Option<PluginContext>,
}
impl MyPlugin {
pub fn new() -> Self {
Self { ctx: None }
}
}
#[async_trait]
impl ProPlugin for MyPlugin {
fn metadata(&self) -> PluginMetadata {
PluginMetadata {
id: "my-plugin".into(),
name: "My Custom Plugin".into(),
version: "0.1.0".into(),
description: "Does something cool".into(),
author: "Your Name".into(),
}
}
async fn on_load(&mut self, ctx: PluginContext) -> Result<()> {
self.ctx = Some(ctx);
Ok(())
}
async fn on_unload(&mut self) -> Result<()> {
Ok(())
}
fn routes(&self) -> Vec<PluginRoute> {
vec![] // Add custom HTTP routes here
}
fn panels(&self) -> Vec<PanelDef> {
vec![] // Add UI panels here
}
fn event_types(&self) -> Vec<String> {
vec![] // WS events this plugin emits
}
}
// This macro generates the C ABI entry points
export_plugin!(MyPlugin, MyPlugin::new);4. Build and install
# Build the dynamic library cargo build --release # Copy to the plugins directory cp target/release/libmy_plugin.so ~/.config/openskylab/plugins/ # Restart OpenSkyLab — the plugin will be auto-discovered
The export_plugin! macro generates two extern C functions: _astro_create_plugin() and _astro_plugin_api_version(). The loader checks the API version before calling create to ensure binary compatibility.
Version Compatibility
The SDK defines PLUGIN_API_VERSION as a u32 constant. If the plugin was compiled against a different version than the host, it will be rejected at load time. Bump this constant only when making breaking changes to the ProPlugin trait.
