systemprompt-config 0.67.0

Profile-based configuration for systemprompt.io AI governance infrastructure. Bootstraps profiles, secrets, and credentials with zero environment-variable fallback.
Documentation
//! Remote secrets provider seam.
//!
//! A provider returns a [`SecretsDocument`] — the raw JSON object holding the
//! `secrets.json` shape — rather than a parsed [`Secrets`]. Keeping the
//! untyped document one step longer is what lets per-key overrides be merged
//! in before a single validation pass decides whether the result is usable.
//!
//! [`SecretsProvider`] uses an `async fn`-style signature and so is not
//! object-safe; [`DynSecretsProvider`] is its boxed-future twin, implemented
//! for every `SecretsProvider`, so registered external sources can be held as
//! trait objects.
//!
//! Copyright (c) systemprompt.io — Business Source License 1.1.
//! See <https://systemprompt.io> for licensing details.

use std::future::Future;
use std::pin::Pin;
use std::time::Duration;

use systemprompt_manifest::secrets::Secrets;

use super::SecretsBootstrapError;

pub trait SecretsProvider {
    fn fetch(&self) -> impl Future<Output = Result<SecretsDocument, SecretsBootstrapError>> + Send;

    fn describe(&self) -> String;
}

pub type SecretsFetch<'a> =
    Pin<Box<dyn Future<Output = Result<SecretsDocument, SecretsBootstrapError>> + Send + 'a>>;

pub trait DynSecretsProvider: Send + Sync {
    fn fetch_boxed(&self) -> SecretsFetch<'_>;

    fn describe(&self) -> String;

    fn refresh_hint(&self) -> Option<Duration> {
        None
    }
}

impl<T: SecretsProvider + Send + Sync> DynSecretsProvider for T {
    fn fetch_boxed(&self) -> SecretsFetch<'_> {
        Box::pin(self.fetch())
    }

    fn describe(&self) -> String {
        SecretsProvider::describe(self)
    }
}

#[derive(Debug, Clone, Default)]
// JSON: `secrets.json` document; keys are operator-defined until parsed.
pub struct SecretsDocument(serde_json::Map<String, serde_json::Value>);

impl SecretsDocument {
    #[must_use]
    // JSON: `secrets.json` document; keys are operator-defined until parsed.
    pub const fn new(fields: serde_json::Map<String, serde_json::Value>) -> Self {
        Self(fields)
    }

    // JSON: `secrets.json` document; keys are operator-defined until parsed.
    pub fn merge_field(&mut self, key: impl Into<String>, value: serde_json::Value) {
        self.0.insert(key.into(), value);
    }

    #[must_use]
    pub fn key_names(&self) -> Vec<String> {
        let mut names: Vec<String> = self.0.keys().cloned().collect();
        names.sort_unstable();
        names
    }

    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    pub fn into_secrets(self) -> Result<Secrets, SecretsBootstrapError> {
        let body = serde_json::to_string(&serde_json::Value::Object(self.0))
            .map_err(SecretsBootstrapError::DocumentEncode)?;
        Secrets::parse(&body).map_err(SecretsBootstrapError::InvalidSecretsFile)
    }
}