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::{is_literal_command, MANIFEST_FILE_NAME};
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 path = root.join(MANIFEST_FILE_NAME);
79 if path.exists() {
80 return Ok(path);
81 }
82 Err(PluginError::NotFound(format!(
83 "plugin manifest not found at {}",
84 path.display(),
85 )))
86}
87
88fn build_plugin_manifest(
89 root: &Path,
90 raw: RawPluginManifest,
91) -> Result<PluginManifest, PluginError> {
92 let mut errors = Vec::new();
93
94 validate_required_manifest_field("name", &raw.name, &mut errors);
95 validate_required_manifest_field("version", &raw.version, &mut errors);
96 validate_required_manifest_field("description", &raw.description, &mut errors);
97
98 let permissions = build_manifest_permissions(&raw.permissions, &mut errors);
99 validate_command_entries(root, raw.hooks.pre_tool_use.iter(), "hook", &mut errors);
100 validate_command_entries(root, raw.hooks.post_tool_use.iter(), "hook", &mut errors);
101 validate_command_entries(
102 root,
103 raw.lifecycle.init.iter(),
104 "lifecycle command",
105 &mut errors,
106 );
107 validate_command_entries(
108 root,
109 raw.lifecycle.shutdown.iter(),
110 "lifecycle command",
111 &mut errors,
112 );
113 let tools = build_manifest_tools(root, raw.tools, &mut errors);
114 let commands = build_manifest_commands(root, raw.commands, &mut errors);
115
116 if !errors.is_empty() {
117 return Err(PluginError::ManifestValidation(errors));
118 }
119
120 Ok(PluginManifest {
121 name: raw.name,
122 version: raw.version,
123 description: raw.description,
124 permissions,
125 default_enabled: raw.default_enabled,
126 hooks: raw.hooks,
127 lifecycle: raw.lifecycle,
128 tools,
129 commands,
130 })
131}
132
133fn validate_required_manifest_field(
134 field: &'static str,
135 value: &str,
136 errors: &mut Vec<PluginManifestValidationError>,
137) {
138 if value.trim().is_empty() {
139 errors.push(PluginManifestValidationError::EmptyField { field });
140 }
141}
142
143fn build_manifest_permissions(
144 permissions: &[String],
145 errors: &mut Vec<PluginManifestValidationError>,
146) -> Vec<PluginPermission> {
147 let mut seen = BTreeSet::new();
148 let mut validated = Vec::new();
149
150 for permission in permissions {
151 let permission = permission.trim();
152 if permission.is_empty() {
153 errors.push(PluginManifestValidationError::EmptyEntryField {
154 kind: "permission",
155 field: "value",
156 name: None,
157 });
158 continue;
159 }
160 if !seen.insert(permission.to_string()) {
161 errors.push(PluginManifestValidationError::DuplicatePermission {
162 permission: permission.to_string(),
163 });
164 continue;
165 }
166 match PluginPermission::parse(permission) {
167 Some(permission) => validated.push(permission),
168 None => errors.push(PluginManifestValidationError::InvalidPermission {
169 permission: permission.to_string(),
170 }),
171 }
172 }
173
174 validated
175}
176
177fn build_manifest_tools(
178 root: &Path,
179 tools: Vec<RawPluginToolManifest>,
180 errors: &mut Vec<PluginManifestValidationError>,
181) -> Vec<PluginToolManifest> {
182 let mut seen = BTreeSet::new();
183 let mut validated = Vec::new();
184
185 for tool in tools {
186 let name = tool.name.trim().to_string();
187 if name.is_empty() {
188 errors.push(PluginManifestValidationError::EmptyEntryField {
189 kind: "tool",
190 field: "name",
191 name: None,
192 });
193 continue;
194 }
195 if !seen.insert(name.clone()) {
196 errors.push(PluginManifestValidationError::DuplicateEntry { kind: "tool", name });
197 continue;
198 }
199 if tool.description.trim().is_empty() {
200 errors.push(PluginManifestValidationError::EmptyEntryField {
201 kind: "tool",
202 field: "description",
203 name: Some(name.clone()),
204 });
205 }
206 if tool.command.trim().is_empty() {
207 errors.push(PluginManifestValidationError::EmptyEntryField {
208 kind: "tool",
209 field: "command",
210 name: Some(name.clone()),
211 });
212 } else {
213 validate_command_entry(root, &tool.command, "tool", errors);
214 }
215 if !tool.input_schema.is_object() {
216 errors.push(PluginManifestValidationError::InvalidToolInputSchema {
217 tool_name: name.clone(),
218 });
219 }
220 let Some(required_permission) =
221 PluginToolPermission::parse(tool.required_permission.trim())
222 else {
223 errors.push(
224 PluginManifestValidationError::InvalidToolRequiredPermission {
225 tool_name: name.clone(),
226 permission: tool.required_permission.trim().to_string(),
227 },
228 );
229 continue;
230 };
231
232 validated.push(PluginToolManifest {
233 name,
234 description: tool.description,
235 input_schema: tool.input_schema,
236 command: tool.command,
237 args: tool.args,
238 required_permission,
239 });
240 }
241
242 validated
243}
244
245fn build_manifest_commands(
246 root: &Path,
247 commands: Vec<PluginCommandManifest>,
248 errors: &mut Vec<PluginManifestValidationError>,
249) -> Vec<PluginCommandManifest> {
250 let mut seen = BTreeSet::new();
251 let mut validated = Vec::new();
252
253 for command in commands {
254 let name = command.name.trim().to_string();
255 if name.is_empty() {
256 errors.push(PluginManifestValidationError::EmptyEntryField {
257 kind: "command",
258 field: "name",
259 name: None,
260 });
261 continue;
262 }
263 if !seen.insert(name.clone()) {
264 errors.push(PluginManifestValidationError::DuplicateEntry {
265 kind: "command",
266 name,
267 });
268 continue;
269 }
270 if command.description.trim().is_empty() {
271 errors.push(PluginManifestValidationError::EmptyEntryField {
272 kind: "command",
273 field: "description",
274 name: Some(name.clone()),
275 });
276 }
277 if command.command.trim().is_empty() {
278 errors.push(PluginManifestValidationError::EmptyEntryField {
279 kind: "command",
280 field: "command",
281 name: Some(name.clone()),
282 });
283 } else {
284 validate_command_entry(root, &command.command, "command", errors);
285 }
286 validated.push(command);
287 }
288
289 validated
290}
291
292fn validate_command_entries<'a>(
293 root: &Path,
294 entries: impl Iterator<Item = &'a String>,
295 kind: &'static str,
296 errors: &mut Vec<PluginManifestValidationError>,
297) {
298 for entry in entries {
299 validate_command_entry(root, entry, kind, errors);
300 }
301}
302
303fn validate_command_entry(
304 root: &Path,
305 entry: &str,
306 kind: &'static str,
307 errors: &mut Vec<PluginManifestValidationError>,
308) {
309 if entry.trim().is_empty() {
310 errors.push(PluginManifestValidationError::EmptyEntryField {
311 kind,
312 field: "command",
313 name: None,
314 });
315 return;
316 }
317 if is_literal_command(entry) {
318 return;
319 }
320
321 let path = if Path::new(entry).is_absolute() {
322 PathBuf::from(entry)
323 } else {
324 root.join(entry)
325 };
326 if !path.exists() {
327 errors.push(PluginManifestValidationError::MissingPath { kind, path });
328 }
329}