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}