1use crate::agent::AgentCore;
7use crate::error::AgentError;
8use std::path::PathBuf;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::{Arc, Mutex, MutexGuard};
11use std::time::{Duration, Instant};
12use zeroize::Zeroizing;
13
14pub const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(30 * 60);
16
17pub struct AgentHandle {
28 core: Arc<Mutex<AgentCore>>,
30 socket_path: PathBuf,
32 pid_file: Option<PathBuf>,
34 running: Arc<AtomicBool>,
36 last_activity: Mutex<Instant>,
38 idle_timeout: Duration,
40 locked: AtomicBool,
42}
43
44impl std::fmt::Debug for AgentHandle {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 f.debug_struct("AgentHandle")
47 .field("socket_path", &self.socket_path)
48 .field("pid_file", &self.pid_file)
49 .field("running", &self.is_running())
50 .field("locked", &self.is_agent_locked())
51 .field("idle_timeout", &self.idle_timeout)
52 .finish_non_exhaustive()
53 }
54}
55
56impl AgentHandle {
57 pub fn new(socket_path: PathBuf) -> Self {
59 Self::with_timeout(socket_path, DEFAULT_IDLE_TIMEOUT)
60 }
61
62 pub fn with_timeout(socket_path: PathBuf, idle_timeout: Duration) -> Self {
64 Self {
65 core: Arc::new(Mutex::new(AgentCore::default())),
66 socket_path,
67 pid_file: None,
68 running: Arc::new(AtomicBool::new(false)),
69 last_activity: Mutex::new(Instant::now()),
70 idle_timeout,
71 locked: AtomicBool::new(false),
72 }
73 }
74
75 pub fn with_pid_file(socket_path: PathBuf, pid_file: PathBuf) -> Self {
77 Self {
78 core: Arc::new(Mutex::new(AgentCore::default())),
79 socket_path,
80 pid_file: Some(pid_file),
81 running: Arc::new(AtomicBool::new(false)),
82 last_activity: Mutex::new(Instant::now()),
83 idle_timeout: DEFAULT_IDLE_TIMEOUT,
84 locked: AtomicBool::new(false),
85 }
86 }
87
88 pub fn with_pid_file_and_timeout(
90 socket_path: PathBuf,
91 pid_file: PathBuf,
92 idle_timeout: Duration,
93 ) -> Self {
94 Self {
95 core: Arc::new(Mutex::new(AgentCore::default())),
96 socket_path,
97 pid_file: Some(pid_file),
98 running: Arc::new(AtomicBool::new(false)),
99 last_activity: Mutex::new(Instant::now()),
100 idle_timeout,
101 locked: AtomicBool::new(false),
102 }
103 }
104
105 pub fn from_core(core: AgentCore, socket_path: PathBuf) -> Self {
107 Self {
108 core: Arc::new(Mutex::new(core)),
109 socket_path,
110 pid_file: None,
111 running: Arc::new(AtomicBool::new(false)),
112 last_activity: Mutex::new(Instant::now()),
113 idle_timeout: DEFAULT_IDLE_TIMEOUT,
114 locked: AtomicBool::new(false),
115 }
116 }
117
118 pub fn socket_path(&self) -> &PathBuf {
120 &self.socket_path
121 }
122
123 pub fn pid_file(&self) -> Option<&PathBuf> {
125 self.pid_file.as_ref()
126 }
127
128 pub fn set_pid_file(&mut self, path: PathBuf) {
130 self.pid_file = Some(path);
131 }
132
133 pub fn lock(&self) -> Result<MutexGuard<'_, AgentCore>, AgentError> {
138 self.core
139 .lock()
140 .map_err(|_| AgentError::MutexError("Agent core mutex poisoned".to_string()))
141 }
142
143 pub fn core_arc(&self) -> Arc<Mutex<AgentCore>> {
145 Arc::clone(&self.core)
146 }
147
148 pub fn is_running(&self) -> bool {
150 self.running.load(Ordering::SeqCst)
151 }
152
153 pub fn set_running(&self, running: bool) {
155 self.running.store(running, Ordering::SeqCst);
156 }
157
158 pub fn idle_timeout(&self) -> Duration {
162 self.idle_timeout
163 }
164
165 pub fn touch(&self) {
167 if let Ok(mut last) = self.last_activity.lock() {
168 *last = Instant::now();
169 }
170 }
171
172 pub fn idle_duration(&self) -> Duration {
174 self.last_activity
175 .lock()
176 .map(|last| last.elapsed())
177 .unwrap_or(Duration::ZERO)
178 }
179
180 pub fn is_idle_timed_out(&self) -> bool {
182 if self.idle_timeout.is_zero() {
184 return false;
185 }
186 self.idle_duration() >= self.idle_timeout
187 }
188
189 pub fn is_agent_locked(&self) -> bool {
191 self.locked.load(Ordering::SeqCst)
192 }
193
194 pub fn lock_agent(&self) -> Result<(), AgentError> {
198 log::info!("Locking agent (clearing keys from memory)");
199
200 {
202 let mut core = self.lock()?;
203 core.clear_keys();
204 }
205
206 self.locked.store(true, Ordering::SeqCst);
208 log::debug!("Agent locked");
209 Ok(())
210 }
211
212 pub fn unlock_agent(&self) {
217 log::info!("Unlocking agent");
218 self.locked.store(false, Ordering::SeqCst);
219 self.touch(); }
221
222 pub fn check_idle_timeout(&self) -> Result<bool, AgentError> {
226 if self.is_idle_timed_out() && !self.is_agent_locked() {
227 log::info!(
228 "Agent idle for {:?}, locking due to timeout",
229 self.idle_duration()
230 );
231 self.lock_agent()?;
232 return Ok(true);
233 }
234 Ok(false)
235 }
236
237 pub fn shutdown(&self) -> Result<(), AgentError> {
244 log::info!("Shutting down agent at {:?}", self.socket_path);
245
246 {
248 let mut core = self.lock()?;
249 core.clear_keys();
250 log::debug!("Cleared all keys from agent core");
251 }
252
253 self.set_running(false);
255
256 if self.socket_path.exists() {
258 if let Err(e) = std::fs::remove_file(&self.socket_path) {
259 log::warn!("Failed to remove socket file {:?}: {}", self.socket_path, e);
260 } else {
261 log::debug!("Removed socket file {:?}", self.socket_path);
262 }
263 }
264
265 if let Some(ref pid_file) = self.pid_file
267 && pid_file.exists()
268 {
269 if let Err(e) = std::fs::remove_file(pid_file) {
270 log::warn!("Failed to remove PID file {:?}: {}", pid_file, e);
271 } else {
272 log::debug!("Removed PID file {:?}", pid_file);
273 }
274 }
275
276 log::info!("Agent shutdown complete");
277 Ok(())
278 }
279
280 pub fn key_count(&self) -> Result<usize, AgentError> {
282 let core = self.lock()?;
283 Ok(core.key_count())
284 }
285
286 pub fn public_keys(&self) -> Result<Vec<Vec<u8>>, AgentError> {
288 let core = self.lock()?;
289 Ok(core.public_keys())
290 }
291
292 pub fn register_key(&self, pkcs8_bytes: Zeroizing<Vec<u8>>) -> Result<(), AgentError> {
294 let mut core = self.lock()?;
295 core.register_key(pkcs8_bytes)
296 }
297
298 pub fn sign(&self, pubkey: &[u8], data: &[u8]) -> Result<Vec<u8>, AgentError> {
303 if self.is_agent_locked() {
305 return Err(AgentError::AgentLocked);
306 }
307
308 let core = self.lock()?;
309 let result = core.sign(pubkey, data);
310
311 if result.is_ok() {
313 self.touch();
314 }
315
316 result
317 }
318}
319
320impl Clone for AgentHandle {
321 fn clone(&self) -> Self {
322 let last_activity = self
325 .last_activity
326 .lock()
327 .map(|t| *t)
328 .unwrap_or_else(|_| Instant::now());
329
330 Self {
331 core: Arc::clone(&self.core),
332 socket_path: self.socket_path.clone(),
333 pid_file: self.pid_file.clone(),
334 running: Arc::clone(&self.running),
335 last_activity: Mutex::new(last_activity),
336 idle_timeout: self.idle_timeout,
337 locked: AtomicBool::new(self.locked.load(Ordering::SeqCst)),
338 }
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345 use ring::rand::SystemRandom;
346 use ring::signature::Ed25519KeyPair;
347 use tempfile::TempDir;
348
349 fn generate_test_pkcs8() -> Vec<u8> {
350 let rng = SystemRandom::new();
351 let pkcs8_doc = Ed25519KeyPair::generate_pkcs8(&rng).expect("Failed to generate PKCS#8");
352 pkcs8_doc.as_ref().to_vec()
353 }
354
355 #[test]
356 fn test_agent_handle_new() {
357 let handle = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
358 assert_eq!(handle.socket_path(), &PathBuf::from("/tmp/test.sock"));
359 assert!(handle.pid_file().is_none());
360 assert!(!handle.is_running());
361 }
362
363 #[test]
364 fn test_agent_handle_with_pid_file() {
365 let handle = AgentHandle::with_pid_file(
366 PathBuf::from("/tmp/test.sock"),
367 PathBuf::from("/tmp/test.pid"),
368 );
369 assert_eq!(handle.socket_path(), &PathBuf::from("/tmp/test.sock"));
370 assert_eq!(handle.pid_file(), Some(&PathBuf::from("/tmp/test.pid")));
371 }
372
373 #[test]
374 fn test_agent_handle_running_state() {
375 let handle = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
376 assert!(!handle.is_running());
377
378 handle.set_running(true);
379 assert!(handle.is_running());
380
381 handle.set_running(false);
382 assert!(!handle.is_running());
383 }
384
385 #[test]
386 fn test_agent_handle_key_operations() {
387 let handle = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
388
389 assert_eq!(handle.key_count().unwrap(), 0);
390
391 let pkcs8_bytes = generate_test_pkcs8();
392 handle
393 .register_key(Zeroizing::new(pkcs8_bytes))
394 .expect("Failed to register key");
395
396 assert_eq!(handle.key_count().unwrap(), 1);
397
398 let pubkeys = handle.public_keys().unwrap();
399 assert_eq!(pubkeys.len(), 1);
400 }
401
402 #[test]
403 fn test_agent_handle_clone_shares_state() {
404 let handle1 = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
405 let handle2 = handle1.clone();
406
407 let pkcs8_bytes = generate_test_pkcs8();
408 handle1
409 .register_key(Zeroizing::new(pkcs8_bytes))
410 .expect("Failed to register key");
411
412 assert_eq!(handle1.key_count().unwrap(), 1);
414 assert_eq!(handle2.key_count().unwrap(), 1);
415 }
416
417 #[test]
418 fn test_agent_handle_shutdown() {
419 let temp_dir = TempDir::new().unwrap();
420 let socket_path = temp_dir.path().join("test.sock");
421
422 std::fs::write(&socket_path, "dummy").unwrap();
424
425 let handle = AgentHandle::new(socket_path.clone());
426 let pkcs8_bytes = generate_test_pkcs8();
427 handle
428 .register_key(Zeroizing::new(pkcs8_bytes))
429 .expect("Failed to register key");
430 handle.set_running(true);
431
432 assert_eq!(handle.key_count().unwrap(), 1);
433 assert!(handle.is_running());
434 assert!(socket_path.exists());
435
436 handle.shutdown().expect("Shutdown failed");
437
438 assert_eq!(handle.key_count().unwrap(), 0);
439 assert!(!handle.is_running());
440 assert!(!socket_path.exists());
441 }
442
443 #[test]
444 fn test_multiple_handles_independent() {
445 let handle1 = AgentHandle::new(PathBuf::from("/tmp/agent1.sock"));
446 let handle2 = AgentHandle::new(PathBuf::from("/tmp/agent2.sock"));
447
448 let pkcs8_bytes = generate_test_pkcs8();
449 handle1
450 .register_key(Zeroizing::new(pkcs8_bytes))
451 .expect("Failed to register key");
452
453 assert_eq!(handle1.key_count().unwrap(), 1);
455 assert_eq!(handle2.key_count().unwrap(), 0);
456 }
457
458 #[test]
459 fn test_agent_handle_lock_unlock() {
460 let handle = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
461
462 assert!(!handle.is_agent_locked());
464
465 let pkcs8_bytes = generate_test_pkcs8();
467 handle
468 .register_key(Zeroizing::new(pkcs8_bytes))
469 .expect("Failed to register key");
470 assert_eq!(handle.key_count().unwrap(), 1);
471
472 handle.lock_agent().expect("Lock failed");
474 assert!(handle.is_agent_locked());
475 assert_eq!(handle.key_count().unwrap(), 0); handle.unlock_agent();
479 assert!(!handle.is_agent_locked());
480 }
481
482 #[test]
483 fn test_agent_handle_sign_when_locked() {
484 let handle = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
485
486 let pkcs8_bytes = generate_test_pkcs8();
488 handle
489 .register_key(Zeroizing::new(pkcs8_bytes))
490 .expect("Failed to register key");
491
492 let pubkeys = handle.public_keys().unwrap();
494 let pubkey = &pubkeys[0];
495
496 let result = handle.sign(pubkey, b"test data");
498 assert!(result.is_ok());
499
500 handle.lock_agent().expect("Lock failed");
502 let result = handle.sign(pubkey, b"test data");
503 assert!(matches!(result, Err(AgentError::AgentLocked)));
504 }
505
506 #[test]
507 fn test_agent_handle_idle_timeout() {
508 let handle =
510 AgentHandle::with_timeout(PathBuf::from("/tmp/test.sock"), Duration::from_millis(10));
511
512 assert!(!handle.is_idle_timed_out());
514 assert!(!handle.is_agent_locked());
515
516 std::thread::sleep(Duration::from_millis(20));
518
519 assert!(handle.is_idle_timed_out());
521
522 handle.touch();
524 assert!(!handle.is_idle_timed_out());
525 }
526
527 #[test]
528 fn test_agent_handle_zero_timeout_never_expires() {
529 let handle = AgentHandle::with_timeout(PathBuf::from("/tmp/test.sock"), Duration::ZERO);
531
532 std::thread::sleep(Duration::from_millis(10));
534
535 assert!(!handle.is_idle_timed_out());
537 }
538}