prodex 0.49.0

OpenAI profile pooling and safe auto-rotate for Codex CLI and Claude Code
Documentation
use anyhow::{Context, Result};
use std::fs;
use std::path::Path;

use super::{
    AppPaths,
    cache::{cached_policy_for, store_cached_policy},
    paths::{
        resolve_runtime_policy_path, runtime_policy_enabled_for_current_process,
        runtime_policy_path,
    },
    types::{
        RuntimePolicyConfig, RuntimePolicyFile, RuntimePolicyProxySettings,
        RuntimePolicyRuntimeSettings, RuntimePolicySecretsSettings, RuntimePolicySummary,
    },
    validate::{parse_secret_backend_kind, validate_runtime_policy_file},
};

pub(crate) fn ensure_runtime_policy_valid() -> Result<()> {
    if !runtime_policy_enabled_for_current_process() {
        return Ok(());
    }
    let paths = AppPaths::discover()?;
    let _ = load_runtime_policy_cached(&paths.root)?;
    Ok(())
}

pub(crate) fn runtime_policy_summary() -> Result<Option<RuntimePolicySummary>> {
    if !runtime_policy_enabled_for_current_process() {
        return Ok(None);
    }
    let paths = AppPaths::discover()?;
    Ok(
        load_runtime_policy_cached(&paths.root)?.map(|config| RuntimePolicySummary {
            path: config.path,
            version: config.version,
        }),
    )
}

pub(crate) fn runtime_policy_runtime() -> Option<RuntimePolicyRuntimeSettings> {
    if !runtime_policy_enabled_for_current_process() {
        return None;
    }
    let paths = AppPaths::discover().ok()?;
    load_runtime_policy_cached(&paths.root)
        .ok()
        .flatten()
        .map(|config| config.runtime)
}

pub(crate) fn runtime_policy_proxy() -> Option<RuntimePolicyProxySettings> {
    if !runtime_policy_enabled_for_current_process() {
        return None;
    }
    let paths = AppPaths::discover().ok()?;
    load_runtime_policy_cached(&paths.root)
        .ok()
        .flatten()
        .map(|config| config.runtime_proxy)
}

pub(crate) fn runtime_policy_secrets() -> Option<RuntimePolicySecretsSettings> {
    if !runtime_policy_enabled_for_current_process() {
        return None;
    }
    let paths = AppPaths::discover().ok()?;
    load_runtime_policy_cached(&paths.root)
        .ok()
        .flatten()
        .map(|config| config.secrets)
}

fn load_runtime_policy_cached(root: &Path) -> Result<Option<RuntimePolicyConfig>> {
    if let Some(cached) = cached_policy_for(root) {
        return Ok(cached);
    }
    let loaded = load_runtime_policy_from_root(root)?;
    store_cached_policy(root, loaded.clone());
    Ok(loaded)
}

pub(crate) fn load_runtime_policy_from_root(root: &Path) -> Result<Option<RuntimePolicyConfig>> {
    let path = runtime_policy_path(root);
    if !path.exists() {
        return Ok(None);
    }

    let content =
        fs::read_to_string(&path).with_context(|| format!("failed to read {}", path.display()))?;
    let parsed: RuntimePolicyFile =
        toml::from_str(&content).with_context(|| format!("failed to parse {}", path.display()))?;
    validate_runtime_policy_file(&parsed, &path)?;

    let runtime = RuntimePolicyRuntimeSettings {
        log_format: parsed.runtime.log_format,
        log_dir: parsed
            .runtime
            .log_dir
            .as_deref()
            .map(|value| resolve_runtime_policy_path(root, value))
            .transpose()?,
    };
    let secrets = RuntimePolicySecretsSettings {
        backend: parsed
            .secrets
            .backend
            .as_deref()
            .map(parse_secret_backend_kind)
            .transpose()?,
        keyring_service: parsed
            .secrets
            .keyring_service
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(str::to_string),
    };

    Ok(Some(RuntimePolicyConfig {
        path,
        version: parsed.version,
        runtime,
        runtime_proxy: parsed.runtime_proxy,
        secrets,
    }))
}