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