m2m/error.rs
1//! M2M Protocol error types with epistemic classification.
2//!
3//! # Epistemic Error Taxonomy
4//!
5//! This module organizes errors by their epistemic nature - what they tell us
6//! about the state of knowledge at the time of failure. This classification
7//! helps developers understand:
8//!
9//! 1. **Whether the error was predictable** (and thus should be handled gracefully)
10//! 2. **What remediation is appropriate** (retry, fail-fast, degrade gracefully)
11//! 3. **Where defensive code is needed** (validation boundaries, I/O edges)
12//!
13//! ## Categories
14//!
15//! ### B_i Falsified (Belief Proven Wrong)
16//!
17//! The caller held a belief about validity that was proven false at runtime.
18//! These are "expected" errors in the sense that the system was designed to
19//! detect and report them. Examples:
20//!
21//! - Invalid input format (caller believed input was valid)
22//! - Session not established (caller believed session was ready)
23//! - Capability mismatch (caller believed peers were compatible)
24//!
25//! **Handling**: Validate early, return descriptive errors, don't retry.
26//!
27//! ### I^B (Bounded Ignorance Materialized)
28//!
29//! The system could not know the outcome at compile time - it depends on
30//! external state (network, filesystem, RNG, upstream services). These errors
31//! represent the inherent uncertainty of distributed systems.
32//!
33//! - Network errors (connectivity is I^B until we try)
34//! - Upstream service errors (availability is I^B)
35//! - RNG failures (entropy availability is I^B)
36//!
37//! **Handling**: Timeouts, retries with backoff, circuit breakers, fallbacks.
38//!
39//! ### K_i Violated (Invariant Broken)
40//!
41//! A known invariant that "should never happen" was violated. These indicate
42//! bugs in the code or corruption. They are NOT expected in normal operation.
43//!
44//! - Internal state corruption
45//! - Logic errors (unreachable code reached)
46//!
47//! **Handling**: Log extensively, fail fast, alert operators.
48//!
49//! ## Usage Example
50//!
51//! ```rust,ignore
52//! use m2m::error::{M2MError, Result};
53//!
54//! fn process_message(session: &Session, data: &[u8]) -> Result<Response> {
55//! // B_i: Caller believes session is established
56//! if !session.is_established() {
57//! return Err(M2MError::SessionNotEstablished);
58//! }
59//!
60//! // B_i: Caller believes data is valid JSON
61//! let parsed = serde_json::from_slice(data)?;
62//!
63//! // I^B: Network availability unknown until we try
64//! let response = send_upstream(parsed).await?;
65//!
66//! Ok(response)
67//! }
68//! ```
69
70use thiserror::Error;
71
72use crate::codec::m2m::crypto::CryptoError;
73
74/// M2M Protocol errors, organized by epistemic category.
75///
76/// See module documentation for the full epistemic taxonomy.
77#[derive(Error, Debug)]
78pub enum M2MError {
79 // ═══════════════════════════════════════════════════════════════════════
80 // B_i FALSIFIED — Caller's belief about validity proven wrong
81 // ═══════════════════════════════════════════════════════════════════════
82 //
83 // These errors indicate the caller made an assumption that was incorrect.
84 // They are "expected" errors - the system is designed to detect them.
85 // Handling: Validate inputs, return clear errors, don't retry.
86 // ═══════════════════════════════════════════════════════════════════════
87 /// Compression failed due to invalid input or unsupported content.
88 ///
89 /// **Epistemic**: B_i falsified — caller believed content was compressible.
90 #[error("Compression error: {0}")]
91 Compression(String),
92
93 /// Decompression failed due to corrupted or invalid wire format.
94 ///
95 /// **Epistemic**: B_i falsified — caller believed data was valid M2M format.
96 #[error("Decompression error: {0}")]
97 Decompression(String),
98
99 /// Codec identifier not recognized or not supported.
100 ///
101 /// **Epistemic**: B_i falsified — caller believed codec was available.
102 #[error("Invalid codec: {0}")]
103 InvalidCodec(String),
104
105 /// Protocol state machine violation.
106 ///
107 /// **Epistemic**: B_i falsified — caller believed operation was valid
108 /// in current state.
109 #[error("Protocol error: {0}")]
110 Protocol(String),
111
112 /// Session handshake failed to agree on capabilities.
113 ///
114 /// **Epistemic**: B_i falsified — caller believed negotiation would succeed.
115 #[error("Negotiation failed: {0}")]
116 NegotiationFailed(String),
117
118 /// Operation requires an established session, but session is not ready.
119 ///
120 /// **Epistemic**: B_i falsified — caller believed session was established.
121 #[error("Session not established")]
122 SessionNotEstablished,
123
124 /// Session has exceeded its timeout duration.
125 ///
126 /// **Epistemic**: B_i falsified — caller believed session was still valid.
127 #[error("Session expired")]
128 SessionExpired,
129
130 /// Message does not conform to expected format.
131 ///
132 /// **Epistemic**: B_i falsified — caller believed message was well-formed.
133 #[error("Invalid message format: {0}")]
134 InvalidMessage(String),
135
136 /// Peers have incompatible capabilities for the requested operation.
137 ///
138 /// **Epistemic**: B_i falsified — caller believed capabilities were compatible.
139 #[error("Capability mismatch: {0}")]
140 CapabilityMismatch(String),
141
142 /// ML model was expected to be loaded but isn't.
143 ///
144 /// **Epistemic**: B_i falsified — caller believed model was available.
145 #[error("Model not loaded: {0}")]
146 ModelNotLoaded(String),
147
148 /// Requested model not found in the registry.
149 ///
150 /// **Epistemic**: B_i falsified — caller believed model existed.
151 #[error("Model not found: {0}")]
152 ModelNotFound(String),
153
154 /// Tokenizer operation failed (invalid encoding, unknown token).
155 ///
156 /// **Epistemic**: B_i falsified — caller believed input was tokenizable.
157 #[error("Tokenizer error: {0}")]
158 Tokenizer(String),
159
160 /// Configuration is invalid or missing required values.
161 ///
162 /// **Epistemic**: B_i falsified — caller believed config was valid.
163 #[error("Config error: {0}")]
164 Config(String),
165
166 /// JSON serialization/deserialization failed.
167 ///
168 /// **Epistemic**: B_i falsified — caller believed data was valid JSON.
169 #[error("JSON error: {0}")]
170 Json(#[from] serde_json::Error),
171
172 // ═══════════════════════════════════════════════════════════════════════
173 // I^B — Bounded Ignorance (External State Unknown Until Runtime)
174 // ═══════════════════════════════════════════════════════════════════════
175 //
176 // These errors stem from inherent uncertainty about external systems.
177 // We cannot know network state, service availability, or RNG entropy
178 // at compile time - it's bounded ignorance that materializes at runtime.
179 // Handling: Timeouts, retries, circuit breakers, graceful degradation.
180 // ═══════════════════════════════════════════════════════════════════════
181 /// Network operation failed (connection, timeout, DNS).
182 ///
183 /// **Epistemic**: I^B materialized — network availability was unknown
184 /// until we attempted the operation.
185 ///
186 /// **Handling**: Retry with exponential backoff, circuit breaker.
187 #[error("Network error: {0}")]
188 Network(String),
189
190 /// Upstream service returned an error or is unavailable.
191 ///
192 /// **Epistemic**: I^B materialized — upstream health was unknown
193 /// until we made the request.
194 ///
195 /// **Handling**: Retry, failover to alternate upstream, degrade gracefully.
196 #[error("Upstream error: {0}")]
197 Upstream(String),
198
199 /// Server-side processing error.
200 ///
201 /// **Epistemic**: I^B materialized — server state was unknown to client.
202 #[error("Server error: {0}")]
203 Server(String),
204
205 /// ML inference failed during execution.
206 ///
207 /// **Epistemic**: I^B materialized — model execution success depends on
208 /// input characteristics and runtime state.
209 #[error("Inference error: {0}")]
210 Inference(String),
211
212 /// Failed to load ML model from filesystem or network.
213 ///
214 /// **Epistemic**: I^B materialized — model file availability unknown
215 /// until load attempted.
216 #[error("Model load error: {0}")]
217 ModelLoad(String),
218
219 /// I/O operation failed.
220 ///
221 /// **Epistemic**: I^B materialized — filesystem/resource state unknown
222 /// until operation attempted.
223 #[error("IO error: {0}")]
224 Io(#[from] std::io::Error),
225
226 /// Cryptographic operation failed (key derivation, encryption, auth).
227 ///
228 /// **Epistemic**: Mixed — may be B_i (invalid key) or I^B (RNG failure).
229 /// The inner `CryptoError` provides specific classification.
230 ///
231 /// This variant preserves the full error chain via `#[source]`,
232 /// enabling tools like `anyhow` to display complete context.
233 #[error("Crypto error: {0}")]
234 Crypto(#[source] CryptoError),
235
236 // ═══════════════════════════════════════════════════════════════════════
237 // SECURITY — Policy Violations (Special Category)
238 // ═══════════════════════════════════════════════════════════════════════
239 //
240 // Security errors are epistemically B_i (content was believed safe),
241 // but they warrant special handling due to their nature. They should
242 // NOT be retried and may require alerting.
243 // ═══════════════════════════════════════════════════════════════════════
244 /// Security scanner detected a threat in content.
245 ///
246 /// **Epistemic**: B_i falsified — content was believed to be safe.
247 ///
248 /// **Handling**: Do NOT retry, log for security audit, consider blocking source.
249 #[error("Security threat detected: {threat_type}")]
250 SecurityThreat {
251 /// Type of threat detected (e.g., "prompt_injection", "jailbreak").
252 threat_type: String,
253 /// Detection confidence score (0.0-1.0).
254 confidence: f32,
255 },
256
257 /// Content blocked by security policy.
258 ///
259 /// **Epistemic**: B_i falsified — content was believed to comply with policy.
260 ///
261 /// **Handling**: Do NOT retry, inform user of policy violation.
262 #[error("Content blocked: {0}")]
263 ContentBlocked(String),
264}
265
266/// Result type alias for M2M operations.
267pub type Result<T> = std::result::Result<T, M2MError>;
268
269// ═══════════════════════════════════════════════════════════════════════════
270// From Implementations
271// ═══════════════════════════════════════════════════════════════════════════
272
273impl From<CryptoError> for M2MError {
274 fn from(err: CryptoError) -> Self {
275 M2MError::Crypto(err)
276 }
277}
278
279impl From<reqwest::Error> for M2MError {
280 fn from(err: reqwest::Error) -> Self {
281 M2MError::Network(err.to_string())
282 }
283}
284
285impl From<toml::de::Error> for M2MError {
286 fn from(err: toml::de::Error) -> Self {
287 M2MError::Config(err.to_string())
288 }
289}
290
291impl From<base64::DecodeError> for M2MError {
292 fn from(err: base64::DecodeError) -> Self {
293 M2MError::Decompression(format!("Base64 decode error: {err}"))
294 }
295}
296
297// ═══════════════════════════════════════════════════════════════════════════
298// Helper Methods
299// ═══════════════════════════════════════════════════════════════════════════
300
301impl M2MError {
302 /// Returns `true` if this error is retryable.
303 ///
304 /// I^B errors (network, upstream, inference) are generally retryable.
305 /// B_i errors (validation failures) are NOT retryable without changes.
306 ///
307 /// # Example
308 ///
309 /// ```rust,ignore
310 /// match operation() {
311 /// Err(e) if e.is_retryable() => retry_with_backoff(operation),
312 /// Err(e) => return Err(e),
313 /// Ok(v) => v,
314 /// }
315 /// ```
316 pub fn is_retryable(&self) -> bool {
317 matches!(
318 self,
319 M2MError::Network(_)
320 | M2MError::Upstream(_)
321 | M2MError::Server(_)
322 | M2MError::Inference(_)
323 | M2MError::Io(_)
324 )
325 }
326
327 /// Returns `true` if this error is security-related.
328 ///
329 /// Security errors should NOT be retried and may warrant special logging.
330 pub fn is_security_error(&self) -> bool {
331 matches!(
332 self,
333 M2MError::SecurityThreat { .. } | M2MError::ContentBlocked(_)
334 )
335 }
336
337 /// Returns `true` if this error indicates bounded ignorance (I^B).
338 ///
339 /// These errors stem from external system state that was unknown
340 /// at compile time.
341 pub fn is_bounded_ignorance(&self) -> bool {
342 matches!(
343 self,
344 M2MError::Network(_)
345 | M2MError::Upstream(_)
346 | M2MError::Server(_)
347 | M2MError::Inference(_)
348 | M2MError::ModelLoad(_)
349 | M2MError::Io(_)
350 | M2MError::Crypto(_)
351 )
352 }
353
354 /// Returns `true` if this error indicates a falsified belief (B_i).
355 ///
356 /// These errors indicate the caller made an incorrect assumption
357 /// about input validity or system state.
358 pub fn is_belief_falsified(&self) -> bool {
359 !self.is_bounded_ignorance()
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 #[test]
368 fn test_retryable_classification() {
369 // I^B errors are retryable
370 assert!(M2MError::Network("timeout".to_string()).is_retryable());
371 assert!(M2MError::Upstream("503".to_string()).is_retryable());
372 assert!(M2MError::Server("overloaded".to_string()).is_retryable());
373
374 // B_i errors are NOT retryable
375 assert!(!M2MError::SessionNotEstablished.is_retryable());
376 assert!(!M2MError::InvalidMessage("bad format".to_string()).is_retryable());
377 assert!(!M2MError::Decompression("corrupt".to_string()).is_retryable());
378 }
379
380 #[test]
381 fn test_security_classification() {
382 assert!(M2MError::SecurityThreat {
383 threat_type: "injection".to_string(),
384 confidence: 0.95
385 }
386 .is_security_error());
387
388 assert!(M2MError::ContentBlocked("policy".to_string()).is_security_error());
389
390 // Non-security errors
391 assert!(!M2MError::Network("timeout".to_string()).is_security_error());
392 }
393
394 #[test]
395 fn test_bounded_ignorance_classification() {
396 // I^B errors
397 assert!(M2MError::Network("timeout".to_string()).is_bounded_ignorance());
398 assert!(M2MError::Upstream("503".to_string()).is_bounded_ignorance());
399 assert!(M2MError::ModelLoad("not found".to_string()).is_bounded_ignorance());
400
401 // B_i errors (NOT bounded ignorance)
402 assert!(!M2MError::SessionNotEstablished.is_bounded_ignorance());
403 assert!(!M2MError::InvalidCodec("unknown".to_string()).is_bounded_ignorance());
404 }
405
406 #[test]
407 fn test_belief_falsified_is_inverse() {
408 let network_err = M2MError::Network("timeout".to_string());
409 let session_err = M2MError::SessionNotEstablished;
410
411 // These should be inverses
412 assert_eq!(
413 network_err.is_bounded_ignorance(),
414 !network_err.is_belief_falsified()
415 );
416 assert_eq!(
417 session_err.is_bounded_ignorance(),
418 !session_err.is_belief_falsified()
419 );
420 }
421}