openlatch-client 0.6.23

OpenLatch runtime enforcement node — the capture-and-enforce adapter that evaluates every covered action against a coding agent's Autonomy Zone before it runs
//! The version each agent was last seen at, by this process.
//!
//! Three sources, in order of trust. **Reported**: the agent answered the
//! daemon itself — Codex's app-server `initialize` reply names its version,
//! and that is the Codex the daemon asks to trust our hooks. **On events**:
//! the version the egress stamped on the agent's last event (`agentversion`).
//! **Installed**: what each binding reads from the host
//! ([`AgentBinding::installed_versions`]), refreshed at start and every 12 h
//! by [`refresh_installed`] — the only source for an agent nobody uses, and
//! the only one that sees each install of a multi-surface agent
//! (`cursor:cli`, `cline:app`). Telemetry that has no payload of its own to
//! read a version from asks here.
//!
//! [`AgentBinding::installed_versions`]: crate::hooks::binding::AgentBinding::installed_versions
//!
//! In memory only, and never a path or an id: a version string is aggregate
//! context, safe for telemetry.

use std::collections::{BTreeMap, HashMap};
use std::sync::{Mutex, OnceLock};

#[derive(Default)]
struct Seen {
    reported: Option<String>,
    on_events: Option<String>,
}

fn seen() -> &'static Mutex<HashMap<String, Seen>> {
    static SEEN: OnceLock<Mutex<HashMap<String, Seen>>> = OnceLock::new();
    SEEN.get_or_init(Mutex::default)
}

/// The last [`refresh_installed`]'s answer, by surface key.
fn installed() -> &'static Mutex<BTreeMap<String, String>> {
    static INSTALLED: OnceLock<Mutex<BTreeMap<String, String>>> = OnceLock::new();
    INSTALLED.get_or_init(Mutex::default)
}

/// Ask every detected agent what is installed, and keep that as the
/// installed view — replaced whole, so an uninstalled surface drops out.
///
/// Blocking: CLIs are asked `--version`, each bounded by
/// [`crate::hooks::cli_version::TIMEOUT`]. Run it on a blocking thread, never
/// on the hook path. Not in unit tests, which must not run the developer's
/// own agent CLIs.
pub fn refresh_installed() {
    if cfg!(test) {
        return;
    }
    let found: BTreeMap<String, String> = crate::hooks::detect_agents()
        .iter()
        .flat_map(|agent| agent.binding.installed_versions())
        .filter(|(key, version)| !key.is_empty() && looks_like_version(version))
        .collect();
    tracing::info!(versions = ?found, "installed agent versions read");
    set_installed(found);
}

/// Add one installed version, for tests outside this module.
#[cfg(test)]
pub(crate) fn set_installed_for_tests(key: &str, version: &str) {
    installed()
        .lock()
        .unwrap_or_else(|e| e.into_inner())
        .insert(key.to_string(), version.to_string());
}

fn set_installed(found: BTreeMap<String, String>) {
    *installed().lock().unwrap_or_else(|e| e.into_inner()) = found;
}

/// Whether `value` has the shape of a version: what is accepted from a file or
/// a reply this process does not control.
pub(crate) fn looks_like_version(value: &str) -> bool {
    !value.is_empty()
        && value.len() <= 64
        && value.starts_with(|c: char| c.is_ascii_digit())
        && value
            .chars()
            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '+'))
}

/// The agent told the daemon its version itself.
pub fn record_reported(agent: &str, version: &str) {
    record(agent, version, |s| &mut s.reported);
}

/// The egress stamped `version` on one of the agent's events.
pub fn record_on_events(agent: &str, version: &str) {
    record(agent, version, |s| &mut s.on_events);
}

fn record(agent: &str, version: &str, slot: impl FnOnce(&mut Seen) -> &mut Option<String>) {
    if agent.is_empty() || !looks_like_version(version) {
        return;
    }
    let mut seen = seen().lock().unwrap_or_else(|e| e.into_inner());
    *slot(seen.entry(agent.to_string()).or_default()) = Some(version.to_string());
}

/// The agent's version: the one it reported, else the one its events
/// carried — both the build that actually ran — else its main install's.
/// `None` until one is known.
pub fn latest(agent: &str) -> Option<String> {
    let ran = {
        let seen = seen().lock().unwrap_or_else(|e| e.into_inner());
        seen.get(agent)
            .and_then(|s| s.reported.clone().or_else(|| s.on_events.clone()))
    };
    ran.or_else(|| {
        installed()
            .lock()
            .unwrap_or_else(|e| e.into_inner())
            .get(agent)
            .cloned()
    })
}

/// Every version this process knows, by surface key, by [`latest`]'s
/// precedence: every install found on the host, with the version that
/// actually ran in place of the main install's. A host-level view for
/// telemetry that is not about one agent.
pub fn all() -> BTreeMap<String, String> {
    let mut all = installed()
        .lock()
        .unwrap_or_else(|e| e.into_inner())
        .clone();
    let seen = seen().lock().unwrap_or_else(|e| e.into_inner());
    for (agent, s) in seen.iter() {
        if let Some(version) = s.reported.clone().or_else(|| s.on_events.clone()) {
            all.insert(agent.clone(), version);
        }
    }
    all
}

/// The Codex version in its app-server `initialize` reply's `userAgent`:
/// `<client name>/<codex version> (<os>; <arch>) …`.
pub fn from_codex_user_agent(user_agent: &str) -> Option<&str> {
    let product = user_agent.split_whitespace().next()?;
    let (_, version) = product.split_once('/')?;
    looks_like_version(version).then_some(version)
}

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

    /// Each test uses its own agent name: the registry is process-wide.
    #[test]
    fn a_reported_version_wins_over_one_seen_on_events() {
        assert_eq!(latest("test-agent-a"), None);
        record_on_events("test-agent-a", "0.129.0");
        assert_eq!(latest("test-agent-a").as_deref(), Some("0.129.0"));
        record_reported("test-agent-a", "0.130.0");
        record_on_events("test-agent-a", "0.131.0");
        assert_eq!(latest("test-agent-a").as_deref(), Some("0.130.0"));
    }

    #[test]
    fn all_lists_every_known_agent_by_the_same_precedence() {
        record_on_events("test-agent-c", "1.0.0");
        record_reported("test-agent-c", "1.0.1");
        record_on_events("test-agent-d", "2.0.0");
        let all = all();
        assert_eq!(all.get("test-agent-c").map(String::as_str), Some("1.0.1"));
        assert_eq!(all.get("test-agent-d").map(String::as_str), Some("2.0.0"));
        assert!(!all.contains_key("test-agent-never-seen"));
    }

    /// An installed version fills in for an agent never seen running, and
    /// a second install of the same agent is reported under its own key.
    #[test]
    fn installs_fill_in_and_keep_their_own_keys() {
        // Keys added, never the map replaced: tests share this registry.
        set_installed_for_tests("test-agent-e", "3.0.0");
        set_installed_for_tests("test-agent-e:cli", "2.9.0");
        assert_eq!(latest("test-agent-e").as_deref(), Some("3.0.0"));
        record_on_events("test-agent-e", "3.0.1");
        assert_eq!(
            latest("test-agent-e").as_deref(),
            Some("3.0.1"),
            "what ran wins"
        );
        let all = all();
        assert_eq!(all.get("test-agent-e").map(String::as_str), Some("3.0.1"));
        assert_eq!(
            all.get("test-agent-e:cli").map(String::as_str),
            Some("2.9.0")
        );
    }

    #[test]
    fn what_is_not_a_version_is_not_recorded() {
        for bad in ["", "v", "latest", "0.1 rm", "/etc/passwd", &"9".repeat(65)] {
            record_on_events("test-agent-b", bad);
        }
        record_reported("", "0.130.0");
        assert_eq!(latest("test-agent-b"), None);
        assert!(looks_like_version("0.164.0-alpha.6"));
    }

    /// What Codex 0.162.1 answered, and the shape 0.130.0 builds the same way.
    #[test]
    fn the_codex_user_agent_names_its_version() {
        assert_eq!(
            from_codex_user_agent(
                "openlatch/0.162.1 (Mac OS 26.6.2; arm64) iTerm.app/3.7.6 (openlatch; 0.6.23)"
            ),
            Some("0.162.1")
        );
        assert_eq!(
            from_codex_user_agent("openlatch/0.130.0 (Windows 11; x86_64)"),
            Some("0.130.0")
        );
        assert_eq!(from_codex_user_agent("no-slash-here"), None);
        assert_eq!(from_codex_user_agent(""), None);
    }
}