Skip to main content

pleiades_agent_plugins/
manifest.rs

1use std::collections::BTreeSet;
2use std::path::{Path, PathBuf};
3
4use serde::{Deserialize, Serialize};
5
6pub const MANIFEST_FILE_NAME: &str = "plugin.json";
7pub const MANIFEST_RELATIVE_PATH: &str = ".pleiades-plugin/plugin.json";
8
9#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
10pub struct PluginHooks {
11    #[serde(rename = "PreToolUse", default)]
12    pub pre_tool_use: Vec<String>,
13    #[serde(rename = "PostToolUse", default)]
14    pub post_tool_use: Vec<String>,
15    #[serde(rename = "PostToolUseFailure", default)]
16    pub post_tool_use_failure: Vec<String>,
17}
18
19impl PluginHooks {
20    pub fn is_empty(&self) -> bool {
21        self.pre_tool_use.is_empty()
22            && self.post_tool_use.is_empty()
23            && self.post_tool_use_failure.is_empty()
24    }
25
26    pub fn merged_with(&self, other: &Self) -> Self {
27        let mut merged = self.clone();
28        merged
29            .pre_tool_use
30            .extend(other.pre_tool_use.iter().cloned());
31        merged
32            .post_tool_use
33            .extend(other.post_tool_use.iter().cloned());
34        merged
35            .post_tool_use_failure
36            .extend(other.post_tool_use_failure.iter().cloned());
37        merged
38    }
39}
40
41#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
42pub struct PluginLifecycle {
43    #[serde(rename = "Init", default)]
44    pub init: Vec<String>,
45    #[serde(rename = "Shutdown", default)]
46    pub shutdown: Vec<String>,
47}
48
49impl PluginLifecycle {
50    pub fn is_empty(&self) -> bool {
51        self.init.is_empty() && self.shutdown.is_empty()
52    }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
56#[serde(rename_all = "lowercase")]
57pub enum PluginPermission {
58    Read,
59    Write,
60    Execute,
61}
62
63impl PluginPermission {
64    pub fn as_str(self) -> &'static str {
65        match self {
66            Self::Read => "read",
67            Self::Write => "write",
68            Self::Execute => "execute",
69        }
70    }
71}
72
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74pub struct PluginToolManifest {
75    pub name: String,
76    pub description: String,
77    #[serde(rename = "inputSchema")]
78    pub input_schema: serde_json::Value,
79    pub command: String,
80    #[serde(default)]
81    pub args: Vec<String>,
82    #[serde(rename = "requiredPermission", default = "default_tool_permission")]
83    pub required_permission: PluginToolPermission,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
87#[serde(rename_all = "kebab-case")]
88pub enum PluginToolPermission {
89    ReadOnly,
90    WorkspaceWrite,
91    DangerFullAccess,
92}
93
94impl PluginToolPermission {
95    pub fn as_str(self) -> &'static str {
96        match self {
97            Self::ReadOnly => "read-only",
98            Self::WorkspaceWrite => "workspace-write",
99            Self::DangerFullAccess => "danger-full-access",
100        }
101    }
102}
103
104fn default_tool_permission() -> PluginToolPermission {
105    PluginToolPermission::DangerFullAccess
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109pub struct PluginCommandManifest {
110    pub name: String,
111    pub description: String,
112    pub command: String,
113}
114
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
116pub struct PluginManifest {
117    pub name: String,
118    pub version: String,
119    pub description: String,
120    #[serde(default)]
121    pub permissions: Vec<String>,
122    #[serde(rename = "defaultEnabled", default)]
123    pub default_enabled: bool,
124    #[serde(default)]
125    pub hooks: PluginHooks,
126    #[serde(default)]
127    pub lifecycle: PluginLifecycle,
128    #[serde(default)]
129    pub tools: Vec<PluginToolManifest>,
130    #[serde(default)]
131    pub commands: Vec<PluginCommandManifest>,
132}
133
134impl PluginManifest {
135    pub fn validate(&self, root: &Path) -> Result<(), PluginError> {
136        let mut errors = Vec::new();
137
138        if self.name.trim().is_empty() {
139            errors.push(PluginErrorKind::EmptyField { field: "name" });
140        }
141        if self.version.trim().is_empty() {
142            errors.push(PluginErrorKind::EmptyField { field: "version" });
143        }
144        if self.description.trim().is_empty() {
145            errors.push(PluginErrorKind::EmptyField {
146                field: "description",
147            });
148        }
149
150        let mut seen_perms = BTreeSet::new();
151        for perm in &self.permissions {
152            if !seen_perms.insert(perm.clone()) {
153                errors.push(PluginErrorKind::DuplicatePermission {
154                    permission: perm.clone(),
155                });
156            }
157            match perm.as_str() {
158                "read" | "write" | "execute" => {}
159                other => errors.push(PluginErrorKind::InvalidPermission {
160                    permission: other.to_string(),
161                }),
162            }
163        }
164
165        Self::validate_paths(root, &self.hooks.pre_tool_use, "hook", &mut errors);
166        Self::validate_paths(root, &self.hooks.post_tool_use, "hook", &mut errors);
167        Self::validate_paths(root, &self.hooks.post_tool_use_failure, "hook", &mut errors);
168        Self::validate_paths(root, &self.lifecycle.init, "lifecycle", &mut errors);
169        Self::validate_paths(root, &self.lifecycle.shutdown, "lifecycle", &mut errors);
170
171        if !errors.is_empty() {
172            return Err(PluginError::Validation(errors));
173        }
174        Ok(())
175    }
176
177    fn validate_paths(
178        root: &Path,
179        paths: &[String],
180        kind: &str,
181        errors: &mut Vec<PluginErrorKind>,
182    ) {
183        for path in paths {
184            let full_path = if Path::new(path).is_absolute() {
185                PathBuf::from(path)
186            } else {
187                root.join(path)
188            };
189            if !full_path.exists() {
190                errors.push(PluginErrorKind::MissingPath {
191                    kind: kind.to_string(),
192                    path: full_path,
193                });
194            }
195        }
196    }
197
198    pub fn load_from_directory(root: &Path) -> Result<Self, PluginError> {
199        let manifest_path = find_manifest_path(root)?;
200        let content = std::fs::read_to_string(&manifest_path).map_err(|e| {
201            PluginError::Io(std::io::Error::new(
202                std::io::ErrorKind::NotFound,
203                format!(
204                    "plugin manifest not found at {}: {e}",
205                    manifest_path.display()
206                ),
207            ))
208        })?;
209
210        let manifest: Self = serde_json::from_str(&content)?;
211        manifest.validate(root)?;
212        Ok(manifest)
213    }
214}
215
216fn find_manifest_path(root: &Path) -> Result<PathBuf, PluginError> {
217    let direct = root.join(MANIFEST_FILE_NAME);
218    if direct.exists() {
219        return Ok(direct);
220    }
221    let packaged = root.join(MANIFEST_RELATIVE_PATH);
222    if packaged.exists() {
223        return Ok(packaged);
224    }
225    Err(PluginError::Io(std::io::Error::new(
226        std::io::ErrorKind::NotFound,
227        format!(
228            "plugin manifest not found at {} or {}",
229            direct.display(),
230            packaged.display()
231        ),
232    )))
233}
234
235#[derive(Debug)]
236pub enum PluginError {
237    Io(std::io::Error),
238    Json(serde_json::Error),
239    Validation(Vec<PluginErrorKind>),
240    NotFound(String),
241    CommandFailed(String),
242}
243
244impl std::fmt::Display for PluginError {
245    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246        match self {
247            Self::Io(e) => write!(f, "{e}"),
248            Self::Json(e) => write!(f, "{e}"),
249            Self::Validation(errors) => {
250                for (i, e) in errors.iter().enumerate() {
251                    if i > 0 {
252                        write!(f, "; ")?;
253                    }
254                    write!(f, "{e}")?;
255                }
256                Ok(())
257            }
258            Self::NotFound(msg) | Self::CommandFailed(msg) => write!(f, "{msg}"),
259        }
260    }
261}
262
263impl std::error::Error for PluginError {}
264
265impl From<std::io::Error> for PluginError {
266    fn from(e: std::io::Error) -> Self {
267        Self::Io(e)
268    }
269}
270
271impl From<serde_json::Error> for PluginError {
272    fn from(e: serde_json::Error) -> Self {
273        Self::Json(e)
274    }
275}
276
277#[derive(Debug, Clone)]
278pub enum PluginErrorKind {
279    EmptyField { field: &'static str },
280    InvalidPermission { permission: String },
281    DuplicatePermission { permission: String },
282    MissingPath { kind: String, path: PathBuf },
283}
284
285impl std::fmt::Display for PluginErrorKind {
286    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287        match self {
288            Self::EmptyField { field } => write!(f, "plugin manifest {field} cannot be empty"),
289            Self::InvalidPermission { permission } => {
290                write!(
291                    f,
292                    "invalid permission `{permission}`, must be read/write/execute"
293                )
294            }
295            Self::DuplicatePermission { permission } => {
296                write!(f, "duplicate permission `{permission}`")
297            }
298            Self::MissingPath { kind, path } => {
299                write!(f, "{kind} path `{}` does not exist", path.display())
300            }
301        }
302    }
303}