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}