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
| Stage | Description |
|---|---|
| Discover | Scan plugin directories and registries for available plugins |
| Load | Load plugin metadata, validate manifest, check compatibility |
| Resolve | Resolve dependencies between plugins, detect conflicts |
| Activate | Initialize the plugin, register its components with the host |
| Deactivate | Gracefully shut down the plugin, unregister components |
Plugin Architecture
Key Components
| Component | Description |
|---|---|
PluginHost | Manages the plugin lifecycle and provides the host API |
PluginRegistry | Stores and indexes all discovered plugins |
DependencyResolver | Resolves plugin dependencies using semver |
PluginSandbox | Resource 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
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
| Problem | Solution |
|---|---|
| "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 working | Verify hot_reload = true in config, check file watcher |
| Plugin crashes on activate | Check logs with --log-level debug, run health check |