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