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