Plugin System#
oxo-flow supports a compile-time + config-based plugin architecture. Plugins
are Rust crates implementing standard traits, registered via TOML configuration
files. Plugin manifests are integrity-checked using keyed SHA-256 signatures
(SHA256(key ‖ message)). Note: this is not a true HMAC (RFC 2104) and does
not provide cryptographic authentication.
Quick Start#
1. Implement a plugin trait#
use oxo_flow_core::plugin::RulePlugin;
use oxo_flow_core::rule::Rule;
use oxo_flow_core::error::Result;
use std::collections::HashMap;
struct MyPlugin;
impl RulePlugin for MyPlugin {
fn rule_type(&self) -> &str { "my-custom-type" }
fn build_command(&self, rule: &Rule, values: &HashMap<String, String>) -> Result<String> {
Ok(format!("custom_tool --input {}", rule.input[0]))
}
fn validate(&self, rule: &Rule) -> Result<()> { Ok(()) }
}
2. Register with the registry#
use oxo_flow_core::plugin::PluginRegistry;
let mut registry = PluginRegistry::default();
registry.register_rule(Box::new(MyPlugin));
registry.add_trusted_key("key-001", "your-secret-key");
3. Declare in your workflow#
[plugins]
rules = ["my-custom-type"]
executor = "slurm-custom"
reports = ["native-pdf"]
trusted_keys_file = ".oxo-flow/trusted_keys.toml"
Available Traits#
| Trait | Purpose | Key Method |
|---|---|---|
RulePlugin |
Custom rule types | build_command() |
ExecutorPlugin |
Custom executors | submit() |
ReportPlugin |
Custom report renderers | render() |
Plugin Discovery#
Plugins are discovered from .plugin.toml files in:
~/.oxo-flow/plugins/— user-level (shared across projects)<project>/.oxo-flow/plugins/— project-level
# my-plugin.plugin.toml
name = "my-custom-type"
version = "1.0.0"
plugin_type = "rule"
description = "Custom rule for specialized analysis"
author = "Your Name"
command_template = "custom_tool {input} > {output}"
[signature]
key_id = "key-001"
value = "a1b2c3d4..."
Signature Verification#
Each plugin manifest can include a [signature] section with a keyed
SHA-256 digest. During discovery, the registry verifies signatures against
trusted keys when any are configured; plugins with invalid signatures are
skipped, while unsigned plugins and plugins with signatures but no
configured trusted keys are loaded with a warning:
registry.add_trusted_key("key-001", "shared-secret-key");
registry.discover(Some(project_dir))?; // invalid signatures are skipped
API Reference#
PluginRegistry#
impl PluginRegistry {
pub fn register_rule(&mut self, plugin: Box<dyn RulePlugin>);
pub fn register_executor(&mut self, plugin: Box<dyn ExecutorPlugin>);
pub fn register_report(&mut self, plugin: Box<dyn ReportPlugin>);
pub fn add_trusted_key(&mut self, key_id: &str, key: &str);
pub fn discover(&mut self, project_dir: Option<&Path>) -> Result<usize>;
pub fn find_rule(&self, rule_type: &str) -> Option<&dyn RulePlugin>;
pub fn find_executor(&self, backend: &str) -> Option<&dyn ExecutorPlugin>;
pub fn find_report(&self, renderer: &str) -> Option<&dyn ReportPlugin>;
}
PluginsConfig (TOML [plugins] section)#
pub struct PluginsConfig {
pub rules: Vec<String>, // Rule plugin types to enable
pub executor: Option<String>, // Executor plugin to use
pub reports: Vec<String>, // Report plugins to enable
pub trusted_keys_file: Option<String>, // Path to keys file
}
Dynamic Loading (Subprocess Protocol)#
oxo-flow uses a subprocess-based plugin protocol — the safe, portable alternative to shared-library loading. Plugins are standalone executables that communicate via JSON over stdin/stdout.
Protocol#
Input (stdin, JSON):
{
"rule": "my_analysis",
"inputs": ["raw/sample.fq"],
"outputs": ["results/output.txt"],
"command": "custom_tool --input raw/sample.fq > results/output.txt",
"config": {"reference": "/data/ref.fa"},
"params": {}
}
Output (stdout, JSON):
{
"success": true,
"command": "custom_tool --input raw/sample.fq --threads 8 > results/output.txt",
"errors": [],
"logs": ["Processing sample..."],
"exit_code": 0
}
Executing a plugin#
use oxo_flow_core::plugin::{execute_plugin_subprocess, PluginInput};
let input = PluginInput {
rule: "my_rule".into(),
inputs: vec!["in.txt".into()],
outputs: vec!["out.txt".into()],
command: Some("custom_tool {input} > {output}".into()),
config: HashMap::new(),
params: HashMap::new(),
};
let output = execute_plugin_subprocess(
Path::new("/path/to/plugin"),
&input,
30, // timeout seconds
).await?;
Writing a plugin#
Plugins can be written in any language. A minimal Python plugin:
#!/usr/bin/env python3
import sys, json
input_data = json.load(sys.stdin)
# Transform the command
command = input_data.get("command", "").replace("{input}", input_data["inputs"][0])
command = command.replace("{output}", input_data["outputs"][0])
output = {"success": True, "command": command, "errors": [], "logs": [], "exit_code": 0}
json.dump(output, sys.stdout)