Skip to main content

systemprompt_cli/commands/cloud/tenant/
validation.rs

1//! Tenant name/config validation for cloud commands.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use anyhow::{Context, Result, anyhow, bail};
7use systemprompt_cloud::deploy::find_services_config;
8use systemprompt_loader::{ConfigLoader, ExtensionLoader};
9use systemprompt_logging::CliService;
10use systemprompt_models::ServicesConfig;
11
12use crate::shared::project::ProjectRoot;
13
14pub(super) struct BuildValidationResult {
15    pub required_secrets: Vec<String>,
16}
17
18pub fn check_build_ready() -> Result<(), String> {
19    validate_build_ready()
20        .map(|_| ())
21        .map_err(|e| e.to_string())
22}
23
24pub(super) fn validate_build_ready() -> Result<BuildValidationResult> {
25    let project_root =
26        ProjectRoot::discover().context("Must be in a systemprompt.io project directory")?;
27    let root = project_root.as_path();
28
29    let binary_path = root.join("target/release/systemprompt");
30    if !binary_path.exists() {
31        bail!(
32            "Release binary not found: {}\n\nRun: cargo build --release --bin systemprompt",
33            binary_path.display()
34        );
35    }
36
37    let extension_result = ExtensionLoader::validate(root);
38    if !extension_result.missing_binaries.is_empty() {
39        let missing_list = extension_result.format_missing_binaries();
40        bail!(
41            "MCP extension binaries not found:\n\n{}\n\nRun: just build --release",
42            missing_list
43        );
44    }
45
46    let services_path = find_services_config(root)?;
47    let services_config = ConfigLoader::load_from_path(&services_path).with_context(|| {
48        format!(
49            "Failed to load services config: {}",
50            services_path.display()
51        )
52    })?;
53
54    let required_secrets = validate_ai_config(&services_config)?;
55
56    Ok(BuildValidationResult { required_secrets })
57}
58
59pub fn validate_ai_config(services_config: &ServicesConfig) -> Result<Vec<String>> {
60    let ai = &services_config.ai;
61    let mut required_secrets = vec![];
62
63    if ai.default_provider.is_empty() {
64        bail!(
65            "AI config missing default_provider.\n\nSet default_provider in \
66             services/ai/config.yaml (e.g., default_provider: \"anthropic\")"
67        );
68    }
69
70    let provider = ai.providers.get(&ai.default_provider).ok_or_else(|| {
71        anyhow!(
72            "Default provider '{}' not found in providers.\n\nAdd '{}' to ai.providers in your \
73             config.",
74            ai.default_provider,
75            ai.default_provider
76        )
77    })?;
78
79    if !provider.enabled {
80        bail!(
81            "Default provider '{}' is disabled.\n\nSet enabled: true for the '{}' provider.",
82            ai.default_provider,
83            ai.default_provider
84        );
85    }
86
87    for (name, prov) in &ai.providers {
88        if prov.enabled {
89            let secret_key = match name.as_str() {
90                "anthropic" => "ANTHROPIC_API_KEY",
91                "openai" => "OPENAI_API_KEY",
92                "google" => "GOOGLE_API_KEY",
93                _ => continue,
94            };
95            required_secrets.push(secret_key.to_owned());
96        }
97    }
98
99    Ok(required_secrets)
100}
101
102pub(super) fn warn_required_secrets(required_secrets: &[String]) {
103    if required_secrets.is_empty() {
104        return;
105    }
106
107    CliService::warning("Deployment requires API keys to be set via secrets:");
108    for secret in required_secrets {
109        CliService::info(&format!("  • {}", secret));
110    }
111    CliService::info("");
112    CliService::info("Set secrets with: systemprompt cloud secrets set <KEY> <VALUE>");
113    CliService::warning("Your deployment won't work until these secrets are configured.");
114}