Skip to main content

rho_coding_agent/app/
bootstrap.rs

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