malvin 0.2.5

Non-interactive research and coding agent
use std::path::{Path, PathBuf};

use crate::acp::AuthError;

use super::discover::resolve_codex_bin;

pub fn ensure_codex_authenticated() -> Result<(), AuthError> {
    resolve_codex_bin().map_err(AuthError)?;
    if has_codex_login() {
        return Ok(());
    }
    Err(AuthError(
        "Codex backend is not authenticated. Run `codex login` or set OPENAI_API_KEY. Expected login state in $CODEX_HOME/auth.json (default ~/.codex/auth.json).".into(),
    ))
}

fn has_codex_login() -> bool {
    crate::acp::env_key_nonempty("OPENAI_API_KEY") || auth_file_has_login(&codex_auth_path())
}

fn codex_auth_path() -> PathBuf {
    if let Some(home) = std::env::var_os("CODEX_HOME").filter(|s| !s.is_empty()) {
        return PathBuf::from(home).join("auth.json");
    }
    crate::user_home::user_home_dir()
        .join(".codex")
        .join("auth.json")
}

fn auth_file_has_login(path: &Path) -> bool {
    let Ok(text) = std::fs::read_to_string(path) else {
        return false;
    };
    let Ok(value) = serde_json::from_str::<serde_json::Value>(&text) else {
        return false;
    };
    nonempty_json_str(value.get("OPENAI_API_KEY"))
        || nonempty_json_str(value.pointer("/tokens/access_token"))
        || nonempty_json_str(value.pointer("/tokens/refresh_token"))
}

fn nonempty_json_str(value: Option<&serde_json::Value>) -> bool {
    value
        .and_then(serde_json::Value::as_str)
        .is_some_and(|s| !s.trim().is_empty())
}

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

    #[test]
    fn auth_file_accepts_tokens_or_key() {
        let tmp = tempfile::tempdir().expect("tmp");
        let path = tmp.path().join("auth.json");
        assert!(!auth_file_has_login(&path));
        std::fs::write(&path, r#"{"tokens":{"access_token":"tok"}}"#).expect("write");
        assert!(auth_file_has_login(&path));
        std::fs::write(&path, r#"{"OPENAI_API_KEY":"sk"}"#).expect("write");
        assert!(auth_file_has_login(&path));
        std::fs::write(&path, r#"{"tokens":{}}"#).expect("write");
        assert!(!auth_file_has_login(&path));
    }

    #[test]
    fn ensure_codex_authenticated_ok_with_env_key() {
        let _lock = crate::test_utils::test_env_lock();
        let tmp = tempfile::tempdir().expect("tmp");
        crate::acp::with_env(
            "CODEX_HOME",
            Some(tmp.path().to_str().expect("utf8")),
            || {
                crate::acp::with_env("OPENAI_API_KEY", Some("test-key"), || {
                    crate::acp::with_env("MALVIN_CODEX", None, || {
                        if resolve_codex_bin().is_ok() {
                            assert!(ensure_codex_authenticated().is_ok());
                        }
                    });
                });
            },
        );
    }
}