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