use std::collections::HashSet;
use std::fmt;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::error::PluginError;
#[async_trait]
pub trait Plugin: Send + Sync {
fn config(&self) -> &PluginConfig;
async fn initialize(&self) -> Result<(), Box<PluginError>> {
Ok(())
}
async fn shutdown(&self) -> Result<(), Box<PluginError>> {
Ok(())
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PluginConfig {
pub name: String,
pub kind: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub author: Option<String>,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub hooks: Vec<String>,
#[serde(default)]
pub mode: PluginMode,
#[serde(default = "default_priority")]
pub priority: i32,
#[serde(default)]
pub on_error: OnError,
#[serde(default)]
pub capabilities: HashSet<String>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub conditions: Vec<PluginCondition>,
#[serde(default)]
pub config: Option<serde_json::Value>,
}
impl PluginConfig {
pub fn passes_conditions(&self, extensions: &crate::hooks::payload::Extensions) -> bool {
if self.conditions.is_empty() {
return true;
}
let server_id = extensions.mcp.as_ref().and_then(|m| {
m.tool
.as_ref()
.and_then(|t| t.server_id.as_deref())
.or_else(|| m.resource.as_ref().and_then(|r| r.server_id.as_deref()))
.or_else(|| m.prompt.as_ref().and_then(|p| p.server_id.as_deref()))
});
let tenant_id = extensions
.security
.as_ref()
.and_then(|s| s.subject.as_ref())
.and_then(|sub| sub.claims.get("tenant"))
.map(|s| s.as_str());
let entity_name = extensions
.meta
.as_ref()
.and_then(|m| m.entity_name.as_deref());
let entity_type = extensions
.meta
.as_ref()
.and_then(|m| m.entity_type.as_deref());
let (tool, prompt, resource) = match entity_type {
Some("tool") => (entity_name, None, None),
Some("prompt") => (None, entity_name, None),
Some("resource") => (None, None, entity_name),
_ => (None, None, None),
};
let agent = extensions
.agent
.as_ref()
.and_then(|a| a.agent_id.as_deref());
let user = extensions
.security
.as_ref()
.and_then(|s| s.subject.as_ref())
.and_then(|sub| sub.id.as_deref());
let content_type = extensions
.mcp
.as_ref()
.and_then(|m| m.resource.as_ref())
.and_then(|r| r.mime_type.as_deref());
let ctx = MatchContext {
server_id,
tenant_id,
tool,
prompt,
resource,
agent,
user,
content_type,
};
self.conditions.iter().any(|c| c.matches(&ctx))
}
}
fn default_priority() -> i32 {
100
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PluginCondition {
#[serde(default)]
pub server_ids: Option<HashSet<String>>,
#[serde(default)]
pub tenant_ids: Option<HashSet<String>>,
#[serde(default)]
pub tools: Option<HashSet<String>>,
#[serde(default)]
pub prompts: Option<HashSet<String>>,
#[serde(default)]
pub resources: Option<HashSet<String>>,
#[serde(default)]
pub agents: Option<HashSet<String>>,
#[serde(default)]
pub user_patterns: Option<Vec<String>>,
#[serde(default)]
pub content_types: Option<Vec<String>>,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct MatchContext<'a> {
pub server_id: Option<&'a str>,
pub tenant_id: Option<&'a str>,
pub tool: Option<&'a str>,
pub prompt: Option<&'a str>,
pub resource: Option<&'a str>,
pub agent: Option<&'a str>,
pub user: Option<&'a str>,
pub content_type: Option<&'a str>,
}
impl PluginCondition {
pub fn matches(&self, ctx: &MatchContext<'_>) -> bool {
let MatchContext {
server_id,
tenant_id,
tool,
prompt,
resource,
agent,
user,
content_type,
} = *ctx;
let check_set = |field: &Option<HashSet<String>>, value: Option<&str>| -> bool {
match field {
None => true, Some(set) => match value {
Some(v) => set.contains(v),
None => false, },
}
};
let check_patterns = |field: &Option<Vec<String>>, value: Option<&str>| -> bool {
match field {
None => true,
Some(patterns) => match value {
Some(v) => patterns
.iter()
.any(|p| wildmatch::WildMatch::new(p).matches(v)),
None => false,
},
}
};
let check_list = |field: &Option<Vec<String>>, value: Option<&str>| -> bool {
match field {
None => true,
Some(list) => match value {
Some(v) => list.iter().any(|s| s == v),
None => false,
},
}
};
check_set(&self.server_ids, server_id)
&& check_set(&self.tenant_ids, tenant_id)
&& check_set(&self.tools, tool)
&& check_set(&self.prompts, prompt)
&& check_set(&self.resources, resource)
&& check_set(&self.agents, agent)
&& check_patterns(&self.user_patterns, user)
&& check_list(&self.content_types, content_type)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum PluginMode {
#[default]
Sequential,
Transform,
Audit,
Concurrent,
FireAndForget,
Disabled,
}
impl PluginMode {
pub fn can_block(&self) -> bool {
matches!(self, Self::Sequential | Self::Concurrent)
}
pub fn can_modify(&self) -> bool {
matches!(self, Self::Sequential | Self::Transform)
}
pub fn is_awaited(&self) -> bool {
!matches!(self, Self::FireAndForget | Self::Disabled)
}
}
impl fmt::Display for PluginMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Sequential => write!(f, "sequential"),
Self::Transform => write!(f, "transform"),
Self::Audit => write!(f, "audit"),
Self::Concurrent => write!(f, "concurrent"),
Self::FireAndForget => write!(f, "fire_and_forget"),
Self::Disabled => write!(f, "disabled"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum OnError {
#[default]
Fail,
Ignore,
Disable,
}
impl fmt::Display for OnError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Fail => write!(f, "fail"),
Self::Ignore => write!(f, "ignore"),
Self::Disable => write!(f, "disable"),
}
}
}