systemprompt_models/services/
plugin.rs1use std::fmt;
10
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13use systemprompt_identifiers::PluginId;
14
15use crate::errors::ConfigValidationError;
16
17const fn default_true() -> bool {
18 true
19}
20
21#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
22#[serde(rename_all = "lowercase")]
23pub enum ComponentSource {
24 #[default]
25 Instance,
26 Explicit,
27}
28
29impl fmt::Display for ComponentSource {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 match self {
32 Self::Instance => write!(f, "instance"),
33 Self::Explicit => write!(f, "explicit"),
34 }
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
39#[serde(rename_all = "lowercase")]
40pub enum ComponentFilter {
41 Enabled,
42}
43
44impl fmt::Display for ComponentFilter {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match self {
47 Self::Enabled => write!(f, "enabled"),
48 }
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct PluginConfigFile {
54 pub plugin: PluginConfig,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
58pub struct PluginVariableDef {
59 pub name: String,
60 #[serde(default)]
61 pub description: String,
62 #[serde(default = "default_true")]
63 pub required: bool,
64 #[serde(default)]
65 pub secret: bool,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub example: Option<String>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct PluginConfig {
72 pub id: PluginId,
73 pub name: String,
74 pub description: String,
75 pub version: String,
76 #[serde(default = "default_true")]
77 pub enabled: bool,
78 pub author: PluginAuthor,
79 pub keywords: Vec<String>,
80 pub license: String,
81 pub category: String,
82
83 pub skills: PluginComponentRef,
84 pub agents: PluginComponentRef,
85 #[serde(default)]
86 pub mcp_servers: PluginComponentRef,
87 #[serde(default)]
88 pub content_sources: PluginComponentRef,
89 #[serde(default)]
90 pub scripts: Vec<PluginScript>,
91}
92
93#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
94pub struct PluginComponentRef {
95 #[serde(default)]
96 pub source: ComponentSource,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub filter: Option<ComponentFilter>,
99 #[serde(default, skip_serializing_if = "Vec::is_empty")]
100 pub include: Vec<String>,
101 #[serde(default, skip_serializing_if = "Vec::is_empty")]
102 pub exclude: Vec<String>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct PluginScript {
107 pub name: String,
108 pub source: String,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct PluginAuthor {
113 pub name: String,
114 pub email: String,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
118pub struct PluginSummary {
119 pub id: PluginId,
120 pub name: String,
121 pub display_name: String,
122 pub enabled: bool,
123 pub skill_count: usize,
124 pub agent_count: usize,
125}
126
127impl From<&PluginConfig> for PluginSummary {
128 fn from(config: &PluginConfig) -> Self {
129 Self {
130 id: config.id.clone(),
131 name: config.name.clone(),
132 display_name: config.name.clone(),
133 enabled: config.enabled,
134 skill_count: config.skills.include.len(),
135 agent_count: config.agents.include.len(),
136 }
137 }
138}
139
140impl PluginConfig {
141 pub fn validate(&self, key: &str) -> Result<(), ConfigValidationError> {
142 let id_str = self.id.as_str();
143 if id_str.len() < 3 || id_str.len() > 50 {
144 return Err(ConfigValidationError::invalid_field(format!(
145 "Plugin '{key}': id must be between 3 and 50 characters"
146 )));
147 }
148
149 if !id_str
150 .chars()
151 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
152 {
153 return Err(ConfigValidationError::invalid_field(format!(
154 "Plugin '{key}': id must be lowercase alphanumeric with hyphens only (kebab-case)"
155 )));
156 }
157
158 if self.version.is_empty() {
159 return Err(ConfigValidationError::required(format!(
160 "Plugin '{key}': version must not be empty"
161 )));
162 }
163
164 Self::validate_component_ref(&self.skills, key, "skills")?;
165 Self::validate_component_ref(&self.agents, key, "agents")?;
166
167 Ok(())
168 }
169
170 fn validate_component_ref(
171 component: &PluginComponentRef,
172 key: &str,
173 field: &str,
174 ) -> Result<(), ConfigValidationError> {
175 if component.source == ComponentSource::Explicit && component.include.is_empty() {
176 return Err(ConfigValidationError::invalid_field(format!(
177 "Plugin '{key}': {field}.source is 'explicit' but {field}.include is empty"
178 )));
179 }
180
181 Ok(())
182 }
183}