Skip to main content

palisade_errors/
lib.rs

1//! # Palisade Errors
2//!
3//! Security-conscious error handling with operational security principles.
4//!
5//! ## Design Philosophy
6//!
7//! 1. **Internal errors contain full context** for forensic analysis
8//! 2. **External errors reveal nothing** that aids an adversary
9//! 3. **Error codes enable tracking** without information disclosure
10//! 4. **Sensitive data is explicitly marked** and zeroized
11//! 5. **Sanitization is mandatory** and provides useful signals without leaking details
12//! 6. **Timing attacks are mitigated** through optional normalization
13//!
14//! ## Security Principles
15//!
16//! - Never expose file paths, usernames, PIDs, or configuration values externally
17//! - Never reveal internal architecture, component names, or validation logic
18//! - Never show stack traces, memory addresses, or timing information
19//! - Sanitized output still provides operation category and retry hints
20//! - ALL context data is zeroized on drop - NO LEAKS, NO EXCEPTIONS
21//! - Zero-allocation hot paths where possible
22//! - Timing side-channels can be mitigated with normalization
23//!
24//! ## Threat Model
25//!
26//! We assume attackers:
27//! - Read our source code
28//! - Trigger errors intentionally to fingerprint the system
29//! - Collect error messages to map internal architecture
30//! - Use timing and error patterns for side-channel attacks
31//! - Perform post-compromise memory scraping and core dump analysis
32//!
33//! Therefore:
34//! - External errors provide only error codes and operation categories
35//! - Internal logs use structured data with explicit, short lifetimes
36//! - ALL error context is zeroized on drop
37//! - No string concatenation of sensitive data
38//! - No leaked allocations that bypass zeroization
39//! - Sensitive/Internal fields kept separate to prevent conflation
40//! - Timing normalization available for sensitive operations
41//!
42//! ## Quick Start
43//!
44//! ```rust
45//! use palisade_errors::{AgentError, definitions, Result};
46//!
47//! fn validate_config(threshold: f64) -> Result<()> {
48//!     if threshold < 0.0 || threshold > 100.0 {
49//!         return Err(AgentError::config(
50//!             definitions::CFG_INVALID_VALUE,
51//!             "validate_threshold",
52//!             "Threshold must be between 0 and 100"
53//!         ));
54//!     }
55//!     Ok(())
56//! }
57//!
58//! // External display (safe for untrusted viewers):
59//! // "Configuration operation failed [permanent] (E-CFG-103)"
60//!
61//! // Internal log (full context for forensics):
62//! // [E-CFG-103] operation='validate_threshold' details='Threshold must be between 0 and 100'
63//! ```
64//!
65//! ## Working with Sensitive Data
66//!
67//! ```rust
68//! use palisade_errors::{AgentError, definitions, Result};
69//! use std::fs::File;
70//!
71//! fn load_config(path: String) -> Result<File> {
72//!     File::open(&path).map_err(|e|
73//!         AgentError::from_io_path(
74//!             definitions::IO_READ_FAILED,
75//!             "load_config",
76//!             path,  // Kept separate as sensitive data
77//!             e
78//!         )
79//!     )
80//! }
81//! ```
82//!
83//! ## Timing Attack Mitigation
84//!
85//! ```rust
86//! use palisade_errors::{AgentError, definitions, Result};
87//! use std::time::Duration;
88//!
89//! fn authenticate(username: &str, password: &str) -> Result<()> {
90//!     // Perform authentication...
91//!     # Ok(())
92//! }
93//!
94//! // Normalize timing to prevent timing side-channels
95//! let result = authenticate("user", "pass");
96//! if let Err(e) = result {
97//!     // Delay error response to constant time
98//!     return Err(e.with_timing_normalization(Duration::from_millis(100)));
99//! }
100//! # Ok(())
101//! ```
102//!
103//! ## Features
104//!
105//! - `trusted_debug`: Enable detailed debug formatting for trusted environments (debug builds only)
106//! - `external_signaling`: Reserved for future external signaling capabilities
107
108#![warn(missing_docs)]
109#![warn(clippy::all)]
110
111use std::fmt;
112use std::io;
113use std::result;
114use std::time::{Duration, Instant};
115use zeroize::Zeroize;
116use std::error::Error;
117use std::borrow::Cow;
118
119pub mod codes;
120pub mod context;
121pub mod convenience;
122pub mod definitions;
123pub mod logging;
124pub mod models;
125pub mod obfuscation;
126pub mod ring_buffer;
127
128pub use codes::*;
129pub use context::*;
130pub use convenience::*;
131pub use definitions::*;
132pub use logging::*;
133pub use models::*;
134pub use obfuscation::*;
135pub use ring_buffer::*;
136
137/// Type alias for Results using our error type.
138pub type Result<T> = result::Result<T, AgentError>;
139
140// ============================================================================
141// Internal Error Context (Legacy, Still Used by AgentError)
142// ============================================================================
143
144/// Internal error context storage for `AgentError`.
145///
146/// This preserves the legacy context model while newer DualContextError APIs evolve.
147struct ErrorContext {
148    operation: Cow<'static, str>,
149    details: Cow<'static, str>,
150    source_internal: Option<Cow<'static, str>>,
151    source_sensitive: Option<Cow<'static, str>>,
152    metadata: Vec<(&'static str, ContextField)>,
153}
154
155impl ErrorContext {
156    #[inline]
157    fn new(operation: impl Into<Cow<'static, str>>, details: impl Into<Cow<'static, str>>) -> Self {
158        Self {
159            operation: operation.into(),
160            details: details.into(),
161            source_internal: None,
162            source_sensitive: None,
163            metadata: Vec::new(),
164        }
165    }
166
167    #[inline]
168    fn with_sensitive(
169        operation: impl Into<Cow<'static, str>>,
170        details: impl Into<Cow<'static, str>>,
171        sensitive_info: impl Into<Cow<'static, str>>,
172    ) -> Self {
173        Self {
174            operation: operation.into(),
175            details: details.into(),
176            source_internal: None,
177            source_sensitive: Some(sensitive_info.into()),
178            metadata: Vec::new(),
179        }
180    }
181
182    #[inline]
183    fn with_source_split(
184        operation: impl Into<Cow<'static, str>>,
185        details: impl Into<Cow<'static, str>>,
186        internal_source: impl Into<Cow<'static, str>>,
187        sensitive_source: impl Into<Cow<'static, str>>,
188    ) -> Self {
189        Self {
190            operation: operation.into(),
191            details: details.into(),
192            source_internal: Some(internal_source.into()),
193            source_sensitive: Some(sensitive_source.into()),
194            metadata: Vec::new(),
195        }
196    }
197
198    #[inline]
199    fn add_metadata(&mut self, key: &'static str, value: impl Into<Cow<'static, str>>) {
200        self.metadata.push((key, ContextField::from(value.into())));
201    }
202}
203
204impl Zeroize for ErrorContext {
205    fn zeroize(&mut self) {
206        if let Cow::Owned(ref mut s) = self.operation {
207            s.zeroize();
208        }
209        if let Cow::Owned(ref mut s) = self.details {
210            s.zeroize();
211        }
212        if let Some(Cow::Owned(ref mut s)) = self.source_internal {
213            s.zeroize();
214        }
215        if let Some(Cow::Owned(ref mut s)) = self.source_sensitive {
216            s.zeroize();
217        }
218        for (_, value) in &mut self.metadata {
219            value.zeroize();
220        }
221        self.metadata.clear();
222    }
223}
224
225impl Drop for ErrorContext {
226    fn drop(&mut self) {
227        self.zeroize();
228    }
229}
230
231/// Main error type with security-conscious design.
232///
233/// # Key Properties
234///
235/// - All context is zeroized on drop (including source errors)
236/// - External display reveals minimal information
237/// - Internal logging uses structured data with explicit lifetimes
238/// - No implicit conversions from stdlib errors
239/// - Hot paths are zero-allocation where possible
240/// - Built-in constant-time error generation to reduce timing side-channels
241/// - Always-on error code obfuscation to resist fingerprinting
242///
243/// # Design Rationale - Error Constructors
244///
245/// We provide convenience constructors like `config()`, `telemetry()`, etc.
246/// even though `ErrorCode` already contains the category. This is intentional:
247///
248/// 1. **Ergonomics**: `AgentError::config(code, ...)` is clearer than `AgentError::new(code, ...)`
249/// 2. **Future extensibility**: Different subsystems may need different context types
250/// 3. **Type safety**: Prevents mixing categories (compile-time check vs runtime enum match)
251/// 4. **Grep-ability**: Engineers can search for "::telemetry(" to find all telemetry errors
252///
253/// The redundancy is acceptable because it improves maintainability and reduces
254/// the chance of errors being created with mismatched code/category pairs.
255#[must_use = "errors should be handled or logged"]
256pub struct AgentError {
257    code: ErrorCode,
258    context: ErrorContext,
259    retryable: bool,
260    source: Option<Box<dyn Error + Send + Sync>>,
261    created_at: Instant,
262}
263
264impl AgentError {
265    #[inline]
266    fn enforce_constant_time(created_at: Instant) {
267        const ERROR_GEN_FLOOR_US: u64 = 1;
268        let target = Duration::from_micros(ERROR_GEN_FLOOR_US);
269        let end = created_at + target;
270        while Instant::now() < end {
271            std::hint::spin_loop();
272        }
273    }
274
275    /// Create a generic error with internal context only.
276    #[inline]
277    fn new(code: ErrorCode, operation: impl Into<Cow<'static, str>>, details: impl Into<Cow<'static, str>>) -> Self {
278        let created_at = Instant::now();
279        Self {
280            code: crate::obfuscation::obfuscate_code(&code),
281            context: ErrorContext::new(operation, details),
282            retryable: false,
283            source: None,
284            created_at,
285        }
286        .with_constant_time(created_at)
287    }
288
289    /// Create an error with sensitive information (paths, usernames, etc.)
290    #[inline]
291    fn new_sensitive(
292        code: ErrorCode,
293        operation: impl Into<Cow<'static, str>>,
294        details: impl Into<Cow<'static, str>>,
295        sensitive_info: impl Into<Cow<'static, str>>,
296    ) -> Self {
297        let created_at = Instant::now();
298        Self {
299            code: crate::obfuscation::obfuscate_code(&code),
300            context: ErrorContext::with_sensitive(operation, details, sensitive_info),
301            retryable: false,
302            source: None,
303            created_at,
304        }
305        .with_constant_time(created_at)
306    }
307
308    /// Create an error with split internal/sensitive sources.
309    ///
310    /// This keeps sensitive context (paths) separate from semi-sensitive
311    /// context (error kinds), preventing conflation.
312    #[inline]
313    fn new_with_split_source(
314        code: ErrorCode,
315        operation: impl Into<Cow<'static, str>>,
316        details: impl Into<Cow<'static, str>>,
317        internal_source: impl Into<Cow<'static, str>>,
318        sensitive_source: impl Into<Cow<'static, str>>,
319    ) -> Self {
320        let created_at = Instant::now();
321        Self {
322            code: crate::obfuscation::obfuscate_code(&code),
323            context: ErrorContext::with_source_split(
324                operation,
325                details,
326                internal_source,
327                sensitive_source,
328            ),
329            retryable: false,
330            source: None,
331            created_at,
332        }
333        .with_constant_time(created_at)
334    }
335
336    #[inline]
337    fn with_constant_time(mut self, created_at: Instant) -> Self {
338        Self::enforce_constant_time(created_at);
339        self.created_at = created_at;
340        self
341    }
342
343    /// Mark this error as retryable (transient failure)
344    #[inline]
345    pub fn with_retry(mut self) -> Self {
346        self.retryable = true;
347        self
348    }
349
350    /// Add tracking metadata (correlation IDs, session tokens, etc.)
351    ///
352    /// MUTATES IN PLACE - no cloning, no wasted allocations.
353    #[inline]
354    pub fn with_metadata(mut self, key: &'static str, value: impl Into<Cow<'static, str>>) -> Self {
355        self.context.add_metadata(key, value);
356        self
357    }
358
359    /// Normalize timing to prevent timing side-channel attacks.
360    ///
361    /// This sleeps until `target_duration` has elapsed since error creation,
362    /// ensuring that error responses take a consistent amount of time regardless
363    /// of which code path failed.
364    ///
365    /// # Use Cases
366    ///
367    /// - Authentication failures (prevent user enumeration)
368    /// - Sensitive file operations (prevent path existence probing)
369    /// - Cryptographic operations (prevent timing attacks on key material)
370    ///
371    /// # Example
372    ///
373    /// ```rust
374    /// use palisade_errors::{AgentError, definitions};
375    /// use std::time::Duration;
376    ///
377    /// fn authenticate(user: &str, pass: &str) -> palisade_errors::Result<()> {
378    ///     // Fast path: invalid username
379    ///     if !user_exists(user) {
380    ///         return Err(
381    ///             AgentError::config(definitions::CFG_VALIDATION_FAILED, "auth", "Invalid credentials")
382    ///                 .with_timing_normalization(Duration::from_millis(100))
383    ///         );
384    ///     }
385    ///     
386    ///     // Slow path: password hash check
387    ///     if !check_password(user, pass) {
388    ///         return Err(
389    ///             AgentError::config(definitions::CFG_VALIDATION_FAILED, "auth", "Invalid credentials")
390    ///                 .with_timing_normalization(Duration::from_millis(100))
391    ///         );
392    ///     }
393    ///     
394    ///     Ok(())
395    /// }
396    /// # fn user_exists(_: &str) -> bool { true }
397    /// # fn check_password(_: &str, _: &str) -> bool { true }
398    /// ```
399    ///
400    /// Both error paths now take at least 100ms, preventing attackers from
401    /// distinguishing between "user doesn't exist" and "wrong password".
402    ///
403    /// # Performance Note
404    ///
405    /// This adds a sleep to the error path, which is acceptable since errors
406    /// are not the hot path. The slight performance cost is worth the security
407    /// benefit for sensitive operations.
408    ///
409    /// # Limitations
410    /// 
411    /// - **Not async-safe**: Blocks the thread. Use in sync contexts only.
412    /// - **Coarse precision**: OS scheduling affects accuracy (1-15ms jitter).
413    /// - **Partial protection**: Only normalizes error return timing, not upstream operations.
414    /// - **Observable side-channels**: Network timing, cache behavior, DB queries remain.
415    /// 
416    /// This provides defense-in-depth against timing attacks but is not a complete solution.
417    #[inline]
418    pub fn with_timing_normalization(self, target_duration: Duration) -> Self {
419        let elapsed = self.created_at.elapsed();
420        if elapsed < target_duration {
421            std::thread::sleep(target_duration - elapsed);
422        }
423        self
424    }
425
426    /// Check if this error can be retried
427    #[inline]
428    pub const fn is_retryable(&self) -> bool {
429        self.retryable
430    }
431
432    /// Get error code
433    #[inline]
434    pub const fn code(&self) -> &ErrorCode {
435        &self.code
436    }
437
438    /// Get operation category
439    #[inline]
440    pub const fn category(&self) -> OperationCategory {
441        self.code.category()
442    }
443
444    /// Get the time elapsed since this error was created.
445    ///
446    /// Useful for metrics and debugging, but should NOT be exposed externally
447    /// as it could leak timing information.
448    #[inline]
449    pub fn age(&self) -> Duration {
450        self.created_at.elapsed()
451    }
452
453    /// Create structured internal log entry with explicit lifetime.
454    ///
455    /// # Critical Security Property
456    ///
457    /// The returned `InternalLog` borrows from `self` and CANNOT
458    /// outlive this error. This is intentional and enforces that sensitive
459    /// data is consumed immediately by the logger and cannot be retained.
460    ///
461    /// # Usage Pattern
462    ///
463    /// ```rust
464    /// # use palisade_errors::{AgentError, definitions};
465    /// let err = AgentError::config(
466    ///     definitions::CFG_PARSE_FAILED,
467    ///     "test",
468    ///     "test details"
469    /// );
470    /// let log = err.internal_log();
471    /// // logger.log_structured(log);  // log dies here
472    /// // err is dropped, all data zeroized
473    /// ```
474    ///
475    /// The short lifetime prevents accidental retention in log buffers,
476    /// async contexts, or background threads.
477    #[inline]
478    pub fn internal_log(&self) -> InternalLog<'_> {
479        InternalLog {
480            code: &self.code,
481            operation: self.context.operation.as_ref(),
482            details: self.context.details.as_ref(),
483            source_internal: self
484                .context
485                .source_internal
486                .as_ref()
487                .map(|s: &Cow<'static, str>| s.as_ref()),
488            source_sensitive: self
489                .context
490                .source_sensitive
491                .as_ref()
492                .map(|s: &Cow<'static, str>| s.as_ref()),
493            metadata: &self.context.metadata,
494            retryable: self.retryable,
495        }
496    }
497
498    /// Alternative logging pattern for frameworks that need callback-style.
499    ///
500    /// This enforces immediate consumption and prevents accidental retention:
501    ///
502    /// ```rust
503    /// # use palisade_errors::{AgentError, definitions};
504    /// # let err = AgentError::config(definitions::CFG_PARSE_FAILED, "test", "test");
505    /// err.with_internal_log(|log| {
506    ///     // logger.write(log.code(), log.operation());
507    ///     // log is destroyed when this closure returns
508    /// });
509    /// ```
510    #[inline]
511    pub fn with_internal_log<F, R>(&self, f: F) -> R
512    where
513        F: FnOnce(&InternalLog<'_>) -> R,
514    {
515        let log = self.internal_log();
516        f(&log)
517    }
518
519    // Convenience constructors for each subsystem.
520    // See "Design Rationale - Error Constructors" above for why these exist
521    // despite apparent redundancy with ErrorCode categories.
522
523    /// Create a configuration error
524    #[inline]
525    pub fn config(
526        code: ErrorCode, 
527        operation: impl Into<Cow<'static, str>>, 
528        details: impl Into<Cow<'static, str>>,
529    ) -> Self {
530        Self::new(code, operation, details)
531    }
532
533    /// Create a configuration error with sensitive context
534    #[inline]
535    pub fn config_sensitive(
536        code: ErrorCode, 
537        operation: impl Into<Cow<'static, str>>, 
538        details: impl Into<Cow<'static, str>>, 
539        sensitive: impl Into<Cow<'static, str>>,
540    ) -> Self {
541        Self::new_sensitive(code, operation, details, sensitive)
542    }
543
544    /// Create a deployment error
545    #[inline]
546    pub fn deployment(
547        code: ErrorCode, 
548        operation: impl Into<Cow<'static, str>>, 
549        details: impl Into<Cow<'static, str>>,
550    ) -> Self {
551        Self::new(code, operation, details)
552    }
553
554    /// Create a telemetry error
555    #[inline]
556    pub fn telemetry(
557        code: ErrorCode, 
558        operation: impl Into<Cow<'static, str>>, 
559        details: impl Into<Cow<'static, str>>,
560    ) -> Self {
561        Self::new(code, operation, details)
562    }
563
564    /// Create a correlation error
565    #[inline]
566    pub fn correlation(
567        code: ErrorCode, 
568        operation: impl Into<Cow<'static, str>>, 
569        details: impl Into<Cow<'static, str>>,
570    ) -> Self {
571        Self::new(code, operation, details)
572    }
573
574    /// Create a response error
575    #[inline]
576    pub fn response(
577        code: ErrorCode, 
578        operation: impl Into<Cow<'static, str>>, 
579        details: impl Into<Cow<'static, str>>,
580    ) -> Self {
581        Self::new(code, operation, details)
582    }
583
584    /// Create a logging error
585    #[inline]
586    pub fn logging(
587        code: ErrorCode, 
588        operation: impl Into<Cow<'static, str>>, 
589        details: impl Into<Cow<'static, str>>,
590    ) -> Self {
591        Self::new(code, operation, details)
592    }
593
594    /// Create a platform error
595    #[inline]
596    pub fn platform(
597        code: ErrorCode, 
598        operation: impl Into<Cow<'static, str>>, 
599        details: impl Into<Cow<'static, str>>,
600    ) -> Self {
601        Self::new(code, operation, details)
602    }
603
604    /// Create an I/O operation error
605    #[inline]
606    pub fn io_operation(
607        code: ErrorCode, 
608        operation: impl Into<Cow<'static, str>>, 
609        details: impl Into<Cow<'static, str>>,
610    ) -> Self {
611        Self::new(code, operation, details)
612    }
613
614    /// Wrap io::Error with explicit context, keeping path and error separate.
615    ///
616    /// This prevents conflation of:
617    /// - Error kind (semi-sensitive, reveals error type)
618    /// - Path (sensitive, reveals filesystem structure)
619    ///
620    /// By keeping them separate, logging systems can choose to handle them
621    /// differently (e.g., hash paths but log error kinds).
622    #[inline]
623    pub fn from_io_path(
624        code: ErrorCode,
625        operation: impl Into<Cow<'static, str>>,
626        path: impl Into<Cow<'static, str>>,
627        error: io::Error,
628    ) -> Self {
629        Self::new_with_split_source(
630            code,
631            operation,
632            "I/O operation failed",
633            format!("{:?}", error.kind()),  // Internal: error kind
634            path.into(),                // Sensitive: filesystem path
635        )
636    }
637
638    /// Async-safe timing normalization for non-blocking contexts.
639    ///
640    /// Unlike `with_timing_normalization`, this uses async sleep primitives
641    /// and won't block the executor thread. Essential for Tokio/async-std runtimes.
642    ///
643    /// # Example
644    ///
645    /// ```rust,ignore
646    /// async fn authenticate(user: &str, pass: &str) -> Result<Session> {
647    ///     let result = check_credentials(user, pass).await;
648    ///     
649    ///     if let Err(e) = result {
650    ///         // Normalize timing without blocking executor
651    ///         return Err(
652    ///             e.with_timing_normalization_async(Duration::from_millis(100)).await
653    ///         );
654    ///     }
655    ///     
656    ///     result
657    /// }
658    /// ```
659    ///
660    /// # Runtime Support
661    ///
662    /// - Requires either `tokio` or `async-std` feature
663    /// - Tokio takes precedence if both are enabled
664    /// - Will not compile without at least one async runtime feature
665    #[cfg(any(feature = "tokio", feature = "async_std"))]
666    #[inline]
667    pub async fn with_timing_normalization_async(self, target_duration: Duration) -> Self {
668        // FIXED: Calculate target absolute time to avoid race conditions
669        let target_time = self.created_at + target_duration;
670        let now = Instant::now();
671        
672        if now < target_time {
673            let sleep_duration = target_time - now;
674            
675            #[cfg(feature = "tokio")]
676            tokio::time::sleep(sleep_duration).await;
677            
678            #[cfg(all(feature = "async_std", not(feature = "tokio")))]
679            async_std::task::sleep(sleep_duration).await;
680        }
681        self
682    }
683
684    // Obfuscation is always applied at construction time.
685}
686
687// Manual Drop implementation to ensure proper zeroization ordering
688impl Drop for AgentError {
689    /// Panic-safe drop with explicit zeroization order.
690    ///
691    /// Marked #[inline(never)] to prevent optimization that could skip zeroization.
692    #[inline(never)]
693    fn drop(&mut self) {
694        // Use catch_unwind to ensure we attempt cleanup even if something panics
695        let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
696            // Drop the source error first (may contain sensitive data)
697            // By setting to None, we ensure the boxed error is dropped
698            self.source = None;
699            
700            // Context zeroizes itself via ZeroizeOnDrop
701            // but we're explicit here for documentation
702            self.context.zeroize();
703        }));
704    }
705}
706
707impl fmt::Debug for AgentError {
708    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
709        f.debug_struct("AgentError")
710            .field("code", &self.code)
711            .field("category", &self.code.category())
712            .field("retryable", &self.retryable)
713            .field("age", &self.created_at.elapsed())
714            .field("context", &"<REDACTED>")
715            .field("source", &self.source.as_ref().map(|_| "<PRESENT>"))
716            .finish()
717    }
718}
719
720impl fmt::Display for AgentError {
721    /// External display - sanitized for untrusted viewers.
722    /// Zero-allocation formatting.
723    ///
724    /// Format: "{Category} operation failed [{permanence}] ({ERROR-CODE})"
725    ///
726    /// Example: "Configuration operation failed [permanent] (E-CFG-100)"
727    ///
728    /// This provides:
729    /// - Operation domain (for troubleshooting)
730    /// - Retry semantics (for automation)
731    /// - Error code (for tracking)
732    ///
733    /// Without revealing:
734    /// - Internal paths or structure
735    /// - Validation logic
736    /// - User identifiers
737    /// - Configuration values
738    /// - Timing information
739    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
740        let permanence = if self.retryable { "temporary" } else { "permanent" };
741        write!(
742            f,
743            "{} operation failed [{}] ({})",
744            self.code.category().display_name(),
745            permanence,
746            self.code  // ErrorCode::Display also writes directly
747        )
748    }
749}
750
751impl std::error::Error for AgentError {
752    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
753        self.source.as_ref().map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
754    }
755}
756
757#[cfg(test)]
758mod unit_tests {
759    use super::*;
760    use std::thread;
761    use std::time::Duration;
762
763    #[test]
764    fn timing_normalization_adds_delay() {
765        let start = Instant::now();
766        let err = AgentError::config(
767            definitions::CFG_PARSE_FAILED,
768            "test",
769            "test"
770        );
771        
772        // This should add delay to reach 50ms
773        let _normalized = err.with_timing_normalization(Duration::from_millis(50));
774        
775        let elapsed = start.elapsed();
776        assert!(elapsed >= Duration::from_millis(50));
777        assert!(elapsed < Duration::from_millis(100)); // Some tolerance
778    }
779
780    #[test]
781    fn timing_normalization_no_delay_if_already_slow() {
782        let err = AgentError::config(
783            definitions::CFG_PARSE_FAILED,
784            "test",
785            "test"
786        );
787        
788        // Wait longer than target
789        thread::sleep(Duration::from_millis(60));
790        
791        let start = Instant::now();
792        let _normalized = err.with_timing_normalization(Duration::from_millis(50));
793        let elapsed = start.elapsed();
794        
795        // Should not add extra delay
796        assert!(elapsed < Duration::from_millis(10));
797    }
798
799    #[test]
800    fn external_display_reveals_no_details() {
801        crate::obfuscation::clear_session_salt();
802        let err = AgentError::from_io_path(
803            definitions::IO_READ_FAILED,
804            "load_config",
805            "/etc/shadow",
806            io::Error::from(io::ErrorKind::PermissionDenied)
807        );
808        
809        let displayed = format!("{}", err);
810        
811        // Should not contain sensitive information
812        assert!(!displayed.contains("/etc"));
813        assert!(!displayed.contains("shadow"));
814        assert!(!displayed.contains("load_config"));
815        assert!(!displayed.contains("Permission"));
816        
817        // Should contain safe information
818        assert!(displayed.contains("I/O"));
819        assert!(displayed.contains("E-IO-800"));
820    }
821
822    #[test]
823    fn internal_log_contains_details() {
824        let err = AgentError::config(
825            definitions::CFG_PARSE_FAILED,
826            "test_operation",
827            "test details"
828        );
829        
830        let log = err.internal_log();
831        assert_eq!(log.operation(), "test_operation");
832        assert_eq!(log.details(), "test details");
833    }
834
835    #[test]
836    fn error_age_increases() {
837        let err = AgentError::config(
838            definitions::CFG_PARSE_FAILED,
839            "test",
840            "test"
841        );
842        
843        let age1 = err.age();
844        thread::sleep(Duration::from_millis(10));
845        let age2 = err.age();
846        
847        assert!(age2 > age1);
848    }
849}