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;