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