systemprompt_cli/commands/admin/config/
secret.rs1use std::path::Path;
12
13use anyhow::{Context, Result, bail};
14use clap::{Args, Subcommand};
15use systemprompt_config::ProfileBootstrap;
16
17use super::profile_io::{load_profile, profile_dir};
18use super::types::ConfigMutationOutput;
19use crate::CliConfig;
20use crate::shared::{CommandOutput, render_result};
21
22const RESERVED: &[&str] = &[
23 "oauth_at_rest_pepper",
24 "manifest_signing_secret_seed",
25 "database_url",
26 "database_write_url",
27 "external_database_url",
28 "internal_database_url",
29];
30
31#[derive(Debug, Subcommand)]
32pub enum SecretCommands {
33 #[command(about = "Set a provider or custom secret")]
34 Set(SetArgs),
35}
36
37#[derive(Debug, Clone, Args)]
38pub struct SetArgs {
39 #[arg(help = "Secret name (e.g. anthropic, minimax)")]
40 pub name: String,
41
42 #[arg(help = "Secret value")]
43 pub value: String,
44}
45
46pub fn execute(command: &SecretCommands, config: &CliConfig) -> Result<()> {
47 let SecretCommands::Set(args) = command;
48
49 let profile_path = ProfileBootstrap::get_path()?;
50 let profile = load_profile(profile_path)?;
51 let secrets_rel = profile
52 .secrets
53 .as_ref()
54 .map(|s| s.secrets_path.clone())
55 .ok_or_else(|| anyhow::anyhow!("profile has no secrets section"))?;
56 let secrets_file = profile_dir(profile_path).join(&secrets_rel);
57
58 set_secret(&secrets_file, &args.name, &args.value)?;
59
60 render_result(
61 &CommandOutput::card_value(
62 "Secret Updated",
63 &ConfigMutationOutput {
64 field: "secrets".to_owned(),
65 message: format!("Secret '{}' set", args.name),
66 },
67 ),
68 config,
69 );
70 Ok(())
71}
72
73pub fn set_secret(secrets_file: &Path, name: &str, value: &str) -> Result<()> {
74 if RESERVED.contains(&name) {
75 bail!("'{name}' is a reserved infrastructure secret and cannot be set here");
76 }
77
78 let content = std::fs::read_to_string(secrets_file)
79 .with_context(|| format!("Failed to read secrets: {}", secrets_file.display()))?;
80 let mut doc: serde_json::Value = serde_json::from_str(&content)
83 .with_context(|| format!("Failed to parse secrets: {}", secrets_file.display()))?;
84 let object = doc.as_object_mut().ok_or_else(|| {
85 anyhow::anyhow!(
86 "secrets file is not a JSON object: {}",
87 secrets_file.display()
88 )
89 })?;
90 object.insert(name.to_owned(), serde_json::Value::String(value.to_owned()));
91
92 let serialized = serde_json::to_string_pretty(&doc).context("Failed to serialize secrets")?;
93 std::fs::write(secrets_file, serialized)
94 .with_context(|| format!("Failed to write {}", secrets_file.display()))?;
95 Ok(())
96}