Skip to content

Commit 6370c79

Browse files
committed
feat: support reading TLS cert paths from env vars
Adds support for specifying TLS certificate paths via environment variables in model provider configuration. Each certificate/key path field now has a corresponding `-env` variant that names an environment variable containing the path. New config fields: - `ca-certificate-env`: env var for CA certificate path - `client-certificate-env`: env var for client certificate path - `client-private-key-env`: env var for client private key path If both a direct path and env var are specified, the env var takes precedence (if set and non-empty). This follows the same pattern used by `env_http_headers` for HTTP headers in the codebase. Example config: ```toml [model_providers.my-provider.tls] ca-certificate-env = "MY_CA_CERT_PATH" client-certificate-env = "MY_CLIENT_CERT_PATH" client-private-key-env = "MY_CLIENT_KEY_PATH" ```
1 parent 9b8e0d0 commit 6370c79

File tree

1 file changed

+129
-4
lines changed

1 file changed

+129
-4
lines changed

codex-rs/core/src/model_provider_info.rs

Lines changed: 129 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,32 +46,81 @@ pub enum WireApi {
4646
}
4747

4848
/// TLS configuration for mutual TLS (mTLS) authentication with model providers.
49+
///
50+
/// Each certificate/key path can be specified either directly or via an environment variable:
51+
/// - `ca-certificate` / `ca-certificate-env`: Path to a custom CA certificate (PEM format)
52+
/// - `client-certificate` / `client-certificate-env`: Path to the client certificate (PEM format)
53+
/// - `client-private-key` / `client-private-key-env`: Path to the client private key (PEM format)
54+
///
55+
/// If both the direct path and env var are specified, the env var takes precedence.
4956
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
5057
#[serde(rename_all = "kebab-case")]
5158
pub struct ModelProviderTlsConfig {
5259
/// Path to a custom CA certificate (PEM format) to trust when connecting to this provider.
5360
/// Relative paths are resolved against `~/.codex/`.
5461
pub ca_certificate: Option<PathBuf>,
5562

63+
/// Environment variable containing the path to a custom CA certificate.
64+
/// Takes precedence over `ca_certificate` if set.
65+
pub ca_certificate_env: Option<String>,
66+
5667
/// Path to the client certificate (PEM format) for mutual TLS authentication.
5768
/// Must be provided together with `client_private_key`.
5869
/// Relative paths are resolved against `~/.codex/`.
5970
pub client_certificate: Option<PathBuf>,
6071

72+
/// Environment variable containing the path to the client certificate.
73+
/// Takes precedence over `client_certificate` if set.
74+
pub client_certificate_env: Option<String>,
75+
6176
/// Path to the client private key (PEM format) for mutual TLS authentication.
6277
/// Must be provided together with `client_certificate`.
6378
/// Relative paths are resolved against `~/.codex/`.
6479
pub client_private_key: Option<PathBuf>,
80+
81+
/// Environment variable containing the path to the client private key.
82+
/// Takes precedence over `client_private_key` if set.
83+
pub client_private_key_env: Option<String>,
6584
}
6685

6786
impl ModelProviderTlsConfig {
68-
/// Convert to the default_client TlsConfig type
87+
/// Convert to the default_client TlsConfig type.
88+
/// Environment variable values take precedence over direct path values.
6989
pub fn to_tls_config(&self) -> crate::default_client::TlsConfig {
7090
crate::default_client::TlsConfig {
71-
ca_certificate: self.ca_certificate.clone(),
72-
client_certificate: self.client_certificate.clone(),
73-
client_private_key: self.client_private_key.clone(),
91+
ca_certificate: Self::resolve_path(&self.ca_certificate_env, &self.ca_certificate),
92+
client_certificate: Self::resolve_path(
93+
&self.client_certificate_env,
94+
&self.client_certificate,
95+
),
96+
client_private_key: Self::resolve_path(
97+
&self.client_private_key_env,
98+
&self.client_private_key,
99+
),
100+
}
101+
}
102+
103+
/// Resolve a path from either an environment variable or a direct value.
104+
/// The env var takes precedence if set and non-empty.
105+
fn resolve_path(env_var: &Option<String>, direct: &Option<PathBuf>) -> Option<PathBuf> {
106+
let env_value = env_var.as_ref().and_then(|name| std::env::var(name).ok());
107+
Self::resolve_path_with_value(env_value.as_deref(), direct)
108+
}
109+
110+
/// Pure function to resolve a path given an optional env var value and direct path.
111+
/// The env var value takes precedence if non-empty.
112+
fn resolve_path_with_value(
113+
env_value: Option<&str>,
114+
direct: &Option<PathBuf>,
115+
) -> Option<PathBuf> {
116+
if let Some(value) = env_value {
117+
let trimmed = value.trim();
118+
if !trimmed.is_empty() {
119+
return Some(PathBuf::from(trimmed));
120+
}
74121
}
122+
// Fall back to direct path
123+
direct.clone()
75124
}
76125
}
77126

@@ -533,4 +582,80 @@ env_http_headers = { "X-Example-Env-Header" = "EXAMPLE_ENV_VAR" }
533582
);
534583
}
535584
}
585+
586+
#[test]
587+
fn test_tls_config_resolve_path_env_var_precedence() {
588+
// Test that env var value takes precedence when set
589+
let result = ModelProviderTlsConfig::resolve_path_with_value(
590+
Some("/from/env/ca.pem"),
591+
&Some(PathBuf::from("/direct/ca.pem")),
592+
);
593+
assert_eq!(result, Some(PathBuf::from("/from/env/ca.pem")));
594+
}
595+
596+
#[test]
597+
fn test_tls_config_resolve_path_fallback_to_direct() {
598+
// Test fallback to direct path when env var is not set
599+
let result = ModelProviderTlsConfig::resolve_path_with_value(
600+
None,
601+
&Some(PathBuf::from("/fallback/ca.pem")),
602+
);
603+
assert_eq!(result, Some(PathBuf::from("/fallback/ca.pem")));
604+
}
605+
606+
#[test]
607+
fn test_tls_config_resolve_path_empty_env_var_falls_back() {
608+
// Test fallback when env var is empty
609+
let result = ModelProviderTlsConfig::resolve_path_with_value(
610+
Some(""),
611+
&Some(PathBuf::from("/fallback/ca.pem")),
612+
);
613+
assert_eq!(result, Some(PathBuf::from("/fallback/ca.pem")));
614+
}
615+
616+
#[test]
617+
fn test_tls_config_resolve_path_whitespace_env_var_falls_back() {
618+
// Test fallback when env var is whitespace only
619+
let result = ModelProviderTlsConfig::resolve_path_with_value(
620+
Some(" "),
621+
&Some(PathBuf::from("/fallback/ca.pem")),
622+
);
623+
assert_eq!(result, Some(PathBuf::from("/fallback/ca.pem")));
624+
}
625+
626+
#[test]
627+
fn test_tls_config_resolve_path_no_env_var_name() {
628+
// Test when no env var value is provided
629+
let result = ModelProviderTlsConfig::resolve_path_with_value(
630+
None,
631+
&Some(PathBuf::from("/direct/ca.pem")),
632+
);
633+
assert_eq!(result, Some(PathBuf::from("/direct/ca.pem")));
634+
}
635+
636+
#[test]
637+
fn test_tls_config_resolve_path_both_none() {
638+
// Test when neither is specified
639+
let result = ModelProviderTlsConfig::resolve_path_with_value(None, &None);
640+
assert_eq!(result, None);
641+
}
642+
643+
#[test]
644+
fn test_deserialize_tls_config_with_env_vars() {
645+
let toml_str = r#"
646+
ca-certificate = "/direct/ca.pem"
647+
ca-certificate-env = "MY_CA_CERT"
648+
client-certificate-env = "MY_CLIENT_CERT"
649+
client-private-key-env = "MY_CLIENT_KEY"
650+
"#;
651+
652+
let config: ModelProviderTlsConfig = toml::from_str(toml_str).unwrap();
653+
654+
assert_eq!(config.ca_certificate, Some(PathBuf::from("/direct/ca.pem")));
655+
assert_eq!(config.ca_certificate_env, Some("MY_CA_CERT".into()));
656+
assert_eq!(config.client_certificate, None);
657+
assert_eq!(config.client_certificate_env, Some("MY_CLIENT_CERT".into()));
658+
assert_eq!(config.client_private_key, None);
659+
assert_eq!(config.client_private_key_env, Some("MY_CLIENT_KEY".into()));
660+
}
536661
}

0 commit comments

Comments
 (0)