Skip to main content

agora_agentkit/
secrets.rs

1//! Secret value management with zeroization and redacted output.
2//!
3//! Use [`Secret::from_file`] to load secrets from file-mounted paths
4//! (e.g., Docker secrets). The inner value is zeroized on drop and never
5//! appears in `Debug` or `Display` output.
6
7use std::path::Path;
8use zeroize::Zeroizing;
9
10/// A secret value that is zeroized on drop and redacted in debug/display output.
11///
12/// Use `from_file` to load secrets from file-mounted paths (e.g., Docker secrets).
13/// Use `expose` to access the inner value when needed.
14pub struct Secret(Zeroizing<String>);
15
16impl Secret {
17    /// Load a secret from a file, trimming trailing whitespace.
18    pub fn from_file(path: &Path) -> Result<Self, std::io::Error> {
19        let contents = std::fs::read_to_string(path)?;
20        Ok(Self(Zeroizing::new(contents.trim().to_owned())))
21    }
22
23    /// Create a secret from a string value.
24    pub fn new(value: String) -> Self {
25        Self(Zeroizing::new(value))
26    }
27
28    /// Access the secret value. Use sparingly.
29    pub fn expose(&self) -> &str {
30        &self.0
31    }
32}
33
34impl Clone for Secret {
35    fn clone(&self) -> Self {
36        Self(self.0.clone())
37    }
38}
39
40impl std::fmt::Debug for Secret {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        f.write_str("[REDACTED]")
43    }
44}
45
46impl std::fmt::Display for Secret {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        f.write_str("[REDACTED]")
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use std::io::Write;
56
57    #[test]
58    fn from_file_and_expose() {
59        let mut file = tempfile::NamedTempFile::new().unwrap();
60        writeln!(file, "  my-secret-value  ").unwrap();
61
62        let secret = Secret::from_file(file.path()).unwrap();
63        assert_eq!(secret.expose(), "my-secret-value");
64    }
65
66    #[test]
67    fn debug_is_redacted() {
68        let secret = Secret::new("hunter2".to_string());
69        assert_eq!(format!("{:?}", secret), "[REDACTED]");
70    }
71
72    #[test]
73    fn display_is_redacted() {
74        let secret = Secret::new("hunter2".to_string());
75        assert_eq!(format!("{}", secret), "[REDACTED]");
76    }
77
78    #[test]
79    fn nonexistent_file_returns_error() {
80        let result = Secret::from_file(Path::new("/nonexistent/path"));
81        assert!(result.is_err());
82    }
83
84    #[test]
85    fn clone_preserves_value() {
86        let secret = Secret::new("cloneable".to_string());
87        let cloned = secret.clone();
88        assert_eq!(cloned.expose(), "cloneable");
89    }
90}