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 /// Returns the string representation used for database storage.
107 #[must_use]
108 pub const fn as_str(self) -> &'static str {
109 match self {
110 Self::Trusted => "trusted",
111 Self::Verified => "verified",
112 Self::Quarantined => "quarantined",
113 Self::Blocked => "blocked",
114 }
115 }
116
117 /// Returns `true` if the level is not `Blocked`.
118 ///
119 /// # Examples
120 ///
121 /// ```rust
122 /// use zeph_common::SkillTrustLevel;
123 ///
124 /// assert!(SkillTrustLevel::Quarantined.is_active());
125 /// assert!(!SkillTrustLevel::Blocked.is_active());
126 /// ```
127 #[must_use]
128 pub const fn is_active(self) -> bool {
129 !matches!(self, Self::Blocked)
130 }
131}
132
133impl FromStr for SkillTrustLevel {
134 type Err = String;
135
136 fn from_str(s: &str) -> Result<Self, Self::Err> {
137 match s {
138 "trusted" => Ok(Self::Trusted),
139 "verified" => Ok(Self::Verified),
140 "quarantined" => Ok(Self::Quarantined),
141 "blocked" => Ok(Self::Blocked),
142 other => Err(format!(
143 "unknown trust level '{other}'; expected: trusted, verified, quarantined, blocked"
144 )),
145 }
146 }
147}
148
149impl fmt::Display for SkillTrustLevel {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 match self {
152 Self::Trusted => f.write_str("trusted"),
153 Self::Verified => f.write_str("verified"),
154 Self::Quarantined => f.write_str("quarantined"),
155 Self::Blocked => f.write_str("blocked"),
156 }
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn severity_ordering() {
166 assert!(SkillTrustLevel::Trusted.severity() < SkillTrustLevel::Verified.severity());
167 assert!(SkillTrustLevel::Verified.severity() < SkillTrustLevel::Quarantined.severity());
168 assert!(SkillTrustLevel::Quarantined.severity() < SkillTrustLevel::Blocked.severity());
169 }
170
171 #[test]
172 fn min_trust_picks_least_trusted() {
173 assert_eq!(
174 SkillTrustLevel::Trusted.min_trust(SkillTrustLevel::Quarantined),
175 SkillTrustLevel::Quarantined
176 );
177 assert_eq!(
178 SkillTrustLevel::Blocked.min_trust(SkillTrustLevel::Trusted),
179 SkillTrustLevel::Blocked
180 );
181 }
182
183 #[test]
184 fn is_active() {
185 assert!(SkillTrustLevel::Trusted.is_active());
186 assert!(SkillTrustLevel::Verified.is_active());
187 assert!(SkillTrustLevel::Quarantined.is_active());
188 assert!(!SkillTrustLevel::Blocked.is_active());
189 }
190
191 #[test]
192 fn default_is_quarantined() {
193 assert_eq!(SkillTrustLevel::default(), SkillTrustLevel::Quarantined);
194 }
195
196 #[test]
197 fn display() {
198 assert_eq!(SkillTrustLevel::Trusted.to_string(), "trusted");
199 assert_eq!(SkillTrustLevel::Blocked.to_string(), "blocked");
200 assert_eq!(SkillTrustLevel::Quarantined.to_string(), "quarantined");
201 assert_eq!(SkillTrustLevel::Verified.to_string(), "verified");
202 }
203
204 #[test]
205 fn serde_roundtrip() {
206 let level = SkillTrustLevel::Quarantined;
207 let json = serde_json::to_string(&level).unwrap();
208 assert_eq!(json, "\"quarantined\"");
209 let back: SkillTrustLevel = serde_json::from_str(&json).unwrap();
210 assert_eq!(back, level);
211 }
212
213 #[test]
214 fn min_trust_same_level_returns_self() {
215 assert_eq!(
216 SkillTrustLevel::Verified.min_trust(SkillTrustLevel::Verified),
217 SkillTrustLevel::Verified
218 );
219 }
220}