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#[non_exhaustive]
10#[derive(Debug, thiserror::Error)]
11pub enum PluginError {
12    /// The plugin manifest (`plugin.toml`) is missing or cannot be parsed.
13    #[error("invalid plugin manifest: {0}")]
14    InvalidManifest(String),
15
16    /// The plugin name is invalid (empty, contains path separators, or reserved).
17    #[error("invalid plugin name {name:?}: {reason}")]
18    InvalidName { name: String, reason: String },
19
20    /// A plugin MCP entry declares a command not in `mcp.allowed_commands`.
21    #[error(
22        "plugin MCP server {id:?} spawns command {command:?}, which is not in mcp.allowed_commands"
23    )]
24    DisallowedMcpCommand { id: String, command: String },
25
26    /// A plugin skill name conflicts with an existing managed (user) skill.
27    #[error("plugin skill {name:?} conflicts with an existing managed skill")]
28    SkillNameConflictWithManaged { name: String },
29
30    /// A plugin skill name conflicts with a compile-time bundled skill.
31    #[error("plugin skill {name:?} conflicts with a bundled skill")]
32    SkillNameConflictWithBundled { name: String },
33
34    /// A plugin skill name conflicts with a skill from another installed plugin.
35    #[error("plugin skill {name:?} conflicts with skill from plugin {plugin:?}")]
36    SkillNameConflictWithPlugin { name: String, plugin: String },
37
38    /// A plugin's `[config]` section contains a key not in the tighten-only safelist.
39    #[error(
40        "plugin config overlay key {key:?} is not allowed; only tools.blocked_commands, tools.allowed_commands, and skills.disambiguation_threshold may be overridden"
41    )]
42    UnsafeOverlay { key: String },
43
44    /// A `[[skills]] path` entry does not contain a valid `SKILL.md` file.
45    #[error("plugin skill entry at {path:?} does not contain a SKILL.md file")]
46    SkillEntryMissing { path: PathBuf },
47
48    /// The plugin directory does not exist or cannot be read.
49    #[error("plugin not found: {name}")]
50    NotFound { name: String },
51
52    /// The plugin source path or URL is invalid.
53    #[error("invalid plugin source {path:?}: {reason}")]
54    InvalidSource { path: String, reason: String },
55
56    /// A filesystem operation failed.
57    #[error("filesystem error at {path}: {source}")]
58    Io {
59        path: PathBuf,
60        #[source]
61        source: std::io::Error,
62    },
63
64    /// TOML serialization/deserialization error.
65    #[error("TOML error: {0}")]
66    Toml(#[from] toml::de::Error),
67
68    /// TOML serialization error.
69    #[error("TOML serialization error: {0}")]
70    TomlSer(#[from] toml::ser::Error),
71
72    /// The SKILL.md semantic compliance scan rejected the skill.
73    ///
74    /// Raised for Stage-1 regex matches treated as blocking in the ephemeral install
75    /// context (`add_remote_ephemeral`). Stage-2 LLM scan returns a string error
76    /// via the command layer, not this variant.
77    #[error("skill {skill:?} failed semantic compliance scan: {reason}")]
78    SemanticViolation { skill: String, reason: String },
79
80    /// SHA-256 digest of a downloaded archive does not match the expected value.
81    ///
82    /// Returned by [`crate::manager::PluginManager::add_remote`] when the caller
83    /// supplies an `expected_sha256` and the download does not match.
84    /// Do not install or extract the archive — it may have been tampered with.
85    #[error(
86        "plugin archive integrity check failed: expected sha256={expected}, got sha256={actual}"
87    )]
88    IntegrityCheckFailed { expected: String, actual: String },
89
90    /// HTTP download of a remote plugin archive failed.
91    #[error("failed to download plugin from {url}: {reason}")]
92    DownloadFailed { url: String, reason: String },
93
94    /// Attempted to remove or disable a plugin that other enabled plugins depend on.
95    #[error("Plugin '{name}' is required by: {dependents}. Disable them first:\n{hints}")]
96    DependencyRequired {
97        /// The plugin that was requested to be removed or disabled.
98        name: String,
99        /// Comma-separated list of dependent plugin names.
100        dependents: String,
101        /// Newline-separated disable hints, one per dependent.
102        hints: String,
103    },
104
105    /// A dependency cycle was detected while enabling a plugin.
106    #[error("dependency cycle detected while enabling plugin '{name}': {cycle}")]
107    DependencyCycle {
108        /// The plugin being enabled when the cycle was found.
109        name: String,
110        /// Human-readable description of the cycle path.
111        cycle: String,
112    },
113
114    /// A declared dependency plugin is not installed.
115    #[error("plugin '{name}' requires dependency '{dependency}' which is not installed")]
116    MissingDependency {
117        /// The plugin declaring the dependency.
118        name: String,
119        /// The missing dependency name.
120        dependency: String,
121    },
122
123    /// The URL supplied to `--plugin-url` uses a non-HTTPS scheme.
124    ///
125    /// Only `https://` is accepted for ephemeral plugin loading. `http://` and
126    /// any other scheme are rejected to prevent MITM attacks against session-scoped
127    /// plugin archives (security invariant INV-EPH-1).
128    #[error("insecure URL scheme for --plugin-url: {0} (only https:// is accepted)")]
129    InsecureUrl(String),
130}