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