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 #[allow(clippy::disallowed_methods)] pub fn shutdown(&self) -> Result<(), AgentError> {
245 log::info!("Shutting down agent at {:?}", self.socket_path);
246
247 {
249 let mut core = self.lock()?;
250 core.clear_keys();
251 log::debug!("Cleared all keys from agent core");
252 }
253
254 self.set_running(false);
256
257 if self.socket_path.exists() {
259 if let Err(e) = std::fs::remove_file(&self.socket_path) {
260 log::warn!("Failed to remove socket file {:?}: {}", self.socket_path, e);
261 } else {
262 log::debug!("Removed socket file {:?}", self.socket_path);
263 }
264 }
265
266 if let Some(ref pid_file) = self.pid_file
268 && pid_file.exists()
269 {
270 if let Err(e) = std::fs::remove_file(pid_file) {
271 log::warn!("Failed to remove PID file {:?}: {}", pid_file, e);
272 } else {
273 log::debug!("Removed PID file {:?}", pid_file);
274 }
275 }
276
277 log::info!("Agent shutdown complete");
278 Ok(())
279 }
280
281 pub fn key_count(&self) -> Result<usize, AgentError> {
283 let core = self.lock()?;
284 Ok(core.key_count())
285 }
286
287 pub fn public_keys(&self) -> Result<Vec<Vec<u8>>, AgentError> {
289 let core = self.lock()?;
290 Ok(core.public_keys())
291 }
292
293 pub fn register_key(&self, pkcs8_bytes: Zeroizing<Vec<u8>>) -> Result<(), AgentError> {
295 let mut core = self.lock()?;
296 core.register_key(pkcs8_bytes)
297 }
298
299 pub fn sign(&self, pubkey: &[u8], data: &[u8]) -> Result<Vec<u8>, AgentError> {
304 if self.is_agent_locked() {
306 return Err(AgentError::AgentLocked);
307 }
308
309 let core = self.lock()?;
310 let result = core.sign(pubkey, data);
311
312 if result.is_ok() {
314 self.touch();
315 }
316
317 result
318 }
319}
320
321impl Clone for AgentHandle {
322 fn clone(&self) -> Self {
323 Self {
324 core: Arc::clone(&self.core),
325 socket_path: self.socket_path.clone(),
326 pid_file: self.pid_file.clone(),
327 running: Arc::clone(&self.running),
328 last_activity: Arc::clone(&self.last_activity),
329 idle_timeout: self.idle_timeout,
330 locked: Arc::clone(&self.locked),
331 }
332 }
333}
334
335#[cfg(test)]
336#[allow(clippy::disallowed_methods)]
337mod tests {
338 use super::*;
339 use ring::rand::SystemRandom;
340 use ring::signature::Ed25519KeyPair;
341 use tempfile::TempDir;
342
343 fn generate_test_pkcs8() -> Vec<u8> {
344 let rng = SystemRandom::new();
345 let pkcs8_doc = Ed25519KeyPair::generate_pkcs8(&rng).expect("Failed to generate PKCS#8");
346 pkcs8_doc.as_ref().to_vec()
347 }
348
349 #[test]
350 fn test_agent_handle_new() {
351 let handle = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
352 assert_eq!(handle.socket_path(), &PathBuf::from("/tmp/test.sock"));
353 assert!(handle.pid_file().is_none());
354 assert!(!handle.is_running());
355 }
356
357 #[test]
358 fn test_agent_handle_with_pid_file() {
359 let handle = AgentHandle::with_pid_file(
360 PathBuf::from("/tmp/test.sock"),
361 PathBuf::from("/tmp/test.pid"),
362 );
363 assert_eq!(handle.socket_path(), &PathBuf::from("/tmp/test.sock"));
364 assert_eq!(handle.pid_file(), Some(&PathBuf::from("/tmp/test.pid")));
365 }
366
367 #[test]
368 fn test_agent_handle_running_state() {
369 let handle = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
370 assert!(!handle.is_running());
371
372 handle.set_running(true);
373 assert!(handle.is_running());
374
375 handle.set_running(false);
376 assert!(!handle.is_running());
377 }
378
379 #[test]
380 fn test_agent_handle_key_operations() {
381 let handle = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
382
383 assert_eq!(handle.key_count().unwrap(), 0);
384
385 let pkcs8_bytes = generate_test_pkcs8();
386 handle
387 .register_key(Zeroizing::new(pkcs8_bytes))
388 .expect("Failed to register key");
389
390 assert_eq!(handle.key_count().unwrap(), 1);
391
392 let pubkeys = handle.public_keys().unwrap();
393 assert_eq!(pubkeys.len(), 1);
394 }
395
396 #[test]
397 fn test_agent_handle_clone_shares_state() {
398 let handle1 = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
399 let handle2 = handle1.clone();
400
401 let pkcs8_bytes = generate_test_pkcs8();
402 handle1
403 .register_key(Zeroizing::new(pkcs8_bytes))
404 .expect("Failed to register key");
405
406 assert_eq!(handle1.key_count().unwrap(), 1);
408 assert_eq!(handle2.key_count().unwrap(), 1);
409 }
410
411 #[test]
412 fn test_agent_handle_shutdown() {
413 let temp_dir = TempDir::new().unwrap();
414 let socket_path = temp_dir.path().join("test.sock");
415
416 std::fs::write(&socket_path, "dummy").unwrap();
418
419 let handle = AgentHandle::new(socket_path.clone());
420 let pkcs8_bytes = generate_test_pkcs8();
421 handle
422 .register_key(Zeroizing::new(pkcs8_bytes))
423 .expect("Failed to register key");
424 handle.set_running(true);
425
426 assert_eq!(handle.key_count().unwrap(), 1);
427 assert!(handle.is_running());
428 assert!(socket_path.exists());
429
430 handle.shutdown().expect("Shutdown failed");
431
432 assert_eq!(handle.key_count().unwrap(), 0);
433 assert!(!handle.is_running());
434 assert!(!socket_path.exists());
435 }
436
437 #[test]
438 fn test_multiple_handles_independent() {
439 let handle1 = AgentHandle::new(PathBuf::from("/tmp/agent1.sock"));
440 let handle2 = AgentHandle::new(PathBuf::from("/tmp/agent2.sock"));
441
442 let pkcs8_bytes = generate_test_pkcs8();
443 handle1
444 .register_key(Zeroizing::new(pkcs8_bytes))
445 .expect("Failed to register key");
446
447 assert_eq!(handle1.key_count().unwrap(), 1);
449 assert_eq!(handle2.key_count().unwrap(), 0);
450 }
451
452 #[test]
453 fn test_agent_handle_lock_unlock() {
454 let handle = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
455
456 assert!(!handle.is_agent_locked());
458
459 let pkcs8_bytes = generate_test_pkcs8();
461 handle
462 .register_key(Zeroizing::new(pkcs8_bytes))
463 .expect("Failed to register key");
464 assert_eq!(handle.key_count().unwrap(), 1);
465
466 handle.lock_agent().expect("Lock failed");
468 assert!(handle.is_agent_locked());
469 assert_eq!(handle.key_count().unwrap(), 0); handle.unlock_agent();
473 assert!(!handle.is_agent_locked());
474 }
475
476 #[test]
477 fn test_agent_handle_sign_when_locked() {
478 let handle = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
479
480 let pkcs8_bytes = generate_test_pkcs8();
482 handle
483 .register_key(Zeroizing::new(pkcs8_bytes))
484 .expect("Failed to register key");
485
486 let pubkeys = handle.public_keys().unwrap();
488 let pubkey = &pubkeys[0];
489
490 let result = handle.sign(pubkey, b"test data");
492 assert!(result.is_ok());
493
494 handle.lock_agent().expect("Lock failed");
496 let result = handle.sign(pubkey, b"test data");
497 assert!(matches!(result, Err(AgentError::AgentLocked)));
498 }
499
500 #[test]
501 fn test_agent_handle_idle_timeout() {
502 let handle =
504 AgentHandle::with_timeout(PathBuf::from("/tmp/test.sock"), Duration::from_millis(10));
505
506 assert!(!handle.is_idle_timed_out());
508 assert!(!handle.is_agent_locked());
509
510 std::thread::sleep(Duration::from_millis(20));
512
513 assert!(handle.is_idle_timed_out());
515
516 handle.touch();
518 assert!(!handle.is_idle_timed_out());
519 }
520
521 #[test]
522 fn test_agent_handle_zero_timeout_never_expires() {
523 let handle = AgentHandle::with_timeout(PathBuf::from("/tmp/test.sock"), Duration::ZERO);
525
526 std::thread::sleep(Duration::from_millis(10));
528
529 assert!(!handle.is_idle_timed_out());
531 }
532
533 #[test]
534 fn test_clone_shares_locked_state() {
535 let handle_a = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
536 let handle_b = handle_a.clone();
537
538 assert!(!handle_b.is_agent_locked());
539 handle_a.lock_agent().unwrap();
540 assert!(handle_b.is_agent_locked());
541
542 handle_a.unlock_agent();
543 assert!(!handle_b.is_agent_locked());
544 }
545
546 #[test]
547 fn test_clone_shares_last_activity() {
548 let handle_a =
549 AgentHandle::with_timeout(PathBuf::from("/tmp/test.sock"), Duration::from_millis(50));
550 let handle_b = handle_a.clone();
551
552 std::thread::sleep(Duration::from_millis(60));
553 assert!(handle_b.is_idle_timed_out());
554
555 handle_a.touch();
557 assert!(!handle_b.is_idle_timed_out());
558 }
559
560 #[test]
561 fn test_clone_sign_returns_locked_after_other_clone_locks() {
562 let handle_a = AgentHandle::new(PathBuf::from("/tmp/test.sock"));
563 let handle_b = handle_a.clone();
564
565 let pkcs8_bytes = generate_test_pkcs8();
566 handle_a.register_key(Zeroizing::new(pkcs8_bytes)).unwrap();
567
568 let pubkeys = handle_a.public_keys().unwrap();
569 let pubkey = &pubkeys[0];
570
571 assert!(handle_b.sign(pubkey, b"test data").is_ok());
572
573 handle_a.lock_agent().unwrap();
574 let result = handle_b.sign(pubkey, b"test data");
575 assert!(matches!(result, Err(AgentError::AgentLocked)));
576 }
577}