OpenSkyLabOpenSkyLab

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

osl-plugin-sdk/src/
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:

traits.rs
#[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:

context.rs
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}/:

route.rs
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:

panel.rs
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

terminal
cargo new my-plugin --lib
cd my-plugin

2. Add the SDK dependency

Cargo.toml
[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

src/lib.rs
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

terminal
# 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.