systemprompt_cli/commands/admin/setup/secrets/
mod.rs1mod data;
12pub mod prompts;
13
14use anyhow::{Context, Result};
15use std::path::Path;
16use systemprompt_identifiers::ProviderId;
17use systemprompt_logging::CliService;
18
19use super::SetupArgs;
20use crate::CliConfig;
21use crate::interactive::Prompter;
22use crate::shared::generate_identity;
23use data::resolve_primary;
24use prompts::{resolve_interactive_primary, select_provider_keys};
25
26pub use data::SecretsData;
27
28#[doc(hidden)]
29pub fn collect_non_interactive(
30 args: &SetupArgs,
31 config: &CliConfig,
32) -> Result<(SecretsData, Option<ProviderId>)> {
33 if !config.is_json_output() {
34 CliService::section("Secrets Setup");
35 }
36
37 let identity = generate_identity()?;
38 if !config.is_json_output() {
39 CliService::success(
40 "Generated OAuth at-rest pepper, encryption master key, manifest signing seed and signing key",
41 );
42 }
43
44 let secrets = SecretsData {
45 oauth_at_rest_pepper: identity.oauth_at_rest_pepper,
46 encryption_master_key: identity.encryption_master_key,
47 manifest_signing_secret_seed: Some(identity.manifest_signing_secret_seed),
48 signing_key_pem: Some(identity.signing_key_pem),
49 database_url: None,
50 gemini: args.gemini_key.clone(),
51 anthropic: args.anthropic_key.clone(),
52 openai: args.openai_key.clone(),
53 github: args.github_token.clone(),
54 };
55
56 validate_secrets(&secrets)?;
57 let primary = resolve_primary(args, &secrets)?;
58
59 if !config.is_json_output() {
60 CliService::success(&format!("Configured keys: {}", secrets.summary()));
61 }
62
63 Ok((secrets, primary))
64}
65
66#[doc(hidden)]
67pub fn collect_interactive(
68 args: &SetupArgs,
69 prompter: &dyn Prompter,
70 env_name: &str,
71 _config: &CliConfig,
72) -> Result<(SecretsData, Option<ProviderId>)> {
73 CliService::section(&format!("Secrets Setup ({})", env_name));
74 CliService::info("At least one AI provider API key is required.");
75
76 let identity = generate_identity()?;
77 CliService::success(
78 "Generated OAuth at-rest pepper, encryption master key, manifest signing seed and signing key",
79 );
80
81 let mut secrets = SecretsData {
82 oauth_at_rest_pepper: identity.oauth_at_rest_pepper,
83 encryption_master_key: identity.encryption_master_key,
84 manifest_signing_secret_seed: Some(identity.manifest_signing_secret_seed),
85 signing_key_pem: Some(identity.signing_key_pem),
86 ..Default::default()
87 };
88
89 if args.has_ai_provider() {
90 args.gemini_key.clone_into(&mut secrets.gemini);
91 args.anthropic_key.clone_into(&mut secrets.anthropic);
92 args.openai_key.clone_into(&mut secrets.openai);
93 args.github_token.clone_into(&mut secrets.github);
94 CliService::success(&format!("Using provided keys: {}", secrets.summary()));
95 let primary = resolve_primary(args, &secrets)?;
96 return Ok((secrets, primary));
97 }
98
99 let explicit = select_provider_keys(prompter, &mut secrets)?;
100 validate_secrets(&secrets)?;
101 let primary = resolve_interactive_primary(prompter, explicit, &secrets)?;
102
103 CliService::success(&format!("Configured keys: {}", secrets.summary()));
104
105 Ok((secrets, primary))
106}
107
108fn validate_secrets(secrets: &SecretsData) -> Result<()> {
109 if secrets.oauth_at_rest_pepper.len() < 32 {
110 anyhow::bail!("OAuth at-rest pepper must be at least 32 characters");
111 }
112 systemprompt_config::decode_master_key(&secrets.encryption_master_key)?;
113
114 if !secrets.has_ai_provider() {
115 anyhow::bail!(
116 "At least one AI provider API key is required.\n\n\
117 Provide one of:\n\
118 --gemini-key <KEY> Google AI (Gemini)\n\
119 --anthropic-key <KEY> Anthropic (Claude)\n\
120 --openai-key <KEY> OpenAI (GPT)\n\n\
121 Or set environment variables:\n\
122 GEMINI_API_KEY, ANTHROPIC_API_KEY, or OPENAI_API_KEY"
123 );
124 }
125
126 Ok(())
127}
128
129pub(super) fn save(secrets: &SecretsData, secrets_path: &Path) -> Result<()> {
130 if let Some(parent) = secrets_path.parent() {
131 std::fs::create_dir_all(parent)
132 .with_context(|| format!("Failed to create directory {}", parent.display()))?;
133 }
134
135 let content = serde_json::to_string_pretty(secrets).context("Failed to serialize secrets")?;
136
137 std::fs::write(secrets_path, content)
138 .with_context(|| format!("Failed to write {}", secrets_path.display()))?;
139
140 #[cfg(unix)]
141 {
142 use std::os::unix::fs::PermissionsExt;
143 let permissions = std::fs::Permissions::from_mode(0o600);
144 std::fs::set_permissions(secrets_path, permissions)
145 .with_context(|| format!("Failed to set permissions on {}", secrets_path.display()))?;
146 }
147
148 CliService::success(&format!("Saved secrets to {}", secrets_path.display()));
149
150 Ok(())
151}