Skip to main content

zeph_plugins/
manifest.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! `plugin.toml` manifest schema.
5
6use serde::{Deserialize, Serialize};
7
8fn default_config_table() -> toml::Value {
9    toml::Value::Table(toml::map::Map::new())
10}
11
12/// Top-level `plugin.toml` manifest.
13///
14/// # Example
15///
16/// ```toml
17/// [plugin]
18/// name = "git-workflows"
19/// version = "0.1.0"
20/// description = "Git workflow skills and MCP git server"
21/// auto_update = false
22///
23/// [[skills]]
24/// path = "skills/git-commit"
25///
26/// [[mcp.servers]]
27/// id = "git"
28/// command = "mcp-git"
29/// args = ["--repo", "."]
30///
31/// [config.tools]
32/// blocked_commands = ["git push --force"]
33/// ```
34#[derive(Debug, Clone, Deserialize, Serialize)]
35pub struct PluginManifest {
36    /// Plugin metadata.
37    pub plugin: PluginMeta,
38    /// Skill entries bundled by this plugin.
39    #[serde(default)]
40    pub skills: Vec<SkillEntry>,
41    /// MCP server declarations.
42    #[serde(default)]
43    pub mcp: McpSection,
44    /// Tighten-only config overlay applied at startup.
45    #[serde(default = "default_config_table")]
46    pub config: toml::Value,
47}
48
49/// Plugin metadata from the `[plugin]` table.
50#[derive(Debug, Clone, Deserialize, Serialize)]
51pub struct PluginMeta {
52    /// Canonical plugin name. Must be a valid identifier: `[a-z0-9][a-z0-9-]*`.
53    pub name: String,
54    /// Plugin version (informational).
55    pub version: String,
56    /// Short description shown in `zeph plugin list`.
57    #[serde(default)]
58    pub description: String,
59    /// When `true`, the plugin manager may update this plugin automatically on startup.
60    /// Defaults to `false` — updates are opt-in to avoid unintended breaking changes.
61    #[serde(default)]
62    pub auto_update: bool,
63    /// Names of other plugins this plugin depends on.
64    ///
65    /// The plugin manager ensures all listed plugins are enabled before enabling this one.
66    /// Removing or disabling a plugin that other enabled plugins depend on is refused with
67    /// a clear error listing the dependents.
68    #[serde(default)]
69    pub dependencies: Vec<String>,
70    // zeph-version field intentionally omitted: version-gating is deferred to a future release
71    // when the semver crate is added as a workspace dependency.
72}
73
74/// A single skill entry in `[[skills]]`.
75#[derive(Debug, Clone, Deserialize, Serialize)]
76pub struct SkillEntry {
77    /// Relative path from the plugin root to the skill directory containing `SKILL.md`.
78    pub path: String,
79}
80
81/// The `[mcp]` section.
82#[derive(Debug, Clone, Default, Deserialize, Serialize)]
83pub struct McpSection {
84    /// MCP server declarations in `[[mcp.servers]]`.
85    #[serde(default)]
86    pub servers: Vec<PluginMcpServer>,
87}
88
89/// A single MCP server declaration in `[[mcp.servers]]`.
90#[derive(Debug, Clone, Deserialize, Serialize)]
91pub struct PluginMcpServer {
92    /// Unique server ID. Used to de-duplicate across plugins.
93    pub id: String,
94    /// Command to spawn (stdio transport). Must be in `mcp.allowed_commands`.
95    #[serde(default)]
96    pub command: Option<String>,
97    /// Arguments passed to `command`.
98    #[serde(default)]
99    pub args: Vec<String>,
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn auto_update_defaults_to_false_when_absent() {
108        let toml = r#"
109[plugin]
110name = "my-plugin"
111version = "0.1.0"
112"#;
113        let manifest: PluginManifest = toml::from_str(toml).unwrap();
114        assert!(!manifest.plugin.auto_update);
115    }
116
117    #[test]
118    fn auto_update_true_parsed_correctly() {
119        let toml = r#"
120[plugin]
121name = "my-plugin"
122version = "0.1.0"
123auto_update = true
124"#;
125        let manifest: PluginManifest = toml::from_str(toml).unwrap();
126        assert!(manifest.plugin.auto_update);
127    }
128
129    #[test]
130    fn auto_update_false_parsed_correctly() {
131        let toml = r#"
132[plugin]
133name = "my-plugin"
134version = "0.1.0"
135auto_update = false
136"#;
137        let manifest: PluginManifest = toml::from_str(toml).unwrap();
138        assert!(!manifest.plugin.auto_update);
139    }
140}