1use std::{
2 io::{self, IsTerminal},
3 sync::Arc,
4};
5
6use {
7 crate::cli::{Cli, Command, CredentialStoreCommand, OutputFormat},
8 crate::credential_store::AppCredentialStore,
9 crate::diagnostics::RuntimeDiagnostics,
10 crate::herdr::HerdrReporter,
11 crate::update,
12 rho_providers::model::ModelError,
13};
14
15use super::{
16 agent_binding::{AgentBinder, AgentInvocation, AgentRole},
17 automation, automation_protocol, cli_config,
18 config_repository::ConfigRepository,
19 interactive, login,
20 sdk_config::SdkBootstrapOptions,
21};
22
23pub async fn run(cli: Cli) -> anyhow::Result<()> {
24 let run_output = match &cli.command {
25 Some(Command::Run { output, .. }) => Some(*output),
26 _ => None,
27 };
28 let result = run_inner(cli).await;
29 let Err(error) = result else {
30 return Ok(());
31 };
32 if error.downcast_ref::<automation::AutomationExit>().is_some()
33 || error
34 .downcast_ref::<automation::AutomationInterrupted>()
35 .is_some()
36 {
37 return Err(error);
38 }
39 if run_output == Some(OutputFormat::Jsonl) {
40 automation::emit_startup_failure()?;
41 return Err(automation::AutomationExit::new(
42 2,
43 automation_protocol::TerminalReason::ConfigurationError,
44 "configuration failed",
45 )
46 .into());
47 }
48 if run_output.is_some() {
49 return Err(automation::AutomationExit::new(
50 2,
51 automation_protocol::TerminalReason::ConfigurationError,
52 error.to_string(),
53 )
54 .into());
55 }
56 Err(error)
57}
58
59async fn run_inner(cli: Cli) -> anyhow::Result<()> {
60 cli_config::validate(&cli)?;
61 if let Some(Command::CredentialStore { command }) = &cli.command {
62 return run_credential_store_command(command);
63 }
64 if let Some(Command::Attach { id }) = &cli.command {
65 return crate::tui::run_attachment(id, HerdrReporter::from_env()).await;
66 }
67 if matches!(cli.command, Some(Command::Update)) {
68 return update::run_update(env!("CARGO_PKG_VERSION")).await;
69 }
70 if let Some(Command::Login {
71 provider,
72 device_auth,
73 }) = &cli.command
74 {
75 crate::credential_store::initialize()?;
76 return login::run(provider, *device_auth).await;
77 }
78
79 let config_path = cli.config.clone();
80 let config_repository = ConfigRepository::new(config_path.clone());
81 let mut config = config_repository.load()?;
82 let cwd = std::env::current_dir()?;
83 let automation_prompt = automation::prompt_for_command(&cli.command)?;
84 let (output_file, output, max_steps, timeout) = match &cli.command {
85 Some(Command::Run {
86 output_file,
87 output,
88 max_steps,
89 timeout,
90 ..
91 }) => (output_file.clone(), *output, *max_steps, *timeout),
92 _ => (None, OutputFormat::Text, None, None),
93 };
94 let catalog = crate::agent::AgentCatalog::discover(&cwd)?;
95 let selected_agent = cli.agent.as_deref().unwrap_or("default");
96 let definition = Arc::new(catalog.find(selected_agent)?.definition.clone());
97
98 let store = AppCredentialStore;
99 let provider_refresh = cli_config::refresh_model_cache(&cli, &config, &store).await?;
100 let mut save_config = cli_config::apply_overrides(&mut config, &cli)?;
101 cli_config::prepare_model_metadata(&config, &store, &provider_refresh).await;
102 save_config |= cli_config::normalize_reasoning_for_cli(
103 &mut config,
104 if cli.reasoning.is_some() {
105 rho_providers::model::ReasoningRequestSource::Explicit
106 } else {
107 rho_providers::model::ReasoningRequestSource::PersistedOrDefault
108 },
109 )?;
110 if save_config {
111 config_repository.save(&config)?;
112 }
113 let reasoning_before_binding = config.reasoning;
114 let role = if automation_prompt.is_some() {
115 AgentRole::AutomationRoot
116 } else {
117 AgentRole::InteractiveRoot
118 };
119 let bound_agent = AgentBinder::bind(
120 definition,
121 AgentInvocation {
122 role,
123 available_tools: host_capabilities(&cli, &config, role),
124 },
125 &config,
126 )?;
127 config = bound_agent.config().clone();
128
129 validate_terminal_mode(&cli)?;
130 cli_config::prepare_model_metadata(&config, &store, &provider_refresh).await;
131 let bound_reasoning_source =
132 if cli.reasoning.is_some() && config.reasoning == reasoning_before_binding {
133 rho_providers::model::ReasoningRequestSource::Explicit
134 } else {
135 rho_providers::model::ReasoningRequestSource::PersistedOrDefault
136 };
137 cli_config::normalize_reasoning_for_cli(&mut config, bound_reasoning_source)?;
138 let herdr = HerdrReporter::from_env();
139 if let Some(prompt) = automation_prompt {
140 let diagnostics = RuntimeDiagnostics::new(&config);
141 diagnostics.update_agent(
142 bound_agent.id().as_str(),
143 &bound_agent.fingerprint().to_string(),
144 );
145 return automation::run(
146 prompt,
147 automation::Startup {
148 config: &config,
149 config_path: absolute_config_path(&config_repository)?,
150 cwd,
151 no_system_prompt: cli.no_system_prompt,
152 no_tools: cli.no_tools,
153 no_subagents: cli.no_subagents,
154 usage_purpose: "agent",
155 parent_session_id: None,
156 agent: bound_agent,
157 output_file,
158 output,
159 max_steps,
160 timeout,
161 diagnostics,
162 herdr,
163 },
164 )
165 .await;
166 }
167 let diagnostics = RuntimeDiagnostics::new(&config);
168 diagnostics.update_agent(
169 bound_agent.id().as_str(),
170 &bound_agent.fingerprint().to_string(),
171 );
172
173 let pending_update_notice = config
174 .check_for_updates
175 .then(|| tokio::spawn(update::update_notice(env!("CARGO_PKG_VERSION"))));
176
177 let sdk_options = SdkBootstrapOptions::from_config(&config, &cwd)?;
178 let credentials = rho_providers::auth::provider_credentials::ApplicationCredentialSource::new(
179 Arc::new(AppCredentialStore),
180 );
181 let provider_result = rho_providers::providers::build_sdk_provider_with_source(
182 sdk_options.provider,
183 &credentials,
184 );
185 let (missing_auth_error, missing_auth_model_error) = match provider_result {
186 Ok(_) => (None, None),
187 Err(error) if is_interactive_startup_unavailable_error(&error) => {
188 (Some(error.to_string()), Some(error))
189 }
190 Err(error) => return Err(error.into()),
191 };
192 let result = interactive::run(interactive::Startup {
193 cli: &cli,
194 config,
195 config_path: absolute_config_path(&config_repository)?,
196 config_repository,
197 cwd,
198 missing_auth_error,
199 missing_auth_model_error,
200 pending_update_notice,
201 diagnostics,
202 herdr,
203 agent: bound_agent,
204 reasoning_source: bound_reasoning_source,
205 })
206 .await;
207 result
208}
209
210fn run_credential_store_command(command: &CredentialStoreCommand) -> anyhow::Result<()> {
211 match command {
212 CredentialStoreCommand::Probe { backend } => {
213 let result = crate::credential_store::probe(*backend);
214 if result.available {
215 println!("available: {}", result.detail);
216 Ok(())
217 } else {
218 anyhow::bail!(result.detail)
219 }
220 }
221 CredentialStoreCommand::Status => {
222 match crate::credential_store::saved_policy_backend()? {
224 None => println!("unset"),
225 Some(backend) => println!("{}", backend.as_str()),
226 }
227 Ok(())
228 }
229 CredentialStoreCommand::Set { backend } => {
230 let path = crate::credential_store::set_backend(*backend)?;
231 println!(
232 "credential store set to {} in {}",
233 backend.as_str(),
234 path.display()
235 );
236 Ok(())
237 }
238 }
239}
240
241fn host_capabilities(
242 cli: &Cli,
243 config: &crate::config::Config,
244 role: AgentRole,
245) -> crate::agent::AgentCapabilities {
246 use crate::agent::ToolCapability;
247
248 if cli.no_tools {
249 return crate::agent::AgentCapabilities::default();
250 }
251 let mut tools = crate::agent::AgentCapabilities::all_host_tools();
252 if !crate::tools::web::access_tools(config).is_available() {
253 tools.remove(&ToolCapability::WebSearch);
254 }
255 #[cfg(windows)]
256 tools.remove(&ToolCapability::Bash);
257 #[cfg(not(windows))]
258 tools.remove(&ToolCapability::Powershell);
259 if cli.no_subagents || !config.enable_subagents {
260 tools.remove(&ToolCapability::Agent);
261 tools.remove(&ToolCapability::Agents);
262 }
263 if role != AgentRole::InteractiveRoot {
264 tools.remove(&ToolCapability::Questionnaire);
265 }
266 #[cfg(debug_assertions)]
267 if std::env::var_os("RHO_TUI_TEST_MODE").as_deref() == Some(std::ffi::OsStr::new("matrix")) {
268 tools.insert(ToolCapability::Extension(
269 crate::tools::tui_fixture::NAME.into(),
270 ));
271 }
272 tools
273}
274
275fn absolute_config_path(repository: &ConfigRepository) -> anyhow::Result<std::path::PathBuf> {
276 let path = repository.configured_path()?;
277 if path.is_absolute() {
278 Ok(path)
279 } else {
280 Ok(std::env::current_dir()?.join(path))
281 }
282}
283
284fn validate_terminal_mode(cli: &Cli) -> anyhow::Result<()> {
285 if cli.command.is_none() && (!io::stdin().is_terminal() || !io::stdout().is_terminal()) {
286 anyhow::bail!(
287 "rho's default mode is the interactive TUI; use `rho run` for non-interactive automation"
288 );
289 }
290 Ok(())
291}
292
293fn is_interactive_startup_unavailable_error(error: &ModelError) -> bool {
294 matches!(
295 error,
296 ModelError::MissingApiKey
297 | ModelError::MissingCodexAuth
298 | ModelError::MissingAnthropicApiKey
299 | ModelError::MissingGithubCopilotAuth
300 | ModelError::MissingXaiApiKey
301 | ModelError::MissingXaiAuth
302 | ModelError::Credentials(_)
303 | ModelError::UnsupportedProvider(_)
304 )
305}
306
307#[cfg(test)]
308#[path = "bootstrap_tests.rs"]
309mod tests;