systemprompt_models/services/
plugin.rs1use std::fmt;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use systemprompt_identifiers::PluginId;
6
7use super::hooks::HookEventsConfig;
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 hooks: HookEventsConfig,
83 #[serde(default)]
84 pub scripts: Vec<PluginScript>,
85}
86
87#[derive(Debug, Clone, Default, Serialize, Deserialize)]
88pub struct PluginComponentRef {
89 #[serde(default)]
90 pub source: ComponentSource,
91 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub filter: Option<ComponentFilter>,
93 #[serde(default, skip_serializing_if = "Vec::is_empty")]
94 pub include: Vec<String>,
95 #[serde(default, skip_serializing_if = "Vec::is_empty")]
96 pub exclude: Vec<String>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct PluginScript {
101 pub name: String,
102 pub source: String,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct PluginAuthor {
107 pub name: String,
108 pub email: String,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
112pub struct PluginSummary {
113 pub id: PluginId,
114 pub name: String,
115 pub display_name: String,
116 pub enabled: bool,
117 pub skill_count: usize,
118 pub agent_count: usize,
119}
120
121impl From<&PluginConfig> for PluginSummary {
122 fn from(config: &PluginConfig) -> Self {
123 Self {
124 id: config.id.clone(),
125 name: config.name.clone(),
126 display_name: config.name.clone(),
127 enabled: config.enabled,
128 skill_count: config.skills.include.len(),
129 agent_count: config.agents.include.len(),
130 }
131 }
132}
133
134impl PluginConfig {
135 pub fn validate(&self, key: &str) -> anyhow::Result<()> {
136 let id_str = self.id.as_str();
137 if id_str.len() < 3 || id_str.len() > 50 {
138 anyhow::bail!("Plugin '{}': id must be between 3 and 50 characters", key);
139 }
140
141 if !id_str
142 .chars()
143 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
144 {
145 anyhow::bail!(
146 "Plugin '{}': id must be lowercase alphanumeric with hyphens only (kebab-case)",
147 key
148 );
149 }
150
151 if self.version.is_empty() {
152 anyhow::bail!("Plugin '{}': version must not be empty", key);
153 }
154
155 Self::validate_component_ref(&self.skills, key, "skills")?;
156 Self::validate_component_ref(&self.agents, key, "agents")?;
157 self.hooks.validate()?;
158
159 Ok(())
160 }
161
162 fn validate_component_ref(
163 component: &PluginComponentRef,
164 key: &str,
165 field: &str,
166 ) -> anyhow::Result<()> {
167 if component.source == ComponentSource::Explicit && component.include.is_empty() {
168 anyhow::bail!(
169 "Plugin '{}': {}.source is 'explicit' but {}.include is empty",
170 key,
171 field,
172 field
173 );
174 }
175
176 Ok(())
177 }
178}