Skip to main content

Plugins

ClawDesk has a modular plugin system that lets you extend every aspect of the gateway—custom channels, tools, pipeline stages, providers, and more. Plugins are discovered, loaded, resolved, and activated through a managed lifecycle with hot-reload support.

Plugin Lifecycle

Lifecycle Stages

StageDescription
DiscoverScan plugin directories and registries for available plugins
LoadLoad plugin metadata, validate manifest, check compatibility
ResolveResolve dependencies between plugins, detect conflicts
ActivateInitialize the plugin, register its components with the host
DeactivateGracefully shut down the plugin, unregister components

Plugin Architecture

Key Components

ComponentDescription
PluginHostManages the plugin lifecycle and provides the host API
PluginRegistryStores and indexes all discovered plugins
DependencyResolverResolves plugin dependencies using semver
PluginSandboxResource isolation and permission enforcement

PluginHost

pub struct PluginHost {
registry: PluginRegistry,
resolver: DependencyResolver,
sandbox_config: SandboxConfig,
hot_reload: bool,
}

impl PluginHost {
/// Discover plugins from configured directories
pub async fn discover(&mut self) -> Result<Vec<PluginManifest>> {
let mut manifests = Vec::new();
for dir in &self.plugin_dirs {
for entry in fs::read_dir(dir)? {
if let Ok(manifest) = PluginManifest::load(&entry.path()) {
manifests.push(manifest);
}
}
}
Ok(manifests)
}

/// Load a specific plugin
pub async fn load(&mut self, name: &str) -> Result<()> {
let manifest = self.registry.get_manifest(name)?;
let plugin = self.load_plugin_binary(&manifest)?;
self.registry.register(name, plugin);
Ok(())
}

/// Resolve all dependencies
pub async fn resolve(&mut self) -> Result<ResolutionPlan> {
self.resolver.resolve(&self.registry)
}

/// Activate a plugin
pub async fn activate(&mut self, name: &str, ctx: &mut HostContext) -> Result<()> {
let plugin = self.registry.get_mut(name)?;
plugin.activate(ctx).await?;
Ok(())
}

/// Deactivate a plugin
pub async fn deactivate(&mut self, name: &str) -> Result<()> {
let plugin = self.registry.get_mut(name)?;
plugin.deactivate().await?;
Ok(())
}
}

DependencyResolver

The resolver builds a dependency graph and produces a topologically sorted activation order:

pub struct DependencyResolver;

impl DependencyResolver {
pub fn resolve(&self, registry: &PluginRegistry) -> Result<ResolutionPlan> {
let mut graph = DependencyGraph::new();

for plugin in registry.iter() {
graph.add_node(&plugin.name);
for (dep_name, version_req) in &plugin.manifest.dependencies {
let dep = registry.get(dep_name)
.ok_or(ResolveError::MissingDependency {
plugin: plugin.name.clone(),
dependency: dep_name.clone(),
})?;

if !version_req.matches(&dep.manifest.version) {
return Err(ResolveError::VersionConflict {
plugin: plugin.name.clone(),
dependency: dep_name.clone(),
required: version_req.clone(),
available: dep.manifest.version.clone(),
});
}

graph.add_edge(dep_name, &plugin.name);
}
}

// Topological sort for activation order
let order = graph.topological_sort()?;
Ok(ResolutionPlan { activation_order: order })
}
}

PluginSandbox

Plugins run within a configurable sandbox that restricts resource access:

pub struct PluginSandbox {
config: SandboxConfig,
}

pub struct SandboxConfig {
/// Maximum memory usage (bytes)
pub max_memory: usize,

/// Maximum CPU time per operation
pub max_cpu_time: Duration,

/// Allowed filesystem paths
pub fs_allowed: Vec<PathBuf>,

/// Allowed network endpoints
pub net_allowed: Vec<String>,

/// Whether to allow spawning subprocesses
pub allow_subprocess: bool,

/// Maximum concurrent operations
pub max_concurrency: usize,
}
[plugins.sandbox]
max_memory_mb = 256
max_cpu_time_secs = 30
fs_allowed = ["${CLAWDESK_DATA_DIR}/plugins/"]
net_allowed = ["api.example.com:443"]
allow_subprocess = false
max_concurrency = 4

Plugin Manifest

Every plugin has a plugin.toml manifest:

[plugin]
name = "weather"
version = "1.2.0"
description = "Weather information tool for ClawDesk"
author = "Jane Doe <jane@example.com>"
license = "MIT"
homepage = "https://github.com/jane/clawdesk-plugin-weather"
min_clawdesk_version = "0.5.0"

[plugin.capabilities]
# What the plugin provides
tools = ["get_weather", "get_forecast"]
channels = []
pipeline_stages = []
providers = []

[plugin.dependencies]
# semver requirements for other plugins
# clawdesk-plugin-geocoding = ">=1.0.0"

[plugin.permissions]
# Required permissions
network = ["api.openweathermap.org:443"]
filesystem = []
subprocess = false

[plugin.config]
# Plugin-specific configuration schema
[plugin.config.api_key]
type = "string"
required = true
env = "WEATHER_API_KEY"
description = "OpenWeatherMap API key"

[plugin.config.units]
type = "string"
default = "metric"
enum = ["metric", "imperial"]
description = "Temperature units"

[plugin.config.cache_ttl_secs]
type = "integer"
default = 300
description = "Cache duration for weather data"

Developing a Plugin

Plugin Trait

Every plugin implements the Plugin trait:

use clawdesk_infra::plugin::{Plugin, HostContext, PluginError};

pub struct WeatherPlugin {
config: WeatherConfig,
client: reqwest::Client,
}

#[async_trait]
impl Plugin for WeatherPlugin {
fn name(&self) -> &str {
"weather"
}

fn version(&self) -> &semver::Version {
&semver::Version::parse("1.2.0").unwrap()
}

async fn activate(&mut self, ctx: &mut HostContext) -> Result<(), PluginError> {
// Register tools with the host
ctx.register_tool(GetWeatherTool::new(self.config.clone()));
ctx.register_tool(GetForecastTool::new(self.config.clone()));

tracing::info!("Weather plugin activated");
Ok(())
}

async fn deactivate(&mut self) -> Result<(), PluginError> {
tracing::info!("Weather plugin deactivated");
Ok(())
}

async fn health_check(&self) -> Result<(), PluginError> {
// Verify the API key is valid
let response = self.client
.get("https://api.openweathermap.org/data/2.5/weather")
.query(&[("q", "London"), ("appid", &self.config.api_key)])
.send()
.await?;

if response.status().is_success() {
Ok(())
} else {
Err(PluginError::HealthCheckFailed("API key invalid".into()))
}
}
}

Scaffolding a Plugin

clawdesk plugins create my-plugin --lang rust

This generates:

my-plugin/
├── Cargo.toml
├── plugin.toml # Plugin manifest
├── src/
│ ├── lib.rs # Plugin entry point
│ └── tools.rs # Tool implementations
├── tests/
│ └── integration.rs # Integration tests
└── README.md

Generated Cargo.toml

[package]
name = "clawdesk-plugin-my-plugin"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"] # dynamic library for plugin loading

[dependencies]
clawdesk-infra = { version = "0.5" }
async-trait = "0.1"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }

Generated src/lib.rs

use clawdesk_infra::plugin::{Plugin, HostContext, PluginError, export_plugin};

mod tools;

pub struct MyPlugin {
// your fields
}

#[async_trait::async_trait]
impl Plugin for MyPlugin {
fn name(&self) -> &str { "my-plugin" }

fn version(&self) -> &semver::Version {
&semver::Version::new(0, 1, 0)
}

async fn activate(&mut self, ctx: &mut HostContext) -> Result<(), PluginError> {
// Register your tools, channels, pipeline stages, etc.
ctx.register_tool(tools::MyTool::new());
Ok(())
}

async fn deactivate(&mut self) -> Result<(), PluginError> {
Ok(())
}
}

// Export the plugin so ClawDesk can discover it
export_plugin!(MyPlugin);

Installing Plugins

From the Plugin Registry

clawdesk plugins install weather
clawdesk plugins install rag-enhanced

From a Local Path

# Install from a built plugin directory
clawdesk plugins install ./my-plugin/

# Install from a .tar.gz
clawdesk plugins install ./clawdesk-plugin-weather-1.2.0.tar.gz

From Git

clawdesk plugins install https://github.com/user/clawdesk-plugin-weather.git
clawdesk plugins install https://github.com/user/clawdesk-plugin-weather.git --branch v1.2

Plugin Configuration

After installing, configure the plugin in config.toml:

[plugins.weather]
enabled = true

[plugins.weather.config]
api_key = "${WEATHER_API_KEY}"
units = "metric"
cache_ttl_secs = 600

Hot Reload

Plugins support hot-reloading in development mode. When a plugin binary is recompiled, ClawDesk detects the change and reloads it without restarting the gateway:

[plugins]
hot_reload = true
watch_interval_secs = 2

Development Workflow

# Terminal 1: Run gateway with hot-reload
clawdesk gateway --log-level debug

# Terminal 2: Develop your plugin
cd my-plugin/
cargo build # ClawDesk detects the new binary and reloads

# Gateway output:
# [INFO] Plugin 'my-plugin' binary changed, reloading...
# [INFO] Deactivating 'my-plugin' v0.1.0
# [INFO] Loading 'my-plugin' v0.1.1
# [INFO] Activating 'my-plugin' v0.1.1
# [INFO] Plugin 'my-plugin' reloaded successfully
warning

Hot-reload in production is supported but should be used carefully. Ensure each plugin version is backward-compatible and test in staging first. Additionally, any in-flight requests to the plugin during reload will be gracefully queued.


Plugin Capabilities

Plugins can extend ClawDesk in multiple ways:

Tools

async fn activate(&mut self, ctx: &mut HostContext) -> Result<(), PluginError> {
ctx.register_tool(MyTool::new());
Ok(())
}

Channels

async fn activate(&mut self, ctx: &mut HostContext) -> Result<(), PluginError> {
ctx.register_channel_factory("my_platform", MyChannelFactory::new());
Ok(())
}

Pipeline Stages

async fn activate(&mut self, ctx: &mut HostContext) -> Result<(), PluginError> {
ctx.register_pipeline_stage("content_filter", ContentFilterStage::new());
Ok(())
}

Providers

async fn activate(&mut self, ctx: &mut HostContext) -> Result<(), PluginError> {
ctx.register_provider("my_llm", MyLLMProvider::new(self.config.clone()));
Ok(())
}

Event Hooks

async fn activate(&mut self, ctx: &mut HostContext) -> Result<(), PluginError> {
ctx.on_message_received(|msg| {
tracing::info!("Message from {}: {}", msg.sender, msg.preview());
});

ctx.on_response_sent(|resp| {
// Analytics, logging, etc.
});

Ok(())
}

Plugin Management

CLI Commands

# List installed plugins
clawdesk plugins list

# Example output:
# ┌──────────────────┬─────────┬──────────┬──────────────────────────┐
# │ Name │ Version │ Status │ Capabilities │
# ├──────────────────┼─────────┼──────────┼──────────────────────────┤
# │ weather │ 1.2.0 │ Active │ tools: get_weather, ... │
# │ rag-enhanced │ 2.0.1 │ Active │ tools: rag_search │
# │ custom-channel │ 0.3.0 │ Inactive │ channels: my_platform │
# └──────────────────┴─────────┴──────────┴──────────────────────────┘

# Inspect a plugin
clawdesk plugins inspect weather

# Enable/disable
clawdesk plugins enable custom-channel
clawdesk plugins disable rag-enhanced

# Update
clawdesk plugins update weather
clawdesk plugins update # update all

# Uninstall
clawdesk plugins uninstall weather

Dependency Conflicts

If a dependency conflict is detected, the resolver reports it:

$ clawdesk plugins install new-plugin
Error: Dependency conflict detected

new-plugin v1.0.0 requires geocoding >=2.0.0
weather v1.2.0 requires geocoding ^1.5.0

These version requirements are incompatible.

Suggestions:
1. Update 'weather' to a version compatible with geocoding 2.x
2. Use 'clawdesk plugins install new-plugin --force' to override (risky)

Testing Plugins

Unit Tests

#[cfg(test)]
mod tests {
use super::*;

#[tokio::test]
async fn test_weather_tool() {
let tool = GetWeatherTool::new(WeatherConfig {
api_key: "test-key".into(),
units: "metric".into(),
cache_ttl_secs: 0,
});

let params = ToolParams::from_json(json!({
"location": "London"
}));

let result = tool.execute(params).await;
assert!(result.is_ok());
}
}

Integration Tests

#[tokio::test]
async fn test_plugin_lifecycle() {
let mut host = PluginHost::test_instance();

// Load and activate
host.load("my-plugin").await.unwrap();
host.resolve().await.unwrap();

let mut ctx = HostContext::test_context();
host.activate("my-plugin", &mut ctx).await.unwrap();

// Verify registration
assert!(ctx.tool_registry.has("my_tool"));

// Deactivate
host.deactivate("my-plugin").await.unwrap();
assert!(!ctx.tool_registry.has("my_tool"));
}
# Run plugin tests
cd my-plugin/
cargo test

# Run with the ClawDesk test harness
clawdesk plugins test ./my-plugin/

Troubleshooting

ProblemSolution
"Plugin not found"Check plugin directory path, run clawdesk doctor
"Incompatible version"Check min_clawdesk_version in manifest
"Dependency not found"Install the required dependency plugin first
"Permission denied"Check sandbox permissions in plugin manifest
Hot-reload not workingVerify hot_reload = true in config, check file watcher
Plugin crashes on activateCheck logs with --log-level debug, run health check