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 crate::{
8    cli::{Cli, Command},
9    credentials::OsCredentialStore,
10    diagnostics::RuntimeDiagnostics,
11    herdr::HerdrReporter,
12    model::{models_dev::cached_model_metadata, ModelError},
13    update,
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    cli_config::refresh_model_cache(&cli, &store).await?;
55    if cli_config::apply_overrides(&mut config, &cli)? {
56        config_repository.save(&config)?;
57    }
58    let role = if automation_prompt.is_some() {
59        AgentRole::AutomationRoot
60    } else {
61        AgentRole::InteractiveRoot
62    };
63    let bound_agent = AgentBinder::bind(
64        definition,
65        AgentInvocation {
66            role,
67            available_tools: host_capabilities(&cli, &config, role),
68        },
69        &config,
70    )?;
71    config = bound_agent.config().clone();
72
73    validate_terminal_mode(&cli)?;
74    if automation_prompt.is_some()
75        && config.provider == "anthropic"
76        && cached_model_metadata(&config.provider, &config.model).is_none()
77    {
78        let _ =
79            crate::model::models_dev::fetch_model_metadata(&config.provider, &config.model).await;
80    }
81    cli_config::normalize_reasoning(&mut config);
82    let herdr = HerdrReporter::from_env();
83    if let Some(prompt) = automation_prompt {
84        let diagnostics = RuntimeDiagnostics::new(&config);
85        diagnostics.update_agent(
86            bound_agent.id().as_str(),
87            &bound_agent.fingerprint().to_string(),
88        );
89        return automation::run(
90            prompt,
91            automation::Startup {
92                config: &config,
93                config_path: absolute_config_path(&config_repository)?,
94                cwd,
95                no_system_prompt: cli.no_system_prompt,
96                no_tools: cli.no_tools,
97                no_subagents: cli.no_subagents,
98                usage_purpose: "agent",
99                parent_session_id: None,
100                agent: bound_agent,
101                output_file,
102                diagnostics,
103                herdr,
104            },
105        )
106        .await;
107    }
108    let diagnostics = RuntimeDiagnostics::new(&config);
109    diagnostics.update_agent(
110        bound_agent.id().as_str(),
111        &bound_agent.fingerprint().to_string(),
112    );
113
114    let pending_update_notice = config
115        .check_for_updates
116        .then(|| tokio::spawn(update::update_notice(env!("CARGO_PKG_VERSION"))));
117
118    let sdk_options = SdkBootstrapOptions::from_config(&config, &cwd)?;
119    let credentials = crate::auth::provider_credentials::ApplicationCredentialSource::new(
120        Arc::new(OsCredentialStore),
121    );
122    let provider_result =
123        crate::providers::build_sdk_provider_with_source(sdk_options.provider, &credentials);
124    let (missing_auth_error, missing_auth_model_error) = match provider_result {
125        Ok(_) => (None, None),
126        Err(error) if is_interactive_startup_unavailable_error(&error) => {
127            (Some(error.to_string()), Some(error))
128        }
129        Err(error) => return Err(error.into()),
130    };
131    let result = interactive::run(interactive::Startup {
132        cli: &cli,
133        config,
134        config_path: absolute_config_path(&config_repository)?,
135        config_repository,
136        cwd,
137        missing_auth_error,
138        missing_auth_model_error,
139        pending_update_notice,
140        diagnostics,
141        herdr,
142        agent: bound_agent,
143    })
144    .await;
145    result
146}
147
148fn host_capabilities(
149    cli: &Cli,
150    config: &crate::config::Config,
151    role: AgentRole,
152) -> BTreeSet<String> {
153    if cli.no_tools {
154        return BTreeSet::new();
155    }
156    let mut tools = crate::agent::KNOWN_TOOLS
157        .iter()
158        .map(|tool| (*tool).to_string())
159        .collect::<BTreeSet<_>>();
160    if !crate::tools::web::access_tools(config).is_available() {
161        tools.remove("web_search");
162    }
163    #[cfg(windows)]
164    tools.remove("bash");
165    #[cfg(not(windows))]
166    tools.remove("powershell");
167    if cli.no_subagents || !config.enable_subagents {
168        tools.remove("agent");
169        tools.remove("agents");
170    }
171    if role != AgentRole::InteractiveRoot {
172        tools.remove("questionnaire");
173    }
174    tools
175}
176
177fn absolute_config_path(repository: &ConfigRepository) -> anyhow::Result<std::path::PathBuf> {
178    let path = repository.configured_path()?;
179    if path.is_absolute() {
180        Ok(path)
181    } else {
182        Ok(std::env::current_dir()?.join(path))
183    }
184}
185
186fn validate_terminal_mode(cli: &Cli) -> anyhow::Result<()> {
187    if cli.command.is_none() && (!io::stdin().is_terminal() || !io::stdout().is_terminal()) {
188        anyhow::bail!(
189            "rho's default mode is the interactive TUI; use `rho run` for non-interactive automation"
190        );
191    }
192    Ok(())
193}
194
195fn is_interactive_startup_unavailable_error(error: &ModelError) -> bool {
196    matches!(
197        error,
198        ModelError::MissingApiKey
199            | ModelError::MissingCodexAuth
200            | ModelError::MissingAnthropicApiKey
201            | ModelError::MissingGithubCopilotAuth
202            | ModelError::MissingXaiApiKey
203            | ModelError::MissingXaiAuth
204            | ModelError::Credentials(_)
205            | ModelError::UnsupportedProvider(_)
206    )
207}
208
209#[cfg(test)]
210#[path = "bootstrap_tests.rs"]
211mod tests;