use anyhow::{Context, Result, bail};
use systemprompt_config::{ProfileBootstrap, SecretsBootstrap};
use systemprompt_identifiers::JobName;
use super::routing_decision::{RoutingDecision, decide_routing};
use super::{args, bootstrap};
use crate::cli_settings::CliConfig;
use crate::commands::{admin, infrastructure};
use crate::descriptor::{CommandDescriptor, RoutingClass};
use crate::env_overrides::EnvOverrides;
use crate::interactive;
use crate::shared::ProfileSource;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BootstrapOutcome {
RemoteExecuted,
ContinueLocal,
ExternalDbUrl(String),
}
pub(super) async fn bootstrap_profile(
cli: &args::Cli,
desc: &CommandDescriptor,
cli_config: &CliConfig,
env: &EnvOverrides,
) -> Result<BootstrapOutcome> {
let has_export = args::has_local_export_flag(cli.command.as_ref());
let ctx = bootstrap::resolve_and_display_profile(cli_config, env, has_export)?;
require_explicit_cloud_profile(ProfileBootstrap::get()?, ctx.source, desc)?;
if enforce_routing_policy(&ctx, cli, desc, cli_config).await?
== BootstrapOutcome::RemoteExecuted
{
return Ok(BootstrapOutcome::RemoteExecuted);
}
let needs_cloud = is_cloud_bypass_command(cli.command.as_ref());
initialize_post_routing(&ctx, desc, needs_cloud).await
}
async fn enforce_routing_policy(
ctx: &bootstrap::ProfileContext,
cli: &args::Cli,
desc: &CommandDescriptor,
cli_config: &CliConfig,
) -> Result<BootstrapOutcome> {
let class = desc.routing_class();
if !ctx.env.is_deployment_host && class != RoutingClass::LocalOnly && !ctx.has_export {
let profile = ProfileBootstrap::get()?;
return try_remote_routing(cli, profile, cli_config, desc).await;
}
if ctx.has_export && ctx.is_cloud && !ctx.external_db_access {
bail!(
"Export with cloud profile '{}' requires external database access.\nEnable \
external_db_access in the profile or use a local profile.",
ctx.profile.name
);
}
if ctx.is_cloud
&& !ctx.env.is_deployment_host
&& !ctx.external_db_access
&& !is_cloud_bypass_command(cli.command.as_ref())
{
bail!(
"Cloud profile '{}' selected but this command doesn't support remote execution.\nUse \
a local profile with --profile <name> or enable external database access.",
ctx.profile.name
);
}
Ok(BootstrapOutcome::ContinueLocal)
}
pub fn require_explicit_cloud_profile(
profile: &systemprompt_manifest::Profile,
source: ProfileSource,
desc: &CommandDescriptor,
) -> Result<()> {
if !desc.is_destructive() || source.is_explicit() {
return Ok(());
}
let has_tenant = profile
.cloud
.as_ref()
.is_some_and(|cloud| cloud.tenant_id.is_some());
if !(has_tenant || profile.target.is_cloud()) {
return Ok(());
}
bail!(
"profile `{}` is a cloud profile selected implicitly (stored session or directory \
discovery); pass `--profile {}` or set SYSTEMPROMPT_PROFILE to target it",
profile.name,
profile.name
)
}
pub const fn is_cloud_bypass_command(command: Option<&args::Commands>) -> bool {
matches!(
command,
Some(args::Commands::Cloud(_) | args::Commands::Admin(admin::AdminCommands::Session(_)))
)
}
async fn initialize_post_routing(
ctx: &bootstrap::ProfileContext,
desc: &CommandDescriptor,
needs_cloud: bool,
) -> Result<BootstrapOutcome> {
if needs_cloud || (ctx.is_cloud && ctx.external_db_access) {
bootstrap::init_credentials_gracefully(needs_cloud).await?;
}
if desc.secrets() {
bootstrap::init_secrets().await?;
}
if ctx.is_cloud && ctx.external_db_access && desc.paths() && !ctx.env.is_deployment_host {
let secrets = SecretsBootstrap::get().context("Secrets required for external DB access")?;
let db_url = secrets.effective_database_url(true).to_owned();
return Ok(BootstrapOutcome::ExternalDbUrl(db_url));
}
if desc.paths() {
bootstrap::init_paths(desc.discovers_models()).await?;
if !desc.skip_validation() {
bootstrap::run_validation()?;
}
}
if !ctx.is_cloud {
bootstrap::validate_cloud_credentials(&ctx.env);
}
Ok(BootstrapOutcome::ContinueLocal)
}
async fn try_remote_routing(
cli: &args::Cli,
profile: &systemprompt_manifest::Profile,
cli_config: &CliConfig,
desc: &CommandDescriptor,
) -> Result<BootstrapOutcome> {
use super::routing;
let decision = decide_routing(
routing::determine_execution_target(),
profile,
desc.routing_class(),
desc.data_impact(),
)?;
let RoutingDecision::ExecuteRemote {
hostname,
token,
context,
} = decision
else {
return Ok(BootstrapOutcome::ContinueLocal);
};
confirm_remote_job_run(cli, cli_config, profile, &hostname)?;
let args = args::reconstruct_args();
let exit_code = routing::execute_remote(&hostname, &token, &context, &args, 300).await?;
if exit_code != 0 {
bail!("Remote command exited with code {}", exit_code);
}
Ok(BootstrapOutcome::RemoteExecuted)
}
pub fn confirm_remote_job_run(
cli: &args::Cli,
cli_config: &CliConfig,
profile: &systemprompt_manifest::Profile,
hostname: &str,
) -> Result<()> {
let Some(args::Commands::Infra(infrastructure::InfraCommands::Jobs(
infrastructure::jobs::JobsCommands::Run(run_args),
))) = cli.command.as_ref()
else {
return Ok(());
};
let selection = if run_args.all {
"all jobs".to_owned()
} else if let Some(tag) = &run_args.tag {
format!("jobs tagged '{tag}'")
} else {
run_args
.job_names
.iter()
.map(JobName::as_str)
.collect::<Vec<_>>()
.join(", ")
};
let message = format!(
"Run {selection} against REMOTE profile '{}' ({hostname})?\nPass --profile \
<local-profile> to target a local environment instead. Continue?",
profile.name
);
interactive::require_confirmation(
&interactive::DialoguerPrompter,
&message,
run_args.yes,
cli_config,
)
}