agora_agentkit/
secrets.rs1use std::path::Path;
8use zeroize::Zeroizing;
9
10pub struct Secret(Zeroizing<String>);
15
16impl Secret {
17 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 pub fn new(value: String) -> Self {
25 Self(Zeroizing::new(value))
26 }
27
28 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(all(test, feature = "client", feature = "misanthropic"))]
58pub(crate) fn live_test_model() -> misanthropic::model::Model {
59 misanthropic::model::Model::Custom("claude-haiku-5-5".into())
60}
61
62#[cfg(all(test, feature = "client", feature = "misanthropic"))]
66pub(crate) const LIVE_TEST_EFFORT: misanthropic::prompt::Effort =
67 misanthropic::prompt::Effort::Low;
68
69#[cfg(all(test, feature = "client", feature = "misanthropic"))]
78pub(crate) fn live_test_api_key() -> Secret {
79 let dir = std::env::var_os("AGORA_SECRETS_DIR")
80 .map(std::path::PathBuf::from)
81 .unwrap_or_else(|| {
82 std::path::PathBuf::from(
83 std::env::var_os("HOME").expect("HOME is set"),
84 )
85 .join("Projects/agora/secrets")
86 });
87 let path = dir.join("anthropic_api_key");
88 Secret::from_file(&path).unwrap_or_else(|e| {
89 panic!(
90 "cannot read the API key from {}: {e} (set AGORA_SECRETS_DIR)",
91 path.display()
92 )
93 })
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use std::io::Write;
100
101 #[test]
102 fn from_file_and_expose() {
103 let mut file = tempfile::NamedTempFile::new().unwrap();
104 writeln!(file, " my-secret-value ").unwrap();
105
106 let secret = Secret::from_file(file.path()).unwrap();
107 assert_eq!(secret.expose(), "my-secret-value");
108 }
109
110 #[test]
111 fn debug_is_redacted() {
112 let secret = Secret::new("hunter2".to_string());
113 assert_eq!(format!("{:?}", secret), "[REDACTED]");
114 }
115
116 #[test]
117 fn display_is_redacted() {
118 let secret = Secret::new("hunter2".to_string());
119 assert_eq!(format!("{}", secret), "[REDACTED]");
120 }
121
122 #[test]
123 fn nonexistent_file_returns_error() {
124 let result = Secret::from_file(Path::new("/nonexistent/path"));
125 assert!(result.is_err());
126 }
127
128 #[test]
129 fn clone_preserves_value() {
130 let secret = Secret::new("cloneable".to_string());
131 let cloned = secret.clone();
132 assert_eq!(cloned.expose(), "cloneable");
133 }
134}