Skip to main content

codineer_plugins/
manifest.rs

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}