Skip to main content

systemprompt_config/bootstrap/secrets/
resolve.rs

1//! Pure precedence resolution for the secrets source.
2//!
3//! The decision is separated from every side effect so the ordering can be
4//! exercised directly: nothing here reads the environment, the filesystem, or
5//! the network. Callers pass the already-observed facts.
6//!
7//! Precedence, highest first:
8//! 1. subprocess marker — the parent handed the child its secrets as env
9//! 2. `source: vault` — wins even on deployment hosts, so a container is not
10//!    silently downgraded to whatever env happens to be set
11//! 3. deployment host — env, with or without a pepper already present
12//! 4. `source: env` locally — file first, env as fallback
13//! 5. `source: file`
14//!
15//! Copyright (c) systemprompt.io — Business Source License 1.1.
16//! See <https://systemprompt.io> for licensing details.
17
18use systemprompt_manifest::profile::{SecretsConfig, SecretsSource, VaultSecretsConfig};
19
20use super::SecretsBootstrapError;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum ResolvedSource<'a> {
24    SubprocessEnv,
25    Vault(&'a VaultSecretsConfig),
26    DeploymentHostEnv,
27    LocalEnvWithFileFallback(&'a str),
28    File(&'a str),
29}
30
31pub fn resolve_source(
32    config: Option<&SecretsConfig>,
33    is_subprocess: bool,
34    is_deployment_host: bool,
35    has_valid_pepper_in_env: bool,
36) -> Result<ResolvedSource<'_>, SecretsBootstrapError> {
37    if is_subprocess && has_valid_pepper_in_env {
38        return Ok(ResolvedSource::SubprocessEnv);
39    }
40
41    let Some(config) = config else {
42        return if is_deployment_host && has_valid_pepper_in_env {
43            Ok(ResolvedSource::DeploymentHostEnv)
44        } else {
45            Err(SecretsBootstrapError::NoSecretsConfigured)
46        };
47    };
48
49    match config.source {
50        SecretsSource::Vault => config
51            .vault
52            .as_ref()
53            .map(ResolvedSource::Vault)
54            .ok_or(SecretsBootstrapError::VaultBlockMissing),
55        SecretsSource::Env if is_deployment_host => Ok(ResolvedSource::DeploymentHostEnv),
56        SecretsSource::File if is_deployment_host && has_valid_pepper_in_env => {
57            Ok(ResolvedSource::DeploymentHostEnv)
58        },
59        SecretsSource::Env => Ok(ResolvedSource::LocalEnvWithFileFallback(configured_path(
60            config,
61        )?)),
62        SecretsSource::File => Ok(ResolvedSource::File(configured_path(config)?)),
63    }
64}
65
66fn configured_path(config: &SecretsConfig) -> Result<&str, SecretsBootstrapError> {
67    config
68        .secrets_path()
69        .map_err(SecretsBootstrapError::SecretsConfigInvalid)
70}