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}