rho-coding-agent 0.7.0

A lightweight agent harness inspired by Pi
use serde::Deserialize;

use crate::{
    credentials::{
        load_codex_tokens, load_openai_api_key, save_codex_tokens, CodexTokens, OsCredentialStore,
    },
    model::ModelError,
};

pub(super) enum Auth {
    ApiKey(String),
    Codex {
        tokens: CodexTokens,
        source: CodexAuthSource,
    },
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum CodexAuthSource {
    Env,
    Store,
}

#[derive(Deserialize)]
struct RefreshResponse {
    id_token: Option<String>,
    access_token: Option<String>,
    refresh_token: Option<String>,
    account_id: Option<String>,
}

pub(super) fn load_api_key_auth() -> Result<Auth, ModelError> {
    if let Ok(key) = std::env::var("OPENAI_API_KEY") {
        return Ok(Auth::ApiKey(key));
    }
    let store = OsCredentialStore;
    let key = load_openai_api_key(&store)?.ok_or(ModelError::MissingApiKey)?;
    Ok(Auth::ApiKey(key))
}

pub(super) fn load_codex_auth() -> Result<Auth, ModelError> {
    if let Ok(access_token) = std::env::var("CODEX_ACCESS_TOKEN") {
        return Ok(Auth::Codex {
            tokens: CodexTokens {
                access_token,
                refresh_token: None,
                id_token: None,
                account_id: std::env::var("CODEX_ACCOUNT_ID").ok(),
            },
            source: CodexAuthSource::Env,
        });
    }
    let store = OsCredentialStore;
    let tokens = load_codex_tokens(&store)?.ok_or(ModelError::MissingCodexAuth)?;
    Ok(Auth::Codex {
        tokens,
        source: CodexAuthSource::Store,
    })
}

pub(super) fn load_codex_tokens_for_request(
    tokens: &CodexTokens,
    source: CodexAuthSource,
) -> Result<CodexTokens, ModelError> {
    match source {
        CodexAuthSource::Env => Ok(tokens.clone()),
        CodexAuthSource::Store => {
            let store = OsCredentialStore;
            load_codex_tokens(&store)?.ok_or(ModelError::MissingCodexAuth)
        }
    }
}

pub(super) async fn refresh_codex_token(
    client: &reqwest::Client,
    refresh_token: &str,
    source: CodexAuthSource,
    previous: &CodexTokens,
) -> Result<CodexTokens, ModelError> {
    let response: RefreshResponse = client
        .post("https://auth.openai.com/oauth/token")
        .form(&[
            ("client_id", "app_EMoamEEZ73f0CkXaXp7hrann"),
            ("grant_type", "refresh_token"),
            ("refresh_token", refresh_token),
        ])
        .send()
        .await?
        .error_for_status()?
        .json()
        .await?;

    let access_token = response.access_token.ok_or_else(|| {
        ModelError::InvalidResponse("refresh response missing access_token".into())
    })?;
    let refreshed = CodexTokens {
        access_token,
        refresh_token: Some(
            response
                .refresh_token
                .unwrap_or_else(|| refresh_token.to_string()),
        ),
        id_token: response.id_token.or_else(|| previous.id_token.clone()),
        account_id: response.account_id.or_else(|| previous.account_id.clone()),
    };

    if source == CodexAuthSource::Store {
        let store = OsCredentialStore;
        save_codex_tokens(&store, &refreshed)?;
    }

    Ok(refreshed)
}