caretta 0.16.4

caretta agent
// Copyright (c) 2026 Geoff Seemueller
//
// Licensed under the MIT License or Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// See LICENSE-MIT or LICENSE-APACHE for the full license text.
//
// Additionally, this file is subject to the Revenue Sharing Agreement terms
// as defined in REVENUE-SHARING.md for covered organizations.

use crate::agent::adapter_dispatch;
use crate::agent::cmd::log;
use crate::agent::grok_auth::grok_subscription_auth_active;
use crate::agent::types::{AgentLaunchOverrides, Config};

pub fn local_inference_overrides(cfg: &Config) -> AgentLaunchOverrides {
    let local = &cfg.local_inference;
    if !local.advanced {
        return AgentLaunchOverrides::default();
    }

    let base_url = local.base_url.trim();
    if base_url.is_empty() {
        return AgentLaunchOverrides::default();
    }

    let (args, env) = adapter_dispatch::launch_local_inference(
        cfg.agent,
        base_url,
        local.api_key.trim(),
        local.model.trim(),
    );
    AgentLaunchOverrides { args, env }
}

/// Generate CLI arguments and/or env vars to pass the user's model selection
/// to the agent subprocess. Returns empty overrides when the model is "Default"
/// (empty string) or when local_inference already provides a model override.
pub fn model_selection_overrides(cfg: &Config) -> AgentLaunchOverrides {
    // Local inference model takes priority.
    if cfg.local_inference.advanced
        && !cfg.local_inference.base_url.trim().is_empty()
        && !cfg.local_inference.model.trim().is_empty()
    {
        return AgentLaunchOverrides::default();
    }

    let model = cfg.model.trim();
    if model.is_empty() {
        return AgentLaunchOverrides::default();
    }

    let (args, env) = adapter_dispatch::launch_model_selection(cfg.agent, model);
    AgentLaunchOverrides { args, env }
}

/// Generate CLI arguments to skip permission prompts when `auto_mode` is enabled.
/// Returns empty overrides when `auto_mode` is false or the agent has no such flag.
pub fn auto_mode_overrides(cfg: &Config) -> AgentLaunchOverrides {
    if !cfg.auto_mode {
        return AgentLaunchOverrides::default();
    }

    AgentLaunchOverrides {
        args: adapter_dispatch::launch_auto_mode(cfg.agent),
        env: Vec::new(),
    }
}

pub fn merged_agent_env(cfg: &Config, extra_env: &[(String, String)]) -> Vec<(String, String)> {
    let mut env = local_inference_overrides(cfg).env;
    env.extend(model_selection_overrides(cfg).env);
    env.extend(adapter_dispatch::launch_env(cfg.agent));
    env.extend(extra_env.iter().cloned());

    // #11: Ensure `gh` never uses a pager (e.g. `less`) when the agent
    // invokes it to fetch context or submit PRs.
    env.push(("GH_PAGER".to_string(), "cat".to_string()));

    if cfg.use_subscription || grok_subscription_auth_active() {
        env.push(("ANTHROPIC_API_KEY".to_string(), "".to_string()));
        env.push(("OPENAI_API_KEY".to_string(), "".to_string()));
        env.push(("GEMINI_API_KEY".to_string(), "".to_string()));
        env.push(("GROK_API_KEY".to_string(), "".to_string()));
        env.push(("JUNIE_API_KEY".to_string(), "".to_string()));
        env.push(("XAI_API_KEY".to_string(), "".to_string()));
    }

    env
}

fn redact_launch_env_value(key: &str, value: &str) -> String {
    if key.ends_with("API_KEY") && !value.is_empty() && value != "local" {
        "<redacted>".to_string()
    } else {
        value.to_string()
    }
}

pub fn log_resolved_agent_launch(cfg: &Config, extra_env: &[(String, String)]) {
    let mut overrides = local_inference_overrides(cfg);
    let model_ov = model_selection_overrides(cfg);
    overrides.args.extend(model_ov.args);
    let auto_ov = auto_mode_overrides(cfg);
    overrides.args.extend(auto_ov.args);
    let env = merged_agent_env(cfg, extra_env);
    let args = if overrides.args.is_empty() {
        "(none)".to_string()
    } else {
        overrides.args.join(" ")
    };
    let env = if env.is_empty() {
        "(none)".to_string()
    } else {
        env.iter()
            .map(|(key, value)| format!("{key}={}", redact_launch_env_value(key, value)))
            .collect::<Vec<_>>()
            .join(", ")
    };

    log(&format!(
        "[dry-run] Agent launch overrides for {} -> args: {args}; env: {env}",
        cfg.agent
    ));
}

#[cfg(test)]
mod tests {
    use super::merged_agent_env;
    use crate::agent::grok_auth::GROK_AUTH_MANAGED_ENV;
    use cli_common::{Agent, Config, PricingConfig, TestCommands};
    use std::env;
    use std::sync::{Mutex, OnceLock};

    fn env_lock() -> &'static Mutex<()> {
        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
        LOCK.get_or_init(|| Mutex::new(()))
    }

    fn test_config(agent: Agent) -> Config {
        Config {
            agent,
            model: String::new(),
            auto_mode: false,
            dry_run: true,
            geodynamo_url: None,
            local_inference: Default::default(),
            root: env!("CARGO_MANIFEST_DIR").to_string(),
            project_name: "caretta-test".to_string(),
            scan_targets: Default::default(),
            skill_paths: Default::default(),
            bootstrap_agent_files: false,
            bootstrap_snapshot: false,
            workflow_preset: "default".to_string(),
            use_subscription: false,
            pricing: PricingConfig::default(),
            bot_settings: Default::default(),
            bot_credentials: None,
            test: TestCommands::default(),
            visual_regression: Default::default(),
            deploy: Default::default(),
            workspace: None,
            telemetry: cli_common::TelemetryConfig::default(),
        }
    }

    #[test]
    fn clears_xai_api_key_when_grok_oauth_is_managed() {
        let _guard = env_lock().lock().expect("env lock");
        unsafe {
            env::set_var(GROK_AUTH_MANAGED_ENV, "1");
            env::set_var("XAI_API_KEY", "should-clear");
        }

        let cfg = test_config(Agent::Xai);
        let env = merged_agent_env(&cfg, &[]);
        let xai = env
            .iter()
            .find(|(key, _)| key == "XAI_API_KEY")
            .map(|(_, value)| value.as_str());
        assert_eq!(xai, Some(""));

        unsafe {
            env::remove_var(GROK_AUTH_MANAGED_ENV);
            env::remove_var("XAI_API_KEY");
        }
    }
}