1use std::collections::BTreeSet;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::constants::{MANIFEST_FILE_NAME, MANIFEST_RELATIVE_PATH};
9use crate::error::{PluginError, PluginManifestValidationError};
10use crate::types::{
11 PluginCommandManifest, PluginHooks, PluginLifecycle, PluginManifest, PluginPermission,
12 PluginToolManifest, PluginToolPermission,
13};
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16struct RawPluginManifest {
17 pub name: String,
18 pub version: String,
19 pub description: String,
20 #[serde(default)]
21 pub permissions: Vec<String>,
22 #[serde(rename = "defaultEnabled", default)]
23 pub default_enabled: bool,
24 #[serde(default)]
25 pub hooks: PluginHooks,
26 #[serde(default)]
27 pub lifecycle: PluginLifecycle,
28 #[serde(default)]
29 pub tools: Vec<RawPluginToolManifest>,
30 #[serde(default)]
31 pub commands: Vec<PluginCommandManifest>,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35struct RawPluginToolManifest {
36 pub name: String,
37 pub description: String,
38 #[serde(rename = "inputSchema")]
39 pub input_schema: Value,
40 pub command: String,
41 #[serde(default)]
42 pub args: Vec<String>,
43 #[serde(
44 rename = "requiredPermission",
45 default = "default_tool_permission_label"
46 )]
47 pub required_permission: String,
48}
49
50fn default_tool_permission_label() -> String {
51 "danger-full-access".to_string()
52}
53
54pub fn load_plugin_from_directory(root: &Path) -> Result<PluginManifest, PluginError> {
55 load_manifest_from_directory(root)
56}
57
58fn load_manifest_from_directory(root: &Path) -> Result<PluginManifest, PluginError> {
59 let manifest_path = plugin_manifest_path(root)?;
60 load_manifest_from_path(root, &manifest_path)
61}
62
63fn load_manifest_from_path(
64 root: &Path,
65 manifest_path: &Path,
66) -> Result<PluginManifest, PluginError> {
67 let contents = fs::read_to_string(manifest_path).map_err(|error| {
68 PluginError::NotFound(format!(
69 "plugin manifest not found at {}: {error}",
70 manifest_path.display()
71 ))
72 })?;
73 let raw_manifest: RawPluginManifest = serde_json::from_str(&contents)?;
74 build_plugin_manifest(root, raw_manifest)
75}
76
77pub(crate) fn plugin_manifest_path(root: &Path) -> Result<PathBuf, PluginError> {
78 let direct_path = root.join(MANIFEST_FILE_NAME);
79 if direct_path.exists() {
80 return Ok(direct_path);
81 }
82
83 let packaged_path = root.join(MANIFEST_RELATIVE_PATH);
84 if packaged_path.exists() {
85 return Ok(packaged_path);
86 }
87
88 Err(PluginError::NotFound(format!(
89 "plugin manifest not found at {} or {}",
90 direct_path.display(),
91 packaged_path.display()
92 )))
93}
94
95fn build_plugin_manifest(
96 root: &Path,
97 raw: RawPluginManifest,
98) -> Result<PluginManifest, PluginError> {
99 let mut errors = Vec::new();
100
101 validate_required_manifest_field("name", &raw.name, &mut errors);
102 validate_required_manifest_field("version", &raw.version, &mut errors);
103 validate_required_manifest_field("description", &raw.description, &mut errors);
104
105 let permissions = build_manifest_permissions(&raw.permissions, &mut errors);
106 validate_command_entries(root, raw.hooks.pre_tool_use.iter(), "hook", &mut errors);
107 validate_command_entries(root, raw.hooks.post_tool_use.iter(), "hook", &mut errors);
108 validate_command_entries(
109 root,
110 raw.lifecycle.init.iter(),
111 "lifecycle command",
112 &mut errors,
113 );
114 validate_command_entries(
115 root,
116 raw.lifecycle.shutdown.iter(),
117 "lifecycle command",
118 &mut errors,
119 );
120 let tools = build_manifest_tools(root, raw.tools, &mut errors);
121 let commands = build_manifest_commands(root, raw.commands, &mut errors);
122
123 if !errors.is_empty() {
124 return Err(PluginError::ManifestValidation(errors));
125 }
126
127 Ok(PluginManifest {
128 name: raw.name,
129 version: raw.version,
130 description: raw.description,
131 permissions,
132 default_enabled: raw.default_enabled,
133 hooks: raw.hooks,
134 lifecycle: raw.lifecycle,
135 tools,
136 commands,
137 })
138}
139
140fn validate_required_manifest_field(
141 field: &'static str,
142 value: &str,
143 errors: &mut Vec<PluginManifestValidationError>,
144) {
145 if value.trim().is_empty() {
146 errors.push(PluginManifestValidationError::EmptyField { field });
147 }
148}
149
150fn build_manifest_permissions(
151 permissions: &[String],
152 errors: &mut Vec<PluginManifestValidationError>,
153) -> Vec<PluginPermission> {
154 let mut seen = BTreeSet::new();
155 let mut validated = Vec::new();
156
157 for permission in permissions {
158 let permission = permission.trim();
159 if permission.is_empty() {
160 errors.push(PluginManifestValidationError::EmptyEntryField {
161 kind: "permission",
162 field: "value",
163 name: None,
164 });
165 continue;
166 }
167 if !seen.insert(permission.to_string()) {
168 errors.push(PluginManifestValidationError::DuplicatePermission {
169 permission: permission.to_string(),
170 });
171 continue;
172 }
173 match PluginPermission::parse(permission) {
174 Some(permission) => validated.push(permission),
175 None => errors.push(PluginManifestValidationError::InvalidPermission {
176 permission: permission.to_string(),
177 }),
178 }
179 }
180
181 validated
182}
183
184fn build_manifest_tools(
185 root: &Path,
186 tools: Vec<RawPluginToolManifest>,
187 errors: &mut Vec<PluginManifestValidationError>,
188) -> Vec<PluginToolManifest> {
189 let mut seen = BTreeSet::new();
190 let mut validated = Vec::new();
191
192 for tool in tools {
193 let name = tool.name.trim().to_string();
194 if name.is_empty() {
195 errors.push(PluginManifestValidationError::EmptyEntryField {
196 kind: "tool",
197 field: "name",
198 name: None,
199 });
200 continue;
201 }
202 if !seen.insert(name.clone()) {
203 errors.push(PluginManifestValidationError::DuplicateEntry { kind: "tool", name });
204 continue;
205 }
206 if tool.description.trim().is_empty() {
207 errors.push(PluginManifestValidationError::EmptyEntryField {
208 kind: "tool",
209 field: "description",
210 name: Some(name.clone()),
211 });
212 }
213 if tool.command.trim().is_empty() {
214 errors.push(PluginManifestValidationError::EmptyEntryField {
215 kind: "tool",
216 field: "command",
217 name: Some(name.clone()),
218 });
219 } else {
220 validate_command_entry(root, &tool.command, "tool", errors);
221 }
222 if !tool.input_schema.is_object() {
223 errors.push(PluginManifestValidationError::InvalidToolInputSchema {
224 tool_name: name.clone(),
225 });
226 }
227 let Some(required_permission) =
228 PluginToolPermission::parse(tool.required_permission.trim())
229 else {
230 errors.push(
231 PluginManifestValidationError::InvalidToolRequiredPermission {
232 tool_name: name.clone(),
233 permission: tool.required_permission.trim().to_string(),
234 },
235 );
236 continue;
237 };
238
239 validated.push(PluginToolManifest {
240 name,
241 description: tool.description,
242 input_schema: tool.input_schema,
243 command: tool.command,
244 args: tool.args,
245 required_permission,
246 });
247 }
248
249 validated
250}
251
252fn build_manifest_commands(
253 root: &Path,
254 commands: Vec<PluginCommandManifest>,
255 errors: &mut Vec<PluginManifestValidationError>,
256) -> Vec<PluginCommandManifest> {
257 let mut seen = BTreeSet::new();
258 let mut validated = Vec::new();
259
260 for command in commands {
261 let name = command.name.trim().to_string();
262 if name.is_empty() {
263 errors.push(PluginManifestValidationError::EmptyEntryField {
264 kind: "command",
265 field: "name",
266 name: None,
267 });
268 continue;
269 }
270 if !seen.insert(name.clone()) {
271 errors.push(PluginManifestValidationError::DuplicateEntry {
272 kind: "command",
273 name,
274 });
275 continue;
276 }
277 if command.description.trim().is_empty() {
278 errors.push(PluginManifestValidationError::EmptyEntryField {
279 kind: "command",
280 field: "description",
281 name: Some(name.clone()),
282 });
283 }
284 if command.command.trim().is_empty() {
285 errors.push(PluginManifestValidationError::EmptyEntryField {
286 kind: "command",
287 field: "command",
288 name: Some(name.clone()),
289 });
290 } else {
291 validate_command_entry(root, &command.command, "command", errors);
292 }
293 validated.push(command);
294 }
295
296 validated
297}
298
299use crate::constants::is_literal_command;
300
301fn validate_command_entries<'a>(
302 root: &Path,
303 entries: impl Iterator<Item = &'a String>,
304 kind: &'static str,
305 errors: &mut Vec<PluginManifestValidationError>,
306) {
307 for entry in entries {
308 validate_command_entry(root, entry, kind, errors);
309 }
310}
311
312fn validate_command_entry(
313 root: &Path,
314 entry: &str,
315 kind: &'static str,
316 errors: &mut Vec<PluginManifestValidationError>,
317) {
318 if entry.trim().is_empty() {
319 errors.push(PluginManifestValidationError::EmptyEntryField {
320 kind,
321 field: "command",
322 name: None,
323 });
324 return;
325 }
326 if is_literal_command(entry) {
327 return;
328 }
329
330 let path = if Path::new(entry).is_absolute() {
331 PathBuf::from(entry)
332 } else {
333 root.join(entry)
334 };
335 if !path.exists() {
336 errors.push(PluginManifestValidationError::MissingPath { kind, path });
337 }
338}