-
Notifications
You must be signed in to change notification settings - Fork 1.5k
feat(reborn): frictionless local-dev onboarding (auto-provision serve, REPL model wizard, onboard launcher) #6285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| use std::io::{self, IsTerminal, Write}; | ||
| use std::path::{Path, PathBuf}; | ||
|
|
||
| use clap::Args; | ||
|
|
@@ -26,6 +27,27 @@ pub(crate) struct OnboardCommand { | |
| /// step explicit without touching v1 setup/import state. | ||
| #[arg(long = "import-history")] | ||
| import_history: bool, | ||
|
|
||
| /// Scaffold only — never prompt to launch REPL/Web UI, even on a TTY. | ||
| /// Use in scripts and CI. | ||
| #[arg(long = "no-launch")] | ||
| no_launch: bool, | ||
| } | ||
|
|
||
| /// Surface an operator can launch straight after onboarding. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| enum LaunchSurface { | ||
| Repl, | ||
| WebUi, | ||
| } | ||
|
|
||
| /// Map a prompt answer to a launch surface. `None` = skip / launch nothing. | ||
| fn parse_launch_choice(input: &str) -> Option<LaunchSurface> { | ||
| match input.trim().to_ascii_lowercase().as_str() { | ||
| "1" | "repl" | "r" => Some(LaunchSurface::Repl), | ||
| "2" | "webui" | "web" | "w" => Some(LaunchSurface::WebUi), | ||
| _ => None, | ||
| } | ||
| } | ||
|
|
||
| impl OnboardCommand { | ||
|
|
@@ -60,19 +82,82 @@ impl OnboardCommand { | |
| println!("- onboarding completion marker available"); | ||
| println!(); | ||
| println!("remaining:"); | ||
| println!("- configure LLM credentials through env vars referenced by config.toml"); | ||
| println!( | ||
| "- run `ironclaw-reborn models set-provider <provider> --model <model>` as needed" | ||
| ); | ||
| println!("- choose a model (in the REPL prompt or the Web UI setup screen below)"); | ||
| if self.import_history { | ||
| println!("- history import requested but not wired yet"); | ||
| } else { | ||
| println!("- history import not requested"); | ||
| } | ||
|
|
||
| // Offer to launch straight into a usable surface, where model setup | ||
| // happens (Web UI setup screen / REPL prompt). Skipped for --no-launch | ||
| // and non-interactive stdin so scripts/CI keep the scaffold-only path. | ||
| if !self.no_launch && io::stdin().is_terminal() { | ||
| if let Some(surface) = prompt_launch_surface()? { | ||
| return launch_surface(surface); | ||
| } | ||
| } else { | ||
| println!(); | ||
| println!("next: run `ironclaw-reborn repl` or `ironclaw-reborn serve` to finish setup"); | ||
| } | ||
|
Comment on lines
+95
to
+102
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When onboarding is run interactively on a TTY and the user chooses to skip the launch prompt (or enters an invalid choice), the command exits silently without printing the next steps. The instructions on how to manually start the REPL or Web UI are only printed in the Removing the if !self.no_launch && io::stdin().is_terminal() {
if let Some(surface) = prompt_launch_surface()? {
return launch_surface(surface);
}
}
println!();
println!("next: run `ironclaw-reborn repl` or `ironclaw-reborn serve` to finish setup"); |
||
| Ok(()) | ||
| } | ||
| } | ||
|
|
||
| /// Whether this binary was compiled with the Web UI (`serve`) subcommand. | ||
| const WEBUI_AVAILABLE: bool = cfg!(feature = "webui-v2-beta"); | ||
|
|
||
| fn prompt_launch_surface() -> anyhow::Result<Option<LaunchSurface>> { | ||
| if WEBUI_AVAILABLE { | ||
| print!("\nStart now? [1] REPL [2] Web UI [Enter] skip: "); | ||
| } else { | ||
| print!( | ||
| "\nStart now? [1] REPL [Enter] skip\n\ | ||
| (Web UI needs a build with `--features webui-v2-beta`): " | ||
| ); | ||
| } | ||
| io::stdout().flush()?; | ||
| let mut line = String::new(); | ||
| if io::stdin().read_line(&mut line)? == 0 { | ||
| return Ok(None); // EOF (piped input) | ||
| } | ||
| match parse_launch_choice(&line) { | ||
| Some(LaunchSurface::WebUi) if !WEBUI_AVAILABLE => { | ||
| println!( | ||
| "Web UI is not available in this build. Rebuild with \ | ||
| `cargo build --features webui-v2-beta`, or choose REPL." | ||
| ); | ||
| Ok(None) | ||
| } | ||
| other => Ok(other), | ||
| } | ||
| } | ||
|
|
||
| /// Re-exec this same binary into the chosen surface so onboarding hands off to a | ||
| /// live session (REPL prompt or Web UI, where the model is configured). | ||
| fn launch_surface(surface: LaunchSurface) -> anyhow::Result<()> { | ||
| let exe = std::env::current_exe()?; | ||
| let subcommand = match surface { | ||
| LaunchSurface::Repl => "repl", | ||
| LaunchSurface::WebUi => "serve", | ||
| }; | ||
| println!("launching `{subcommand}`…"); | ||
| #[cfg(unix)] | ||
| { | ||
| use std::os::unix::process::CommandExt; | ||
| // exec replaces this process; only returns on failure. | ||
| Err(std::process::Command::new(&exe) | ||
| .arg(subcommand) | ||
| .exec() | ||
| .into()) | ||
| } | ||
| #[cfg(not(unix))] | ||
| { | ||
| let status = std::process::Command::new(&exe).arg(subcommand).status()?; | ||
| std::process::exit(status.code().unwrap_or(1)); | ||
| } | ||
| } | ||
|
|
||
| pub(crate) fn onboarding_marker_path(home: &RebornHome) -> PathBuf { | ||
| home.path().join(ONBOARDING_MARKER_FILE) | ||
| } | ||
|
|
@@ -139,3 +224,21 @@ fn pending_steps(import_history: bool) -> Vec<&'static str> { | |
| } | ||
| steps | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod launch_choice_tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn parses_repl_webui_and_skip_answers() { | ||
| for a in ["1", "repl", "REPL", " r ", "\tRepl\n"] { | ||
| assert_eq!(parse_launch_choice(a), Some(LaunchSurface::Repl), "{a:?}"); | ||
| } | ||
| for a in ["2", "webui", "web", "W"] { | ||
| assert_eq!(parse_launch_choice(a), Some(LaunchSurface::WebUi), "{a:?}"); | ||
| } | ||
| for a in ["", "\n", "3", "quit", "xyz"] { | ||
| assert_eq!(parse_launch_choice(a), None, "{a:?}"); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,3 +1,5 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use std::io::{self, IsTerminal, Write}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use clap::Args; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use crate::context::RebornCliContext; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -9,11 +11,21 @@ pub(crate) struct ReplCommand { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Confirm trusted-laptop host filesystem access for local-dev-yolo. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[arg(long = "confirm-host-access")] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| confirm_host_access: bool, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Skip the first-run model-setup prompt shown when no LLM is configured. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[arg(long = "no-setup")] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| no_setup: bool, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| impl ReplCommand { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub(crate) fn execute(self, context: RebornCliContext) -> anyhow::Result<()> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| crate::runtime::init_tracing(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // First-run model setup: only when interactive and not opted out. Runs | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // before the runtime starts so the chosen provider/key are live for the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // session. Skipped for pipes/CI so scripted `repl` keeps working. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !self.no_setup && io::stdin().is_terminal() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maybe_run_model_setup(&context)?; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| crate::runtime::execute( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| context, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -23,3 +35,167 @@ impl ReplCommand { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Parse a provider-menu answer. `None` = skip; otherwise a 1-based index in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// range. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn parse_provider_choice(input: &str, count: usize) -> Option<usize> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let trimmed = input.trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if trimmed.is_empty() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return None; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trimmed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .parse::<usize>() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .ok() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter(|n| *n >= 1 && *n <= count) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[cfg(not(feature = "root-llm-provider"))] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn maybe_run_model_setup(_context: &RebornCliContext) -> anyhow::Result<()> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[cfg(feature = "root-llm-provider")] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn maybe_run_model_setup(context: &RebornCliContext) -> anyhow::Result<()> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use ironclaw_reborn_composition::RebornProviderAdmin; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // An env-driven selection (`LLM_BACKEND=...`) is already active — don't nag. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if std::env::var("LLM_BACKEND") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map(|value| !value.trim().is_empty()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .unwrap_or(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Ok(()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let admin = RebornProviderAdmin::new(context.boot_config().clone()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if let Some(selection) = admin.status()?.default { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // A provider is already selected. Prompt for its API key only when the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // provider actually *requires* one and the env var is missing — | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // otherwise we'd nag for providers that need no key (Ollama) or use a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // different credential (nearai / OAuth providers authenticate via a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // session token or login, not `*_API_KEY`). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if let Some(env_var) = selection.api_key_env.as_deref() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| && std::env::var_os(env_var).is_none() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| && provider_requires_api_key(&admin, selection.provider_id.as_deref()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prompt_and_set_api_key(env_var, selection.provider_id.as_deref()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Ok(()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let providers = admin.list(None, false)?.providers; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if providers.is_empty() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Ok(()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| println!("\nNo AI model configured yet. Choose a provider to get started"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| println!( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "(press Enter to skip and set one later with `ironclaw-reborn models set-provider`):\n" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (idx, provider) in providers.iter().enumerate() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| println!( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| " {}) {:<20} {}", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| idx + 1, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| provider.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| provider.description | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Re-prompt on an invalid entry; only an empty line skips and EOF exits. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let index = loop { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| print!("\nProvider [1-{} / Enter to skip]: ", providers.len()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| io::stdout().flush()?; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut answer = String::new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if io::stdin().read_line(&mut answer)? == 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Ok(()); // EOF | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if answer.trim().is_empty() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Ok(()); // skip | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if let Some(index) = parse_provider_choice(&answer, providers.len()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break index; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| println!("please enter a number between 1 and {}.", providers.len()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let provider = &providers[index - 1]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| print!("Model [Enter for default `{}`]: ", provider.default_model); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| io::stdout().flush()?; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut model = String::new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| io::stdin().read_line(&mut model)?; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let model = model.trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let model_arg = (!model.is_empty()).then_some(model); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let outcome = admin.set_provider(&provider.id, model_arg)?; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| println!( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "configured: provider `{}`, model `{}`", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| outcome.provider_id, outcome.model | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if outcome.missing_api_key | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| && let Some(env_var) = outcome.api_key_env.as_deref() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prompt_and_set_api_key(env_var, Some(&outcome.provider_id)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| println!(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Whether the given provider genuinely requires an API key (vs. Ollama, or a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// session-token / OAuth provider like nearai). Looks up the provider's | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// metadata; defaults to `false` so an unknown/unreadable provider never nags. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[cfg(feature = "root-llm-provider")] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn provider_requires_api_key( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| admin: &ironclaw_reborn_composition::RebornProviderAdmin, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| provider_id: Option<&str>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let Some(id) = provider_id else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| admin | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .list(Some(id), true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .ok() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .and_then(|list| list.providers.into_iter().next()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .and_then(|provider| provider.metadata) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map(|metadata| metadata.api_key_required) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .unwrap_or(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+146
to
+160
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
As per coding guidelines: "justified fallbacks must include an inline ♻️ Add the marker let Some(id) = provider_id else {
return false;
};
+ // silent-ok: provider-metadata lookup — an unreadable/unknown provider just
+ // means "don't nag for a key", not a lost authoritative read.
admin
.list(Some(id), true)
.ok()📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Prompt for an API key and set it in the process env for this session. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[cfg(feature = "root-llm-provider")] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn prompt_and_set_api_key(env_var: &str, provider: Option<&str>) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let label = provider.unwrap_or("the selected provider"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| print!( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "\n{label} needs an API key. Enter value for `{env_var}` (used this session; Enter to skip): " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if io::stdout().flush().is_err() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut key = String::new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if io::stdin().read_line(&mut key).is_err() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let key = key.trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if key.is_empty() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| println!("no key entered — set `{env_var}` in your environment before chatting."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // SAFETY: single-threaded CLI startup, before the runtime spawns any threads | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // that read process environment variables. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| unsafe { std::env::set_var(env_var, key) }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| println!("key set for this session. To persist it, add `export {env_var}=...` to your shell."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[cfg(test)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mod tests { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use super::*; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn provider_choice_parses_index_and_skip() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(parse_provider_choice("1", 3), Some(1)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(parse_provider_choice(" 3 \n", 3), Some(3)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(parse_provider_choice("", 3), None); // Enter = skip | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(parse_provider_choice("\n", 3), None); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(parse_provider_choice("0", 3), None); // out of range | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(parse_provider_choice("4", 3), None); // out of range | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(parse_provider_choice("openai", 3), None); // non-numeric | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: nearai/ironclaw
Length of output: 238
🏁 Script executed:
Repository: nearai/ironclaw
Length of output: 7040
🏁 Script executed:
Repository: nearai/ironclaw
Length of output: 234
🏁 Script executed:
Repository: nearai/ironclaw
Length of output: 50373
🏁 Script executed:
Repository: nearai/ironclaw
Length of output: 50371
🏁 Script executed:
Repository: nearai/ironclaw
Length of output: 4596
Remove
webui-v2-betafromdefaultcrates/ironclaw_reborn_cli/AGENTS.md:35-58says this feature is off by default and keepsserveout of default builds;crates/ironclaw_reborn_cli/Cargo.toml:22flips it on, pulling the axum/auth/HTTP/SSE/WS stack into every default binary. Drop it fromdefaultunless that crate contract changes in the same PR.🤖 Prompt for AI Agents
Source: Coding guidelines