Skip to main content

zeph_common/
trust_level.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Trust tier enum for skill execution permissions.
5//!
6//! [`SkillTrustLevel`] is the single source of truth for trust-level semantics across all
7//! Zeph crates. It lives in `zeph-common` so both `zeph-skills` and `zeph-tools` can depend
8//! on it without introducing a circular dependency.
9
10use std::fmt;
11use std::str::FromStr;
12
13use serde::{Deserialize, Serialize};
14
15/// Trust tier controlling what a skill is allowed to do.
16///
17/// The ordering from most to least trusted is: `Trusted` → `Verified` → `Quarantined` →
18/// `Blocked`. Use [`SkillTrustLevel::severity`] to compare levels numerically, or
19/// [`SkillTrustLevel::min_trust`] to find the least-trusted of two levels.
20///
21/// # Examples
22///
23/// ```rust
24/// use zeph_common::SkillTrustLevel;
25///
26/// let level = SkillTrustLevel::Quarantined;
27/// assert!(level.is_active());
28/// assert_eq!(level.severity(), 2);
29/// ```
30#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Deserialize, Serialize)]
31#[serde(rename_all = "lowercase")]
32#[non_exhaustive]
33pub enum SkillTrustLevel {
34    /// Built-in or user-audited skill: full tool access.
35    Trusted,
36    /// Signature or hash verified: default tool access.
37    Verified,
38    /// Newly imported or hash-mismatch: restricted tool access.
39    #[default]
40    Quarantined,
41    /// Explicitly disabled by user or auto-blocked by anomaly detector.
42    Blocked,
43}
44
45impl SkillTrustLevel {
46    /// Trust level to assume when a skill has no entry in the trust map.
47    ///
48    /// A missing entry means "never classified yet" (e.g. persistence not wired, or a
49    /// transient trust-map read failure), not "known untrusted" — callers must not fall
50    /// back to [`SkillTrustLevel::default`] ([`Quarantined`](Self::Quarantined)) for this
51    /// case, as that would misclassify legitimately trusted, already-vetted skills.
52    ///
53    /// # Examples
54    ///
55    /// ```rust
56    /// use zeph_common::SkillTrustLevel;
57    ///
58    /// let trust_levels: std::collections::HashMap<String, SkillTrustLevel> =
59    ///     std::collections::HashMap::new();
60    /// let trust = trust_levels
61    ///     .get("some-skill")
62    ///     .copied()
63    ///     .unwrap_or(SkillTrustLevel::MISSING_ENTRY_FALLBACK);
64    /// assert_eq!(trust, SkillTrustLevel::Trusted);
65    /// ```
66    pub const MISSING_ENTRY_FALLBACK: Self = Self::Trusted;
67
68    /// Ordered severity: lower value = more trusted.
69    ///
70    /// # Examples
71    ///
72    /// ```rust
73    /// use zeph_common::SkillTrustLevel;
74    ///
75    /// assert!(SkillTrustLevel::Trusted.severity() < SkillTrustLevel::Blocked.severity());
76    /// ```
77    #[must_use]
78    pub const fn severity(self) -> u8 {
79        match self {
80            Self::Trusted => 0,
81            Self::Verified => 1,
82            Self::Quarantined => 2,
83            Self::Blocked => 3,
84        }
85    }
86
87    /// Returns the least-trusted (highest severity) of two levels.
88    ///
89    /// # Examples
90    ///
91    /// ```rust
92    /// use zeph_common::SkillTrustLevel;
93    ///
94    /// let result = SkillTrustLevel::Trusted.min_trust(SkillTrustLevel::Quarantined);
95    /// assert_eq!(result, SkillTrustLevel::Quarantined);
96    /// ```
97    #[must_use]
98    pub const fn min_trust(self, other: Self) -> Self {
99        if self.severity() >= other.severity() {
100            self
101        } else {
102            other
103        }
104    }
105
106    /// Inverse of [`severity`](Self::severity): reconstructs a level from its ordinal.
107    ///
108    /// Any value `>= 3` maps to [`Blocked`](Self::Blocked) — the most restrictive level —
109    /// so a corrupted or out-of-range stored ordinal fails closed rather than open.
110    ///
111    /// # Examples
112    ///
113    /// ```rust
114    /// use zeph_common::SkillTrustLevel;
115    ///
116    /// assert_eq!(SkillTrustLevel::from_severity(0), SkillTrustLevel::Trusted);
117    /// assert_eq!(SkillTrustLevel::from_severity(3), SkillTrustLevel::Blocked);
118    /// assert_eq!(SkillTrustLevel::from_severity(255), SkillTrustLevel::Blocked);
119    /// ```
120    #[must_use]
121    pub const fn from_severity(v: u8) -> Self {
122        match v {
123            0 => Self::Trusted,
124            1 => Self::Verified,
125            2 => Self::Quarantined,
126            _ => Self::Blocked,
127        }
128    }
129
130    /// Returns the string representation used for database storage.
131    #[must_use]
132    pub const fn as_str(self) -> &'static str {
133        match self {
134            Self::Trusted => "trusted",
135            Self::Verified => "verified",
136            Self::Quarantined => "quarantined",
137            Self::Blocked => "blocked",
138        }
139    }
140
141    /// Returns `true` if the level is not `Blocked`.
142    ///
143    /// # Examples
144    ///
145    /// ```rust
146    /// use zeph_common::SkillTrustLevel;
147    ///
148    /// assert!(SkillTrustLevel::Quarantined.is_active());
149    /// assert!(!SkillTrustLevel::Blocked.is_active());
150    /// ```
151    #[must_use]
152    pub const fn is_active(self) -> bool {
153        !matches!(self, Self::Blocked)
154    }
155
156    /// Returns `true` for trust levels that must never appear on a listing surface with no
157    /// trust-annotation mechanism — [`Blocked`](Self::Blocked) (explicitly disabled) and
158    /// [`Quarantined`](Self::Quarantined) (unreviewed / hash-mismatched) alike.
159    ///
160    /// This is the *hide* strategy for surfaces that cannot show a trust level inline, e.g.
161    /// the mention-picker catalog (`SkillCatalogItem` carries only `name`/`description`, no
162    /// trust field). Contrast with the *annotate* strategy used by the XML skill-prompt
163    /// catalog (`zeph_skills::prompt::format_skills_catalog`'s `trust_levels` parameter),
164    /// which keeps a `Quarantined`/`Blocked` skill visible with a `trust="..."` attribute so
165    /// the model/operator can still name it and promote it — that surface intentionally does
166    /// *not* use this method. This method says nothing about matching-candidate selection or
167    /// per-turn dispatch gating (`TurnTrustFloor`, weakest-link trust fold) either — those are
168    /// separate concerns with their own filtering logic.
169    ///
170    /// # Examples
171    ///
172    /// ```rust
173    /// use zeph_common::SkillTrustLevel;
174    ///
175    /// assert!(SkillTrustLevel::Blocked.is_hidden_from_catalog());
176    /// assert!(SkillTrustLevel::Quarantined.is_hidden_from_catalog());
177    /// assert!(!SkillTrustLevel::Trusted.is_hidden_from_catalog());
178    /// ```
179    #[must_use]
180    pub const fn is_hidden_from_catalog(self) -> bool {
181        matches!(self, Self::Blocked | Self::Quarantined)
182    }
183}
184
185impl FromStr for SkillTrustLevel {
186    type Err = String;
187
188    fn from_str(s: &str) -> Result<Self, Self::Err> {
189        match s {
190            "trusted" => Ok(Self::Trusted),
191            "verified" => Ok(Self::Verified),
192            "quarantined" => Ok(Self::Quarantined),
193            "blocked" => Ok(Self::Blocked),
194            other => Err(format!(
195                "unknown trust level '{other}'; expected: trusted, verified, quarantined, blocked"
196            )),
197        }
198    }
199}
200
201impl fmt::Display for SkillTrustLevel {
202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203        match self {
204            Self::Trusted => f.write_str("trusted"),
205            Self::Verified => f.write_str("verified"),
206            Self::Quarantined => f.write_str("quarantined"),
207            Self::Blocked => f.write_str("blocked"),
208        }
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn severity_ordering() {
218        assert!(SkillTrustLevel::Trusted.severity() < SkillTrustLevel::Verified.severity());
219        assert!(SkillTrustLevel::Verified.severity() < SkillTrustLevel::Quarantined.severity());
220        assert!(SkillTrustLevel::Quarantined.severity() < SkillTrustLevel::Blocked.severity());
221    }
222
223    #[test]
224    fn min_trust_picks_least_trusted() {
225        assert_eq!(
226            SkillTrustLevel::Trusted.min_trust(SkillTrustLevel::Quarantined),
227            SkillTrustLevel::Quarantined
228        );
229        assert_eq!(
230            SkillTrustLevel::Blocked.min_trust(SkillTrustLevel::Trusted),
231            SkillTrustLevel::Blocked
232        );
233    }
234
235    #[test]
236    fn is_active() {
237        assert!(SkillTrustLevel::Trusted.is_active());
238        assert!(SkillTrustLevel::Verified.is_active());
239        assert!(SkillTrustLevel::Quarantined.is_active());
240        assert!(!SkillTrustLevel::Blocked.is_active());
241    }
242
243    #[test]
244    fn is_hidden_from_catalog_excludes_blocked_and_quarantined_only() {
245        assert!(SkillTrustLevel::Blocked.is_hidden_from_catalog());
246        assert!(SkillTrustLevel::Quarantined.is_hidden_from_catalog());
247        assert!(!SkillTrustLevel::Trusted.is_hidden_from_catalog());
248        assert!(!SkillTrustLevel::Verified.is_hidden_from_catalog());
249    }
250
251    #[test]
252    fn default_is_quarantined() {
253        assert_eq!(SkillTrustLevel::default(), SkillTrustLevel::Quarantined);
254    }
255
256    #[test]
257    fn display() {
258        assert_eq!(SkillTrustLevel::Trusted.to_string(), "trusted");
259        assert_eq!(SkillTrustLevel::Blocked.to_string(), "blocked");
260        assert_eq!(SkillTrustLevel::Quarantined.to_string(), "quarantined");
261        assert_eq!(SkillTrustLevel::Verified.to_string(), "verified");
262    }
263
264    #[test]
265    fn serde_roundtrip() {
266        let level = SkillTrustLevel::Quarantined;
267        let json = serde_json::to_string(&level).unwrap();
268        assert_eq!(json, "\"quarantined\"");
269        let back: SkillTrustLevel = serde_json::from_str(&json).unwrap();
270        assert_eq!(back, level);
271    }
272
273    #[test]
274    fn min_trust_same_level_returns_self() {
275        assert_eq!(
276            SkillTrustLevel::Verified.min_trust(SkillTrustLevel::Verified),
277            SkillTrustLevel::Verified
278        );
279    }
280
281    #[test]
282    fn from_severity_round_trips_through_severity() {
283        for level in [
284            SkillTrustLevel::Trusted,
285            SkillTrustLevel::Verified,
286            SkillTrustLevel::Quarantined,
287            SkillTrustLevel::Blocked,
288        ] {
289            assert_eq!(SkillTrustLevel::from_severity(level.severity()), level);
290        }
291    }
292
293    #[test]
294    fn from_severity_out_of_range_fails_closed_to_blocked() {
295        assert_eq!(SkillTrustLevel::from_severity(4), SkillTrustLevel::Blocked);
296        assert_eq!(
297            SkillTrustLevel::from_severity(255),
298            SkillTrustLevel::Blocked
299        );
300    }
301}