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