systemprompt_cli/commands/cloud/tenant/
validation.rs1use 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}