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