Skip to main content

agentmux_srv/identity/resolver/
secret.rs

1// Copyright 2025-2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! [`resolve_secret`]: turning a `SecretRef` into a plaintext credential.
5//!
6//! Split out of the single ~2193-line `resolver.rs` (pure relocation, no
7//! behavior change).
8
9use crate::backend::storage::store::SecretRef;
10
11use super::errors::ResolverError;
12
13/// Resolve a `SecretRef` to the plaintext credential string. Each
14/// backend has a distinct path:
15///
16/// - **Env**: read `env_var` from the srv process's own environment.
17///   Caller is expected to have set this in their shell or a
18///   .env-style loader before launching AgentMux.
19/// - **PlaintextDev**: return the literal stored string. **Debug
20///   builds only** — guarded behind `cfg(debug_assertions)`. In
21///   release builds, the same call returns
22///   [`ResolverError::PlaintextDevDisabledInRelease`] so a forgotten
23///   dev-secret never leaks into a packaged binary. Reagent P1 on
24///   PR #751 caught the missing guard. Phase 3's encrypted vault is
25///   the production path.
26/// - **SecretsManager**: deferred. Returns
27///   [`ResolverError::SecretsManagerUnsupported`].
28pub fn resolve_secret(secret_ref: &SecretRef) -> Result<String, ResolverError> {
29    match secret_ref {
30        SecretRef::Env { env_var } => std::env::var(env_var)
31            .map_err(|_| ResolverError::EnvVarMissing(env_var.clone())),
32        SecretRef::PlaintextDev { plaintext_dev } => {
33            #[cfg(debug_assertions)]
34            {
35                Ok(plaintext_dev.clone())
36            }
37            #[cfg(not(debug_assertions))]
38            {
39                let _ = plaintext_dev;
40                Err(ResolverError::PlaintextDevDisabledInRelease)
41            }
42        }
43        SecretRef::SecretsManager { .. } => Err(ResolverError::SecretsManagerUnsupported),
44        SecretRef::OAuthConfigDir { dir } => {
45            // Read but ignored — the resolver doesn't consume the
46            // pointer; PR B's oauth-class dispatch in
47            // `inject_identity_env` reads `dir` and sets the
48            // provider's config-dir env var directly, bypassing
49            // `resolve_secret` entirely.
50            let _ = dir;
51            Err(ResolverError::OAuthConfigDirNotASecret)
52        }
53        SecretRef::Keychain { account, .. } => {
54            // Account API keys: pull the plaintext from the OS
55            // keychain at spawn time. The account string is
56            // `acct:<account_id>`; secret_store reconstructs the key
57            // from the id, so strip the namespace prefix here.
58            let account_id = account.strip_prefix("acct:").unwrap_or(account);
59            crate::identity::secret_store::get(account_id)
60                .map(|z| z.to_string())
61                .map_err(ResolverError::KeychainError)
62        }
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    // PlaintextDev-using tests are gated behind cfg(debug_assertions)
71    // because release builds reject PlaintextDev with
72    // ResolverError::PlaintextDevDisabledInRelease, so the assertions
73    // below would fail under `cargo test --release`. Reagent P2
74    // (PR #751). The Env / SecretsManager / unknown-provider paths
75    // are tested separately and have no debug-only dependency.
76
77    #[cfg(debug_assertions)]
78    #[test]
79    fn resolve_plaintext_dev() {
80        let s = resolve_secret(&SecretRef::PlaintextDev {
81            plaintext_dev: "ghp_test123".to_string(),
82        })
83        .unwrap();
84        assert_eq!(s, "ghp_test123");
85    }
86
87    #[test]
88    fn resolve_env_var_missing() {
89        let res = resolve_secret(&SecretRef::Env {
90            env_var: "AGENTMUX_TEST_NEVER_SET_X9Q".to_string(),
91        });
92        assert!(matches!(res, Err(ResolverError::EnvVarMissing(_))));
93    }
94
95    #[test]
96    fn resolve_secrets_manager_unsupported() {
97        let res = resolve_secret(&SecretRef::SecretsManager {
98            sm_path: "ignored".to_string(),
99            sm_json_path: None,
100        });
101        assert!(matches!(res, Err(ResolverError::SecretsManagerUnsupported)));
102    }
103
104    #[test]
105    fn resolve_oauth_config_dir_is_not_a_secret() {
106        // OAuthConfigDir is a pointer to a CLI-managed token directory,
107        // not a resolvable secret string. PR B's oauth-class dispatch
108        // in `inject_identity_env` reads `dir` and sets the provider's
109        // config-dir env var directly, bypassing `resolve_secret`. The
110        // error here is a guard against a caller forgetting that
111        // dispatch — pre-PR-B nothing produces this variant, but the
112        // arm has to exist for the match to be exhaustive.
113        let res = resolve_secret(&SecretRef::OAuthConfigDir {
114            dir: "/path/to/bundle/claude".to_string(),
115        });
116        assert!(matches!(res, Err(ResolverError::OAuthConfigDirNotASecret)));
117    }
118}