Skip to main content

zeph_plugins/
error.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Error types for plugin operations.
5
6use std::path::PathBuf;
7
8/// Errors that can occur during plugin install, remove, or list operations.
9#[derive(Debug, thiserror::Error)]
10pub enum PluginError {
11    /// The plugin manifest (`plugin.toml`) is missing or cannot be parsed.
12    #[error("invalid plugin manifest: {0}")]
13    InvalidManifest(String),
14
15    /// The plugin name is invalid (empty, contains path separators, or reserved).
16    #[error("invalid plugin name {name:?}: {reason}")]
17    InvalidName { name: String, reason: String },
18
19    /// A plugin MCP entry declares a command not in `mcp.allowed_commands`.
20    #[error(
21        "plugin MCP server {id:?} spawns command {command:?}, which is not in mcp.allowed_commands"
22    )]
23    DisallowedMcpCommand { id: String, command: String },
24
25    /// A plugin skill name conflicts with an existing managed (user) skill.
26    #[error("plugin skill {name:?} conflicts with an existing managed skill")]
27    SkillNameConflictWithManaged { name: String },
28
29    /// A plugin skill name conflicts with a compile-time bundled skill.
30    #[error("plugin skill {name:?} conflicts with a bundled skill")]
31    SkillNameConflictWithBundled { name: String },
32
33    /// A plugin skill name conflicts with a skill from another installed plugin.
34    #[error("plugin skill {name:?} conflicts with skill from plugin {plugin:?}")]
35    SkillNameConflictWithPlugin { name: String, plugin: String },
36
37    /// A plugin's `[config]` section contains a key not in the tighten-only safelist.
38    #[error(
39        "plugin config overlay key {key:?} is not allowed; only tools.blocked_commands, tools.allowed_commands, and skills.disambiguation_threshold may be overridden"
40    )]
41    UnsafeOverlay { key: String },
42
43    /// A `[[skills]] path` entry does not contain a valid `SKILL.md` file.
44    #[error("plugin skill entry at {path:?} does not contain a SKILL.md file")]
45    SkillEntryMissing { path: PathBuf },
46
47    /// The plugin directory does not exist or cannot be read.
48    #[error("plugin not found: {name}")]
49    NotFound { name: String },
50
51    /// The plugin source path or URL is invalid.
52    #[error("invalid plugin source {path:?}: {reason}")]
53    InvalidSource { path: String, reason: String },
54
55    /// A filesystem operation failed.
56    #[error("filesystem error at {path}: {source}")]
57    Io {
58        path: PathBuf,
59        #[source]
60        source: std::io::Error,
61    },
62
63    /// TOML serialization/deserialization error.
64    #[error("TOML error: {0}")]
65    Toml(#[from] toml::de::Error),
66
67    /// TOML serialization error.
68    #[error("TOML serialization error: {0}")]
69    TomlSer(#[from] toml::ser::Error),
70
71    /// The SKILL.md semantic scan determined the skill is non-compliant.
72    ///
73    /// Only raised when `skill.semantic_scan = true` in agent config and the LLM
74    /// classifier returns `compliant: false`. Stage-1 regex matches are advisory
75    /// (warnings) and never produce this error.
76    #[error("skill {skill:?} failed semantic compliance scan: {reason}")]
77    SemanticViolation { skill: String, reason: String },
78
79    /// SHA-256 digest of a downloaded archive does not match the expected value.
80    ///
81    /// Returned by [`crate::manager::PluginManager::add_remote`] when the caller
82    /// supplies an `expected_sha256` and the download does not match.
83    /// Do not install or extract the archive — it may have been tampered with.
84    #[error(
85        "plugin archive integrity check failed: expected sha256={expected}, got sha256={actual}"
86    )]
87    IntegrityCheckFailed { expected: String, actual: String },
88
89    /// HTTP download of a remote plugin archive failed.
90    #[error("failed to download plugin from {url}: {reason}")]
91    DownloadFailed { url: String, reason: String },
92
93    /// Attempted to remove or disable a plugin that other enabled plugins depend on.
94    #[error("Plugin '{name}' is required by: {dependents}. Disable them first:\n{hints}")]
95    DependencyRequired {
96        /// The plugin that was requested to be removed or disabled.
97        name: String,
98        /// Comma-separated list of dependent plugin names.
99        dependents: String,
100        /// Newline-separated disable hints, one per dependent.
101        hints: String,
102    },
103
104    /// A dependency cycle was detected while enabling a plugin.
105    #[error("dependency cycle detected while enabling plugin '{name}': {cycle}")]
106    DependencyCycle {
107        /// The plugin being enabled when the cycle was found.
108        name: String,
109        /// Human-readable description of the cycle path.
110        cycle: String,
111    },
112
113    /// A declared dependency plugin is not installed.
114    #[error("plugin '{name}' requires dependency '{dependency}' which is not installed")]
115    MissingDependency {
116        /// The plugin declaring the dependency.
117        name: String,
118        /// The missing dependency name.
119        dependency: String,
120    },
121}