use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use uuid::Uuid;
use crate::context::PluginContext;
use crate::hooks::payload::{Extensions, PluginPayload};
use crate::hooks::trait_def::HookTypeDef;
use crate::hooks::HookType;
use crate::plugin::{Plugin, PluginConfig, PluginMode};
#[derive(Clone)]
pub struct PluginRef {
plugin: Arc<dyn Plugin>,
trusted_config: PluginConfig,
id: Uuid,
disabled: Arc<AtomicBool>,
}
impl PluginRef {
pub fn new(plugin: Arc<dyn Plugin>, trusted_config: PluginConfig) -> Self {
Self {
plugin,
trusted_config,
id: Uuid::new_v4(),
disabled: Arc::new(AtomicBool::new(false)),
}
}
pub fn trusted_config(&self) -> &PluginConfig {
&self.trusted_config
}
pub fn plugin(&self) -> &Arc<dyn Plugin> {
&self.plugin
}
pub fn id(&self) -> Uuid {
self.id
}
pub fn name(&self) -> &str {
&self.trusted_config.name
}
pub fn mode(&self) -> PluginMode {
if self.disabled.load(Ordering::Acquire) {
PluginMode::Disabled
} else {
self.trusted_config.mode
}
}
pub fn disable(&self) {
self.disabled.store(true, Ordering::Release);
}
pub fn is_disabled(&self) -> bool {
self.disabled.load(Ordering::Acquire)
}
pub fn priority(&self) -> i32 {
self.trusted_config.priority
}
}
#[async_trait::async_trait]
pub trait AnyHookHandler: Send + Sync {
async fn invoke(
&self,
payload: &dyn PluginPayload,
extensions: &Extensions,
ctx: &mut PluginContext,
) -> Result<Box<dyn std::any::Any + Send + Sync>, Box<crate::error::PluginError>>;
fn hook_type_name(&self) -> &'static str;
}
#[derive(Clone)]
pub struct HookEntry {
pub plugin_ref: Arc<PluginRef>,
pub handler: Arc<dyn AnyHookHandler>,
}
#[derive(Clone)]
pub struct PluginRegistry {
plugins: HashMap<String, Arc<PluginRef>>,
hook_index: HashMap<HookType, Vec<HookEntry>>,
}
impl PluginRegistry {
pub fn new() -> Self {
Self {
plugins: HashMap::new(),
hook_index: HashMap::new(),
}
}
pub fn register<H: HookTypeDef>(
&mut self,
plugin: Arc<dyn Plugin>,
config: PluginConfig,
handler: Arc<dyn AnyHookHandler>,
) -> Result<(), String> {
self.register_for_names_inner(plugin, config, handler, &[H::NAME])
}
pub fn register_for_names<H: HookTypeDef>(
&mut self,
plugin: Arc<dyn Plugin>,
config: PluginConfig,
handler: Arc<dyn AnyHookHandler>,
names: &[&str],
) -> Result<(), String> {
self.register_for_names_inner(plugin, config, handler, names)
}
pub fn register_for_names_with_handler(
&mut self,
plugin: Arc<dyn Plugin>,
config: PluginConfig,
handler: Arc<dyn AnyHookHandler>,
names: &[&str],
) -> Result<(), String> {
self.register_for_names_inner(plugin, config, handler, names)
}
pub fn register_multi_handler(
&mut self,
plugin: Arc<dyn Plugin>,
config: PluginConfig,
handlers: Vec<(&str, Arc<dyn AnyHookHandler>)>,
) -> Result<(), String> {
let name = config.name.clone();
if self.plugins.contains_key(&name) {
return Err(format!("plugin '{}' is already registered", name));
}
let plugin_ref = Arc::new(PluginRef::new(plugin, config));
for (hook_name, handler) in &handlers {
let hook_type = HookType::new(*hook_name);
let entry = HookEntry {
plugin_ref: Arc::clone(&plugin_ref),
handler: Arc::clone(handler),
};
self.hook_index.entry(hook_type).or_default().push(entry);
}
for (hook_name, _) in &handlers {
let hook_type = HookType::new(*hook_name);
if let Some(entries) = self.hook_index.get_mut(&hook_type) {
entries.sort_by_key(|e| e.plugin_ref.priority());
}
}
self.plugins.insert(name, plugin_ref);
Ok(())
}
fn register_for_names_inner(
&mut self,
plugin: Arc<dyn Plugin>,
config: PluginConfig,
handler: Arc<dyn AnyHookHandler>,
names: &[&str],
) -> Result<(), String> {
let name = config.name.clone();
if self.plugins.contains_key(&name) {
return Err(format!("plugin '{}' is already registered", name));
}
let plugin_ref = Arc::new(PluginRef::new(plugin, config));
for hook_name in names {
let hook_type = HookType::new(*hook_name);
let entry = HookEntry {
plugin_ref: Arc::clone(&plugin_ref),
handler: Arc::clone(&handler),
};
self.hook_index.entry(hook_type).or_default().push(entry);
}
for hook_name in names {
let hook_type = HookType::new(*hook_name);
if let Some(entries) = self.hook_index.get_mut(&hook_type) {
entries.sort_by_key(|e| e.plugin_ref.priority());
}
}
self.plugins.insert(name, plugin_ref);
Ok(())
}
pub fn unregister(&mut self, name: &str) -> Option<Arc<PluginRef>> {
let plugin_ref = self.plugins.remove(name)?;
for entries in self.hook_index.values_mut() {
entries.retain(|e| e.plugin_ref.name() != name);
}
self.hook_index.retain(|_, entries| !entries.is_empty());
Some(plugin_ref)
}
pub fn get(&self, name: &str) -> Option<Arc<PluginRef>> {
self.plugins.get(name).map(Arc::clone)
}
pub fn entries_for_hook(&self, hook_type: &HookType) -> &[HookEntry] {
self.hook_index
.get(hook_type)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn has_hooks_for(&self, hook_type: &HookType) -> bool {
self.hook_index
.get(hook_type)
.map(|v| !v.is_empty())
.unwrap_or(false)
}
pub fn plugin_count(&self) -> usize {
self.plugins.len()
}
pub fn plugin_names(&self) -> Vec<String> {
self.plugins.keys().cloned().collect()
}
pub fn entries_for_plugin(&self, plugin_name: &str) -> Vec<(String, HookEntry)> {
let mut out = Vec::new();
for (hook_type, entries) in &self.hook_index {
for entry in entries {
if entry.plugin_ref.name() == plugin_name {
out.push((hook_type.as_str().to_string(), entry.clone()));
}
}
}
out
}
}
impl Default for PluginRegistry {
fn default() -> Self {
Self::new()
}
}
pub type GroupedHookEntries = (
Vec<HookEntry>,
Vec<HookEntry>,
Vec<HookEntry>,
Vec<HookEntry>,
Vec<HookEntry>,
);
pub fn group_by_mode(entries: &[HookEntry]) -> GroupedHookEntries {
let mut sequential = Vec::new();
let mut transform = Vec::new();
let mut audit = Vec::new();
let mut concurrent = Vec::new();
let mut fire_and_forget = Vec::new();
for entry in entries {
match entry.plugin_ref.mode() {
PluginMode::Sequential => sequential.push(entry.clone()),
PluginMode::Transform => transform.push(entry.clone()),
PluginMode::Audit => audit.push(entry.clone()),
PluginMode::Concurrent => concurrent.push(entry.clone()),
PluginMode::FireAndForget => fire_and_forget.push(entry.clone()),
PluginMode::Disabled => {}, }
}
(sequential, transform, audit, concurrent, fire_and_forget)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::PluginError;
use crate::hooks::payload::PluginPayload;
use crate::hooks::PluginResult;
use async_trait::async_trait;
#[derive(Debug, Clone)]
#[allow(dead_code)] struct TestPayload {
value: String,
}
crate::impl_plugin_payload!(TestPayload);
struct TestHandler;
#[async_trait]
impl AnyHookHandler for TestHandler {
async fn invoke(
&self,
_payload: &dyn PluginPayload,
_extensions: &Extensions,
_ctx: &mut PluginContext,
) -> Result<Box<dyn std::any::Any + Send + Sync>, Box<PluginError>> {
let result: PluginResult<TestPayload> = PluginResult::allow();
Ok(crate::executor::erase_result(result))
}
fn hook_type_name(&self) -> &'static str {
"test_hook"
}
}
struct TestPlugin {
cfg: PluginConfig,
}
fn make_config(name: &str, hooks: Vec<&str>, priority: i32) -> PluginConfig {
PluginConfig {
name: name.to_string(),
kind: "test".to_string(),
description: None,
author: None,
version: None,
hooks: hooks.into_iter().map(String::from).collect(),
mode: PluginMode::Sequential,
priority,
on_error: Default::default(),
capabilities: Default::default(),
tags: Vec::new(),
conditions: Vec::new(),
config: None,
}
}
impl TestPlugin {
fn new(cfg: PluginConfig) -> Self {
Self { cfg }
}
}
#[async_trait]
impl Plugin for TestPlugin {
fn config(&self) -> &PluginConfig {
&self.cfg
}
async fn initialize(&self) -> Result<(), Box<PluginError>> {
Ok(())
}
async fn shutdown(&self) -> Result<(), Box<PluginError>> {
Ok(())
}
}
#[test]
fn test_register_typed_and_lookup() {
let mut reg = PluginRegistry::new();
let config = make_config("test-plugin", vec!["test_hook"], 10);
let plugin = Arc::new(TestPlugin::new(config.clone()));
let handler: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);
reg.register_for_names_inner(plugin, config, handler, &["test_hook"])
.unwrap();
assert_eq!(reg.plugin_count(), 1);
assert!(reg.get("test-plugin").is_some());
assert!(reg.has_hooks_for(&HookType::new("test_hook")));
assert!(!reg.has_hooks_for(&HookType::new("other_hook")));
let entries = reg.entries_for_hook(&HookType::new("test_hook"));
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].plugin_ref.name(), "test-plugin");
}
#[test]
fn test_register_for_multiple_names() {
let mut reg = PluginRegistry::new();
let config = make_config("cmf-plugin", vec![], 10);
let plugin = Arc::new(TestPlugin::new(config.clone()));
let handler: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);
reg.register_for_names_inner(
plugin,
config,
handler,
&[
"cmf.tool_pre_invoke",
"cmf.tool_post_invoke",
"cmf.llm_input",
],
)
.unwrap();
assert_eq!(reg.plugin_count(), 1);
assert!(reg.has_hooks_for(&HookType::new("cmf.tool_pre_invoke")));
assert!(reg.has_hooks_for(&HookType::new("cmf.tool_post_invoke")));
assert!(reg.has_hooks_for(&HookType::new("cmf.llm_input")));
assert!(!reg.has_hooks_for(&HookType::new("cmf.llm_output")));
}
#[test]
fn test_duplicate_registration_fails() {
let mut reg = PluginRegistry::new();
let c1 = make_config("dup", vec![], 10);
let c2 = make_config("dup", vec![], 20);
let p1 = Arc::new(TestPlugin::new(c1.clone()));
let p2 = Arc::new(TestPlugin::new(c2.clone()));
let h1: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);
let h2: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);
assert!(reg
.register_for_names_inner(p1, c1, h1, &["hook_a"])
.is_ok());
assert!(reg
.register_for_names_inner(p2, c2, h2, &["hook_a"])
.is_err());
}
#[test]
fn test_priority_ordering_uses_trusted_config() {
let mut reg = PluginRegistry::new();
let c_low = make_config("low", vec![], 100);
let c_high = make_config("high", vec![], 10);
let p_low = Arc::new(TestPlugin::new(c_low.clone()));
let p_high = Arc::new(TestPlugin::new(c_high.clone()));
let h1: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);
let h2: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);
reg.register_for_names_inner(p_low, c_low, h1, &["hook_a"])
.unwrap();
reg.register_for_names_inner(p_high, c_high, h2, &["hook_a"])
.unwrap();
let entries = reg.entries_for_hook(&HookType::new("hook_a"));
assert_eq!(entries[0].plugin_ref.name(), "high"); assert_eq!(entries[1].plugin_ref.name(), "low"); }
#[test]
fn test_unregister() {
let mut reg = PluginRegistry::new();
let config = make_config("removable", vec![], 10);
let plugin = Arc::new(TestPlugin::new(config.clone()));
let handler: Arc<dyn AnyHookHandler> = Arc::new(TestHandler);
reg.register_for_names_inner(plugin, config, handler, &["hook_a"])
.unwrap();
assert_eq!(reg.plugin_count(), 1);
reg.unregister("removable");
assert_eq!(reg.plugin_count(), 0);
assert!(!reg.has_hooks_for(&HookType::new("hook_a")));
}
#[test]
fn test_plugin_ref_id_is_unique() {
let c1 = make_config("a", vec![], 10);
let c2 = make_config("b", vec![], 10);
let p1 = Arc::new(TestPlugin::new(c1.clone()));
let p2 = Arc::new(TestPlugin::new(c2.clone()));
let ref1 = PluginRef::new(p1, c1);
let ref2 = PluginRef::new(p2, c2);
assert_ne!(ref1.id(), ref2.id());
}
#[test]
fn test_tampered_plugin_config_ignored() {
let trusted = make_config("sneaky", vec![], 100);
let mut tampered = trusted.clone();
tampered.priority = 1;
let plugin = Arc::new(TestPlugin::new(tampered));
let plugin_ref = PluginRef::new(plugin, trusted);
assert_eq!(plugin_ref.priority(), 100);
}
#[tokio::test]
async fn test_handler_invoke() {
let handler = TestHandler;
let payload = TestPayload {
value: "test".into(),
};
let ext = Extensions::default();
let mut ctx = PluginContext::new();
let result = handler
.invoke(&payload as &dyn PluginPayload, &ext, &mut ctx)
.await
.unwrap();
let fields = crate::executor::extract_erased(result).unwrap();
assert!(fields.continue_processing);
}
}