@@ -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" ) ]
5158pub 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
6786impl 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