Skip to main content

capo_agent/permissions/
session_cache.rs

1use std::collections::HashMap;
2use std::sync::Mutex;
3
4use super::Decision;
5
6/// Per-session allow/deny cache keyed by `(tool_name, args_fingerprint)`.
7#[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    // Stable short fingerprint: first 16 chars of the canonical JSON form.
35    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}