symbi_runtime/integrations/agentpin/
key_store.rs1use agentpin::pinning::KeyPinStore;
7use std::fs::{self, File, OpenOptions};
8use std::io::{BufReader, Write};
9use std::path::{Path, PathBuf};
10
11use super::types::AgentPinError;
12
13#[derive(Debug, Clone)]
15pub struct AgentPinKeyStore {
16 store_path: PathBuf,
18}
19
20impl AgentPinKeyStore {
21 pub fn new(store_path: &Path) -> Result<Self, AgentPinError> {
24 if let Some(parent) = store_path.parent() {
26 fs::create_dir_all(parent).map_err(|e| AgentPinError::IoError {
27 reason: format!("Failed to create key store directory: {}", e),
28 })?;
29 }
30
31 let ks = Self {
32 store_path: store_path.to_path_buf(),
33 };
34
35 if !store_path.exists() {
37 let empty = KeyPinStore::new();
38 ks.save_pin_store(&empty)?;
39 }
40
41 Ok(ks)
42 }
43
44 pub fn load_pin_store(&self) -> Result<KeyPinStore, AgentPinError> {
46 if !self.store_path.exists() {
47 return Ok(KeyPinStore::new());
48 }
49
50 let file = File::open(&self.store_path).map_err(|e| AgentPinError::IoError {
51 reason: format!("Failed to open key store: {}", e),
52 })?;
53
54 let reader = BufReader::new(file);
55 let json: String =
56 serde_json::from_reader(reader).map_err(|e| AgentPinError::KeyStoreError {
57 reason: format!("Failed to deserialize key store: {}", e),
58 })?;
59
60 KeyPinStore::from_json(&json).map_err(|e| AgentPinError::KeyStoreError {
61 reason: format!("Failed to parse key store: {}", e),
62 })
63 }
64
65 pub fn save_pin_store(&self, store: &KeyPinStore) -> Result<(), AgentPinError> {
67 let json = store.to_json().map_err(|e| AgentPinError::KeyStoreError {
68 reason: format!("Failed to serialize key store: {}", e),
69 })?;
70
71 if let Some(parent) = self.store_path.parent() {
73 fs::create_dir_all(parent).map_err(|e| AgentPinError::IoError {
74 reason: format!("Failed to create key store directory: {}", e),
75 })?;
76 }
77
78 let mut file = OpenOptions::new()
79 .write(true)
80 .create(true)
81 .truncate(true)
82 .open(&self.store_path)
83 .map_err(|e| AgentPinError::IoError {
84 reason: format!("Failed to open key store for writing: {}", e),
85 })?;
86
87 #[cfg(unix)]
89 {
90 use std::os::unix::fs::PermissionsExt;
91 let perms = std::fs::Permissions::from_mode(0o600);
92 fs::set_permissions(&self.store_path, perms).map_err(|e| AgentPinError::IoError {
93 reason: format!("Failed to set key store permissions: {}", e),
94 })?;
95 }
96
97 serde_json::to_writer_pretty(&mut file, &json).map_err(|e| AgentPinError::IoError {
99 reason: format!("Failed to write key store: {}", e),
100 })?;
101
102 file.flush().map_err(|e| AgentPinError::IoError {
103 reason: format!("Failed to flush key store: {}", e),
104 })?;
105
106 Ok(())
107 }
108
109 pub fn store_path(&self) -> &Path {
111 &self.store_path
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use tempfile::TempDir;
119
120 #[test]
121 fn test_create_new_key_store() {
122 let temp_dir = TempDir::new().unwrap();
123 let store_path = temp_dir.path().join("agentpin_keys.json");
124
125 let ks = AgentPinKeyStore::new(&store_path).unwrap();
126 assert!(ks.store_path().exists());
127 }
128
129 #[test]
130 fn test_load_empty_store() {
131 let temp_dir = TempDir::new().unwrap();
132 let store_path = temp_dir.path().join("agentpin_keys.json");
133
134 let ks = AgentPinKeyStore::new(&store_path).unwrap();
135 let pin_store = ks.load_pin_store().unwrap();
136
137 assert!(pin_store.get_domain("anything.com").is_none());
139 }
140
141 #[test]
142 fn test_save_and_load_roundtrip() {
143 let temp_dir = TempDir::new().unwrap();
144 let store_path = temp_dir.path().join("agentpin_keys.json");
145
146 let ks = AgentPinKeyStore::new(&store_path).unwrap();
147 let store = KeyPinStore::new();
148
149 ks.save_pin_store(&store).unwrap();
150 let loaded = ks.load_pin_store().unwrap();
151
152 assert_eq!(store.to_json().unwrap(), loaded.to_json().unwrap());
154 }
155
156 #[test]
157 fn test_creates_parent_directories() {
158 let temp_dir = TempDir::new().unwrap();
159 let store_path = temp_dir.path().join("nested").join("dir").join("keys.json");
160
161 let ks = AgentPinKeyStore::new(&store_path).unwrap();
162 assert!(ks.store_path().exists());
163 }
164
165 #[cfg(unix)]
166 #[test]
167 fn test_file_permissions() {
168 use std::os::unix::fs::PermissionsExt;
169
170 let temp_dir = TempDir::new().unwrap();
171 let store_path = temp_dir.path().join("agentpin_keys.json");
172
173 let ks = AgentPinKeyStore::new(&store_path).unwrap();
174 let store = KeyPinStore::new();
175 ks.save_pin_store(&store).unwrap();
176
177 let metadata = fs::metadata(&store_path).unwrap();
178 let mode = metadata.permissions().mode() & 0o777;
179 assert_eq!(mode, 0o600);
180 }
181
182 #[test]
183 fn test_load_nonexistent_returns_empty() {
184 let temp_dir = TempDir::new().unwrap();
185 let store_path = temp_dir.path().join("does_not_exist.json");
186
187 let ks = AgentPinKeyStore {
189 store_path: store_path.clone(),
190 };
191
192 let pin_store = ks.load_pin_store().unwrap();
193 assert!(pin_store.get_domain("anything").is_none());
194 }
195}