use crate::{
config::{
app::{ProviderId, StoredConfig, validate_frontend_origin},
store::FileConfigStore,
},
domain::errors::AgentResult,
providers::probe::ProviderProbeResult,
setup::{
probes::{PiProbe, PiProbeResult, ProvidersProbe, SystemProbe, VerifiedToolCommands},
readiness::{CheckId, CheckResult, CheckStatus, ReadinessReport, provider_setup_guidance},
},
};
pub(crate) const THIRD_PARTY_CODE_WARNING: &str = "Warning: selected provider extensions execute third-party code inside Pi; setup may install and switch to the reviewed managed Node/Pi toolchain first.";
pub(crate) trait SetupConfigStore: Send + Sync {
fn load(&self) -> AgentResult<StoredConfig>;
fn save(&self, config: &StoredConfig) -> AgentResult<()>;
}
impl SetupConfigStore for FileConfigStore {
fn load(&self) -> AgentResult<StoredConfig> {
FileConfigStore::load(self)
}
fn save(&self, config: &StoredConfig) -> AgentResult<()> {
FileConfigStore::save(self, config)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SetupConsentKind {
ManagedDependencies,
ThirdPartyExtensions,
}
pub(crate) trait SetupConsent: Send + Sync {
fn confirm(&self, kind: SetupConsentKind, warning: &'static str) -> AgentResult<bool>;
fn notify(&self, kind: SetupConsentKind, warning: &'static str) -> AgentResult<()>;
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct SetupOptions {
pub(crate) interactive: bool,
pub(crate) assume_yes: bool,
pub(crate) selected_providers: Option<Vec<ProviderId>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DelegatedReadiness {
pub(crate) pairing: CheckResult,
pub(crate) connectivity: CheckResult,
}
impl DelegatedReadiness {
pub(crate) fn foundation(legacy_ready: bool) -> Self {
Self {
pairing: CheckResult {
id: CheckId::Pairing,
status: if legacy_ready {
CheckStatus::Ready
} else {
CheckStatus::ActionRequired
},
required: true,
detail: None,
action: (!legacy_ready).then(|| "complete browser pairing setup".to_owned()),
actions: Vec::new(),
},
connectivity: CheckResult {
id: CheckId::Tunnel,
status: CheckStatus::Ready,
required: false,
detail: None,
action: None,
actions: Vec::new(),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SetupStageOutcome {
pub(crate) status: CheckStatus,
pub(crate) warnings: Vec<String>,
pub(crate) action: Option<String>,
}
impl SetupStageOutcome {
fn from_check(check: CheckResult) -> Self {
Self {
status: check.status,
warnings: Vec::new(),
action: check.action,
}
}
}
pub(crate) struct SetupFlow<'a> {
store: &'a dyn SetupConfigStore,
system: &'a dyn SystemProbe,
pi: &'a dyn PiProbe,
providers: &'a dyn ProvidersProbe,
consent: &'a dyn SetupConsent,
options: SetupOptions,
delegated: DelegatedReadiness,
}
impl<'a> SetupFlow<'a> {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
store: &'a dyn SetupConfigStore,
system: &'a dyn SystemProbe,
pi: &'a dyn PiProbe,
providers: &'a dyn ProvidersProbe,
consent: &'a dyn SetupConsent,
options: SetupOptions,
delegated: DelegatedReadiness,
) -> Self {
Self {
store,
system,
pi,
providers,
consent,
options,
delegated,
}
}
pub(crate) async fn check(&self) -> AgentResult<ReadinessReport> {
let mut config = self.store.load()?;
let system = self.system.check().await?;
if system.check.status != CheckStatus::Ready {
return Ok(ReadinessReport::from_checks(vec![
system.check,
prerequisite_check(CheckId::Pi, "Complete System setup before configuring Pi."),
self.delegated.pairing.clone(),
self.connectivity_check(&config),
]));
}
let pi = self.pi.check(&config.tools).await?;
self.persist_verified_commands(&mut config, &pi)?;
Ok(ReadinessReport::from_checks(vec![
system.check,
pi.check,
self.delegated.pairing.clone(),
self.connectivity_check(&config),
]))
}
pub(crate) async fn run_pi_stage(&self) -> AgentResult<SetupStageOutcome> {
let mut config = self.store.load()?;
let system = self.system.check().await?;
if system.check.status == CheckStatus::ActionRequired {
return Ok(SetupStageOutcome::from_check(system.check));
}
let current = self.pi.check(&config.tools).await?;
if current.check.status == CheckStatus::Ready {
self.persist_verified_commands(&mut config, ¤t)?;
return Ok(SetupStageOutcome::from_check(current.check));
}
if !system.managed_install_supported {
return Ok(SetupStageOutcome::from_check(current.check));
}
let allowed = if self.options.assume_yes {
self.consent.notify(
SetupConsentKind::ManagedDependencies,
"Install reviewed pinned Node and Pi dependencies?",
)?;
true
} else if self.options.interactive {
self.consent.confirm(
SetupConsentKind::ManagedDependencies,
"Install reviewed pinned Node and Pi dependencies?",
)?
} else {
false
};
if !allowed {
return Ok(SetupStageOutcome::from_check(current.check));
}
let installed = self.pi.install().await?;
self.persist_verified_commands(&mut config, &installed)?;
Ok(SetupStageOutcome::from_check(installed.check))
}
#[cfg(test)]
pub(crate) async fn run_models_stage(&self) -> AgentResult<SetupStageOutcome> {
let config = self.store.load()?;
let selected = self
.options
.selected_providers
.clone()
.unwrap_or_else(|| config.selected_providers.clone());
self.run_models_stage_with(selected).await
}
pub(crate) fn run_connectivity_stage(&self) -> AgentResult<SetupStageOutcome> {
let config = self.store.load()?;
let check = self.connectivity_check(&config);
Ok(SetupStageOutcome {
status: check.status,
warnings: Vec::new(),
action: check.action,
})
}
fn connectivity_check(&self, config: &StoredConfig) -> CheckResult {
if config
.frontend_origin
.as_deref()
.is_none_or(|origin| validate_frontend_origin(origin).is_err())
{
return CheckResult {
id: CheckId::Tunnel,
status: CheckStatus::ActionRequired,
required: true,
detail: None,
action: Some("frontend_origin must be an exact HTTPS origin.".to_owned()),
actions: Vec::new(),
};
}
CheckResult {
id: CheckId::Tunnel,
status: CheckStatus::Ready,
required: true,
detail: None,
action: None,
actions: Vec::new(),
}
}
pub(crate) async fn run_models_stage_with(
&self,
selected: Vec<ProviderId>,
) -> AgentResult<SetupStageOutcome> {
let mut config = self.store.load()?;
if selected.is_empty() {
return Ok(action_required("Select at least one model provider."));
}
let system = self.system.check().await?;
if system.check.status != CheckStatus::Ready {
return Ok(action_required(
"Complete System setup before configuring model providers.",
));
}
let pi = self.pi.check(&config.tools).await?;
if pi.check.status != CheckStatus::Ready {
return Ok(action_required(
"Complete Pi setup before configuring model providers.",
));
}
let mut results = self.providers.check(&selected).await?;
let extensions = results
.iter()
.filter(|result| {
!result.extension_ready
&& matches!(result.id, ProviderId::ClaudeCode | ProviderId::Antigravity)
})
.map(|result| result.id)
.collect::<Vec<_>>();
if !extensions.is_empty() {
let allowed = if self.options.assume_yes {
self.consent.notify(
SetupConsentKind::ThirdPartyExtensions,
THIRD_PARTY_CODE_WARNING,
)?;
true
} else if self.options.interactive {
self.consent.confirm(
SetupConsentKind::ThirdPartyExtensions,
THIRD_PARTY_CODE_WARNING,
)?
} else {
false
};
if allowed {
self.providers.install_extensions(&extensions).await?;
results = self.providers.check(&selected).await?;
}
}
if self.options.interactive {
for result in results.iter_mut().filter(|result| {
result.executable_ready && result.extension_ready && !result.authenticated
}) {
*result = self.providers.login(result.id).await?;
}
}
let ready = results.iter().any(ProviderProbeResult::ready);
let warnings = provider_warnings(&results);
if ready {
if config.selected_providers != selected {
config.selected_providers = selected;
self.store.save(&config)?;
}
return Ok(SetupStageOutcome {
status: CheckStatus::Ready,
warnings,
action: None,
});
}
Ok(SetupStageOutcome {
status: CheckStatus::ActionRequired,
warnings,
action: results.first().map(non_interactive_provider_action),
})
}
fn persist_verified_commands(
&self,
config: &mut StoredConfig,
result: &PiProbeResult,
) -> AgentResult<()> {
let Some(commands) = &result.commands else {
return Ok(());
};
let changed = apply_commands(config, commands);
if changed {
self.store.save(config)?;
}
Ok(())
}
}
fn apply_commands(config: &mut StoredConfig, commands: &VerifiedToolCommands) -> bool {
let changed = config.tools.node_command.as_ref() != Some(&commands.node)
|| config.tools.pi_command.as_ref() != Some(&commands.pi)
|| config.tools.claude_command != commands.claude;
if changed {
config.tools.node_command = Some(commands.node.clone());
config.tools.pi_command = Some(commands.pi.clone());
config.tools.claude_command = commands.claude.clone();
}
changed
}
fn provider_warnings(results: &[ProviderProbeResult]) -> Vec<String> {
results
.iter()
.filter(|result| !result.ready())
.map(|result| {
let guidance = provider_setup_guidance(result);
format!("{}: {}", guidance.display_name, guidance.summary)
})
.collect()
}
fn non_interactive_provider_action(result: &ProviderProbeResult) -> String {
provider_setup_guidance(result).summary
}
fn action_required(action: impl Into<String>) -> SetupStageOutcome {
SetupStageOutcome {
status: CheckStatus::ActionRequired,
warnings: Vec::new(),
action: Some(action.into()),
}
}
fn prerequisite_check(id: CheckId, action: &'static str) -> CheckResult {
CheckResult {
id,
status: CheckStatus::ActionRequired,
required: true,
detail: None,
action: Some(action.to_owned()),
actions: Vec::new(),
}
}