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}