capo-agent 0.5.0

Coding-agent library built on motosan-agent-loop. Composable, embeddable.
Documentation
use std::collections::HashMap;
use std::sync::Mutex;

use super::Decision;

/// Per-session allow/deny cache keyed by `(tool_name, args_fingerprint)`.
#[derive(Debug, Default)]
pub struct SessionCache {
    inner: Mutex<HashMap<String, Decision>>,
}

impl SessionCache {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn get(&self, key: &str) -> Option<Decision> {
        self.inner.lock().ok()?.get(key).cloned()
    }

    pub fn insert(&self, key: String, decision: Decision) {
        if let Ok(mut guard) = self.inner.lock() {
            guard.insert(key, decision);
        }
    }

    pub fn key(tool: &str, args: &serde_json::Value) -> String {
        let fingerprint = fingerprint(args);
        format!("{tool}::{fingerprint}")
    }
}

fn fingerprint(args: &serde_json::Value) -> String {
    // Stable short fingerprint: first 16 chars of the canonical JSON form.
    let s = serde_json::to_string(args).unwrap_or_default();
    let mut hasher = siphasher::sip::SipHasher13::new();
    std::hash::Hash::hash(&s, &mut hasher);
    format!("{:016x}", std::hash::Hasher::finish(&hasher))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::permissions::Decision;

    #[test]
    fn cache_round_trip() {
        let c = SessionCache::new();
        let key = SessionCache::key("bash", &serde_json::json!({"command":"git status"}));
        c.insert(key.clone(), Decision::Allowed);
        assert!(matches!(c.get(&key), Some(Decision::Allowed)));
    }

    #[test]
    fn key_differs_by_args() {
        let k1 = SessionCache::key("bash", &serde_json::json!({"command":"git status"}));
        let k2 = SessionCache::key("bash", &serde_json::json!({"command":"git diff"}));
        assert_ne!(k1, k2);
    }
}