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}