capo_agent/permissions/
session_cache.rs1use std::collections::HashMap;
2use std::sync::Mutex;
3
4use super::Decision;
5
6#[derive(Debug, Default)]
8pub struct SessionCache {
9 inner: Mutex<HashMap<String, Decision>>,
10}
11
12impl SessionCache {
13 pub fn new() -> Self {
14 Self::default()
15 }
16
17 pub fn get(&self, key: &str) -> Option<Decision> {
18 self.inner.lock().ok()?.get(key).cloned()
19 }
20
21 pub fn insert(&self, key: String, decision: Decision) {
22 if let Ok(mut guard) = self.inner.lock() {
23 guard.insert(key, decision);
24 }
25 }
26
27 pub fn key(tool: &str, args: &serde_json::Value) -> String {
28 let fingerprint = fingerprint(args);
29 format!("{tool}::{fingerprint}")
30 }
31}
32
33fn fingerprint(args: &serde_json::Value) -> String {
34 let s = serde_json::to_string(args).unwrap_or_default();
36 let mut hasher = siphasher::sip::SipHasher13::new();
37 std::hash::Hash::hash(&s, &mut hasher);
38 format!("{:016x}", std::hash::Hasher::finish(&hasher))
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44 use crate::permissions::Decision;
45
46 #[test]
47 fn cache_round_trip() {
48 let c = SessionCache::new();
49 let key = SessionCache::key("bash", &serde_json::json!({"command":"git status"}));
50 c.insert(key.clone(), Decision::Allowed);
51 assert!(matches!(c.get(&key), Some(Decision::Allowed)));
52 }
53
54 #[test]
55 fn key_differs_by_args() {
56 let k1 = SessionCache::key("bash", &serde_json::json!({"command":"git status"}));
57 let k2 = SessionCache::key("bash", &serde_json::json!({"command":"git diff"}));
58 assert_ne!(k1, k2);
59 }
60}