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: Arc<Mutex<Instant>>,
38 idle_timeout: Duration,
40 locked: Arc<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: Arc::new(Mutex::new(Instant::now())),
70 idle_timeout,
71 locked: Arc::new(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: Arc::new(Mutex::new(Instant::now())),
83 idle_timeout: DEFAULT_IDLE_TIMEOUT,
84 locked: Arc::new(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: Arc::new(Mutex::new(Instant::now())),
100 idle_timeout,
101 locked: Arc::new(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: Arc::new(Mutex::new(Instant::now())),
113 idle_timeout: DEFAULT_IDLE_TIMEOUT,
114 locked: Arc::new(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 Self {
323 core: Arc::clone(&self.core),
324 socket_path: self.socket_path.clone(),
325 pid_file: self.pid_file.clone(),
326 running: Arc::clone(&self.running),
327 last_activity: Arc::clone(&self.last_activity),
328 idle_timeout: self.idle_timeout,
329 locked: Arc::clone(&self.locked),
330 }
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use ring::rand::SystemRandom;
338 use ring::signature::Ed25519KeyPair;
339 use tempfile::TempDir;
340
341 fn generate_test_pkcs8() -> Vec<u8> {
342 let rng = SystemRandom::new();
343 let pkcs8_doc = Ed25519KeyPair::generate_pkcs8(&rng).expect("Failed to generate PKCS#8");
344 pkcs8_doc.as_ref().to_vec()
345 }
346
347 #[test]
348 fn test_agent_handle_new() {
349 let handle = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
350 assert_eq!(handle.socket_path(), &PathBuf::from("/tmp/test.sock"));
351 assert!(handle.pid_file().is_none());
352 assert!(!handle.is_running());
353 }
354
355 #[test]
356 fn test_agent_handle_with_pid_file() {
357 let handle = AgentHandle::with_pid_file(
358 PathBuf::from("/tmp/test.sock"),
359 PathBuf::from("/tmp/test.pid"),
360 );
361 assert_eq!(handle.socket_path(), &PathBuf::from("/tmp/test.sock"));
362 assert_eq!(handle.pid_file(), Some(&PathBuf::from("/tmp/test.pid")));
363 }
364
365 #[test]
366 fn test_agent_handle_running_state() {
367 let handle = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
368 assert!(!handle.is_running());
369
370 handle.set_running(true);
371 assert!(handle.is_running());
372
373 handle.set_running(false);
374 assert!(!handle.is_running());
375 }
376
377 #[test]
378 fn test_agent_handle_key_operations() {
379 let handle = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
380
381 assert_eq!(handle.key_count().unwrap(), 0);
382
383 let pkcs8_bytes = generate_test_pkcs8();
384 handle
385 .register_key(Zeroizing::new(pkcs8_bytes))
386 .expect("Failed to register key");
387
388 assert_eq!(handle.key_count().unwrap(), 1);
389
390 let pubkeys = handle.public_keys().unwrap();
391 assert_eq!(pubkeys.len(), 1);
392 }
393
394 #[test]
395 fn test_agent_handle_clone_shares_state() {
396 let handle1 = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
397 let handle2 = handle1.clone();
398
399 let pkcs8_bytes = generate_test_pkcs8();
400 handle1
401 .register_key(Zeroizing::new(pkcs8_bytes))
402 .expect("Failed to register key");
403
404 assert_eq!(handle1.key_count().unwrap(), 1);
406 assert_eq!(handle2.key_count().unwrap(), 1);
407 }
408
409 #[test]
410 fn test_agent_handle_shutdown() {
411 let temp_dir = TempDir::new().unwrap();
412 let socket_path = temp_dir.path().join("test.sock");
413
414 std::fs::write(&socket_path, "dummy").unwrap();
416
417 let handle = AgentHandle::new(socket_path.clone());
418 let pkcs8_bytes = generate_test_pkcs8();
419 handle
420 .register_key(Zeroizing::new(pkcs8_bytes))
421 .expect("Failed to register key");
422 handle.set_running(true);
423
424 assert_eq!(handle.key_count().unwrap(), 1);
425 assert!(handle.is_running());
426 assert!(socket_path.exists());
427
428 handle.shutdown().expect("Shutdown failed");
429
430 assert_eq!(handle.key_count().unwrap(), 0);
431 assert!(!handle.is_running());
432 assert!(!socket_path.exists());
433 }
434
435 #[test]
436 fn test_multiple_handles_independent() {
437 let handle1 = AgentHandle::new(PathBuf::from("/tmp/agent1.sock"));
438 let handle2 = AgentHandle::new(PathBuf::from("/tmp/agent2.sock"));
439
440 let pkcs8_bytes = generate_test_pkcs8();
441 handle1
442 .register_key(Zeroizing::new(pkcs8_bytes))
443 .expect("Failed to register key");
444
445 assert_eq!(handle1.key_count().unwrap(), 1);
447 assert_eq!(handle2.key_count().unwrap(), 0);
448 }
449
450 #[test]
451 fn test_agent_handle_lock_unlock() {
452 let handle = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
453
454 assert!(!handle.is_agent_locked());
456
457 let pkcs8_bytes = generate_test_pkcs8();
459 handle
460 .register_key(Zeroizing::new(pkcs8_bytes))
461 .expect("Failed to register key");
462 assert_eq!(handle.key_count().unwrap(), 1);
463
464 handle.lock_agent().expect("Lock failed");
466 assert!(handle.is_agent_locked());
467 assert_eq!(handle.key_count().unwrap(), 0); handle.unlock_agent();
471 assert!(!handle.is_agent_locked());
472 }
473
474 #[test]
475 fn test_agent_handle_sign_when_locked() {
476 let handle = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
477
478 let pkcs8_bytes = generate_test_pkcs8();
480 handle
481 .register_key(Zeroizing::new(pkcs8_bytes))
482 .expect("Failed to register key");
483
484 let pubkeys = handle.public_keys().unwrap();
486 let pubkey = &pubkeys[0];
487
488 let result = handle.sign(pubkey, b"test data");
490 assert!(result.is_ok());
491
492 handle.lock_agent().expect("Lock failed");
494 let result = handle.sign(pubkey, b"test data");
495 assert!(matches!(result, Err(AgentError::AgentLocked)));
496 }
497
498 #[test]
499 fn test_agent_handle_idle_timeout() {
500 let handle =
502 AgentHandle::with_timeout(PathBuf::from("/tmp/test.sock"), Duration::from_millis(10));
503
504 assert!(!handle.is_idle_timed_out());
506 assert!(!handle.is_agent_locked());
507
508 std::thread::sleep(Duration::from_millis(20));
510
511 assert!(handle.is_idle_timed_out());
513
514 handle.touch();
516 assert!(!handle.is_idle_timed_out());
517 }
518
519 #[test]
520 fn test_agent_handle_zero_timeout_never_expires() {
521 let handle = AgentHandle::with_timeout(PathBuf::from("/tmp/test.sock"), Duration::ZERO);
523
524 std::thread::sleep(Duration::from_millis(10));
526
527 assert!(!handle.is_idle_timed_out());
529 }
530
531 #[test]
532 fn test_clone_shares_locked_state() {
533 let handle_a = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
534 let handle_b = handle_a.clone();
535
536 assert!(!handle_b.is_agent_locked());
537 handle_a.lock_agent().unwrap();
538 assert!(handle_b.is_agent_locked());
539
540 handle_a.unlock_agent();
541 assert!(!handle_b.is_agent_locked());
542 }
543
544 #[test]
545 fn test_clone_shares_last_activity() {
546 let handle_a =
547 AgentHandle::with_timeout(PathBuf::from("/tmp/test.sock"), Duration::from_millis(50));
548 let handle_b = handle_a.clone();
549
550 std::thread::sleep(Duration::from_millis(60));
551 assert!(handle_b.is_idle_timed_out());
552
553 handle_a.touch();
555 assert!(!handle_b.is_idle_timed_out());
556 }
557
558 #[test]
559 fn test_clone_sign_returns_locked_after_other_clone_locks() {
560 let handle_a = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
561 let handle_b = handle_a.clone();
562
563 let pkcs8_bytes = generate_test_pkcs8();
564 handle_a.register_key(Zeroizing::new(pkcs8_bytes)).unwrap();
565
566 let pubkeys = handle_a.public_keys().unwrap();
567 let pubkey = &pubkeys[0];
568
569 assert!(handle_b.sign(pubkey, b"test data").is_ok());
570
571 handle_a.lock_agent().unwrap();
572 let result = handle_b.sign(pubkey, b"test data");
573 assert!(matches!(result, Err(AgentError::AgentLocked)));
574 }
575}