Skip to main content

rho_coding_agent/app/
bootstrap.rs

1use std::{
2    collections::BTreeSet,
3    io::{self, IsTerminal},
4    sync::Arc,
5};
6
7use {
8    crate::cli::{Cli, Command},
9    crate::diagnostics::RuntimeDiagnostics,
10    crate::herdr::HerdrReporter,
11    crate::update,
12    rho_providers::credentials::OsCredentialStore,
13    rho_providers::model::ModelError,
14};
15
16use super::{
17    agent_binding::{AgentBinder, AgentInvocation, AgentRole},
18    automation, cli_config,
19    config_repository::ConfigRepository,
20    interactive, login,
21    sdk_config::SdkBootstrapOptions,
22};
23
24pub async fn run(cli: Cli) -> anyhow::Result<()> {
25    cli_config::validate(&cli)?;
26    if let Some(Command::Attach { id }) = &cli.command {
27        return crate::tui::run_attachment(id, HerdrReporter::from_env()).await;
28    }
29    if matches!(cli.command, Some(Command::Update)) {
30        return update::run_update(env!("CARGO_PKG_VERSION")).await;
31    }
32    if let Some(Command::Login {
33        provider,
34        device_auth,
35    }) = &cli.command
36    {
37        return login::run(provider, *device_auth).await;
38    }
39
40    let config_path = cli.config.clone();
41    let config_repository = ConfigRepository::new(config_path.clone());
42    let mut config = config_repository.load()?;
43    let cwd = std::env::current_dir()?;
44    let automation_prompt = automation::prompt_for_command(&cli.command)?;
45    let output_file = match &cli.command {
46        Some(Command::Run { output_file, .. }) => output_file.clone(),
47        _ => None,
48    };
49    let catalog = crate::agent::AgentCatalog::discover(&cwd)?;
50    let selected_agent = cli.agent.as_deref().unwrap_or("default");
51    let definition = Arc::new(catalog.find(selected_agent)?.definition.clone());
52
53    let store = OsCredentialStore;
54    let provider_refresh = cli_config::refresh_model_cache(&cli, &config, &store).await?;
55    let mut save_config = cli_config::apply_overrides(&mut config, &cli)?;
56    cli_config::prepare_model_metadata(&config, &store, &provider_refresh).await;
57    save_config |= cli_config::normalize_reasoning_for_cli(
58        &mut config,
59        if cli.reasoning.is_some() {
60            rho_providers::model::ReasoningRequestSource::Explicit
61        } else {
62            rho_providers::model::ReasoningRequestSource::PersistedOrDefault
63        },
64    )?;
65    if save_config {
66        config_repository.save(&config)?;
67    }
68    let reasoning_before_binding = config.reasoning;
69    let role = if automation_prompt.is_some() {
70        AgentRole::AutomationRoot
71    } else {
72        AgentRole::InteractiveRoot
73    };
74    let bound_agent = AgentBinder::bind(
75        definition,
76        AgentInvocation {
77            role,
78            available_tools: host_capabilities(&cli, &config, role),
79        },
80        &config,
81    )?;
82    config = bound_agent.config().clone();
83
84    validate_terminal_mode(&cli)?;
85    cli_config::prepare_model_metadata(&config, &store, &provider_refresh).await;
86    let bound_reasoning_source =
87        if cli.reasoning.is_some() && config.reasoning == reasoning_before_binding {
88            rho_providers::model::ReasoningRequestSource::Explicit
89        } else {
90            rho_providers::model::ReasoningRequestSource::PersistedOrDefault
91        };
92    cli_config::normalize_reasoning_for_cli(&mut config, bound_reasoning_source)?;
93    let herdr = HerdrReporter::from_env();
94    if let Some(prompt) = automation_prompt {
95        let diagnostics = RuntimeDiagnostics::new(&config);
96        diagnostics.update_agent(
97            bound_agent.id().as_str(),
98            &bound_agent.fingerprint().to_string(),
99        );
100        return automation::run(
101            prompt,
102            automation::Startup {
103                config: &config,
104                config_path: absolute_config_path(&config_repository)?,
105                cwd,
106                no_system_prompt: cli.no_system_prompt,
107                no_tools: cli.no_tools,
108                no_subagents: cli.no_subagents,
109                usage_purpose: "agent",
110                parent_session_id: None,
111                agent: bound_agent,
112                output_file,
113                diagnostics,
114                herdr,
115            },
116        )
117        .await;
118    }
119    let diagnostics = RuntimeDiagnostics::new(&config);
120    diagnostics.update_agent(
121        bound_agent.id().as_str(),
122        &bound_agent.fingerprint().to_string(),
123    );
124
125    let pending_update_notice = config
126        .check_for_updates
127        .then(|| tokio::spawn(update::update_notice(env!("CARGO_PKG_VERSION"))));
128
129    let sdk_options = SdkBootstrapOptions::from_config(&config, &cwd)?;
130    let credentials = rho_providers::auth::provider_credentials::ApplicationCredentialSource::new(
131        Arc::new(OsCredentialStore),
132    );
133    let provider_result = rho_providers::providers::build_sdk_provider_with_source(
134        sdk_options.provider,
135        &credentials,
136    );
137    let (missing_auth_error, missing_auth_model_error) = match provider_result {
138        Ok(_) => (None, None),
139        Err(error) if is_interactive_startup_unavailable_error(&error) => {
140            (Some(error.to_string()), Some(error))
141        }
142        Err(error) => return Err(error.into()),
143    };
144    let result = interactive::run(interactive::Startup {
145        cli: &cli,
146        config,
147        config_path: absolute_config_path(&config_repository)?,
148        config_repository,
149        cwd,
150        missing_auth_error,
151        missing_auth_model_error,
152        pending_update_notice,
153        diagnostics,
154        herdr,
155        agent: bound_agent,
156        reasoning_source: bound_reasoning_source,
157    })
158    .await;
159    result
160}
161
162fn host_capabilities(
163    cli: &Cli,
164    config: &crate::config::Config,
165    role: AgentRole,
166) -> BTreeSet<String> {
167    if cli.no_tools {
168        return BTreeSet::new();
169    }
170    let mut tools = crate::agent::KNOWN_TOOLS
171        .iter()
172        .map(|tool| (*tool).to_string())
173        .collect::<BTreeSet<_>>();
174    if !crate::tools::web::access_tools(config).is_available() {
175        tools.remove("web_search");
176    }
177    #[cfg(windows)]
178    tools.remove("bash");
179    #[cfg(not(windows))]
180    tools.remove("powershell");
181    if cli.no_subagents || !config.enable_subagents {
182        tools.remove("agent");
183        tools.remove("agents");
184    }
185    if role != AgentRole::InteractiveRoot {
186        tools.remove("questionnaire");
187    }
188    tools
189}
190
191fn absolute_config_path(repository: &ConfigRepository) -> anyhow::Result<std::path::PathBuf> {
192    let path = repository.configured_path()?;
193    if path.is_absolute() {
194        Ok(path)
195    } else {
196        Ok(std::env::current_dir()?.join(path))
197    }
198}
199
200fn validate_terminal_mode(cli: &Cli) -> anyhow::Result<()> {
201    if cli.command.is_none() && (!io::stdin().is_terminal() || !io::stdout().is_terminal()) {
202        anyhow::bail!(
203            "rho's default mode is the interactive TUI; use `rho run` for non-interactive automation"
204        );
205    }
206    Ok(())
207}
208
209fn is_interactive_startup_unavailable_error(error: &ModelError) -> bool {
210    matches!(
211        error,
212        ModelError::MissingApiKey
213            | ModelError::MissingCodexAuth
214            | ModelError::MissingAnthropicApiKey
215            | ModelError::MissingGithubCopilotAuth
216            | ModelError::MissingXaiApiKey
217            | ModelError::MissingXaiAuth
218            | ModelError::Credentials(_)
219            | ModelError::UnsupportedProvider(_)
220    )
221}
222
223#[cfg(test)]
224#[path = "bootstrap_tests.rs"]
225mod tests;