Skip to main content

systemprompt_cli/commands/admin/config/
secret.rs

1//! `admin config secret set` — write a provider credential into the profile's
2//! secrets file without hand-editing JSON.
3//!
4//! Known provider names map to their typed field; any other name becomes a
5//! custom secret (e.g. `minimax`). Infrastructure secrets
6//! (database/pepper/signing seed) are rejected so they cannot collide with the
7//! typed fields on round-trip.
8
9use anyhow::{Context, Result, bail};
10use clap::{Args, Subcommand};
11use systemprompt_config::ProfileBootstrap;
12use systemprompt_models::Secrets;
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    if RESERVED.contains(&args.name.as_str()) {
47        bail!(
48            "'{}' is a reserved infrastructure secret and cannot be set here",
49            args.name
50        );
51    }
52
53    let profile_path = ProfileBootstrap::get_path()?;
54    let profile = load_profile(profile_path)?;
55    let secrets_rel = profile
56        .secrets
57        .as_ref()
58        .map(|s| s.secrets_path.clone())
59        .ok_or_else(|| anyhow::anyhow!("profile has no secrets section"))?;
60    let secrets_file = profile_dir(profile_path).join(&secrets_rel);
61
62    let content = std::fs::read_to_string(&secrets_file)
63        .with_context(|| format!("Failed to read secrets: {}", secrets_file.display()))?;
64    let mut secrets: Secrets = serde_json::from_str(&content)
65        .with_context(|| format!("Failed to parse secrets: {}", secrets_file.display()))?;
66
67    set_named(&mut secrets, &args.name, args.value.clone());
68
69    let serialized =
70        serde_json::to_string_pretty(&secrets).context("Failed to serialize secrets")?;
71    std::fs::write(&secrets_file, serialized)
72        .with_context(|| format!("Failed to write {}", secrets_file.display()))?;
73
74    render_result(&CommandOutput::card_value(
75        "Secret Updated",
76        &ConfigMutationOutput {
77            field: "secrets".to_owned(),
78            message: format!("Secret '{}' set", args.name),
79        },
80    ));
81    Ok(())
82}
83
84fn set_named(secrets: &mut Secrets, name: &str, value: String) {
85    match name {
86        "gemini" => secrets.gemini = Some(value),
87        "anthropic" => secrets.anthropic = Some(value),
88        "openai" => secrets.openai = Some(value),
89        "github" => secrets.github = Some(value),
90        "moonshot" => secrets.moonshot = Some(value),
91        "qwen" => secrets.qwen = Some(value),
92        other => {
93            secrets.custom.insert(other.to_owned(), value);
94        },
95    }
96}