@@ -643,6 +643,60 @@ fn mtls_certs_exist_for_endpoint(name: &str, endpoint: &str) -> bool {
643643 } )
644644}
645645
646+ fn package_managed_tls_dirs ( ) -> Vec < PathBuf > {
647+ if let Some ( path) = std:: env:: var_os ( "OPENSHELL_LOCAL_TLS_DIR" ) {
648+ return vec ! [ PathBuf :: from( path) ] ;
649+ }
650+
651+ let mut dirs = Vec :: new ( ) ;
652+
653+ if cfg ! ( target_os = "macos" ) {
654+ dirs. push ( PathBuf :: from ( "/opt/homebrew/var/openshell/tls" ) ) ;
655+ dirs. push ( PathBuf :: from ( "/usr/local/var/openshell/tls" ) ) ;
656+ }
657+
658+ let state_dir = std:: env:: var_os ( "XDG_STATE_HOME" )
659+ . map ( PathBuf :: from)
660+ . or_else ( || std:: env:: var_os ( "HOME" ) . map ( |home| PathBuf :: from ( home) . join ( ".local/state" ) ) ) ;
661+ if let Some ( state_dir) = state_dir {
662+ dirs. push ( state_dir. join ( "openshell/tls" ) ) ;
663+ }
664+
665+ dirs
666+ }
667+
668+ fn import_local_package_mtls_bundle ( name : & str ) -> Result < Option < PathBuf > > {
669+ for dir in package_managed_tls_dirs ( ) {
670+ let ca = dir. join ( "ca.crt" ) ;
671+ let cert = dir. join ( "client/tls.crt" ) ;
672+ let key = dir. join ( "client/tls.key" ) ;
673+ if !( ca. is_file ( ) && cert. is_file ( ) && key. is_file ( ) ) {
674+ continue ;
675+ }
676+
677+ let bundle = openshell_bootstrap:: pki:: PkiBundle {
678+ ca_cert_pem : std:: fs:: read_to_string ( & ca)
679+ . into_diagnostic ( )
680+ . wrap_err_with ( || format ! ( "failed to read {}" , ca. display( ) ) ) ?,
681+ ca_key_pem : String :: new ( ) ,
682+ server_cert_pem : String :: new ( ) ,
683+ server_key_pem : String :: new ( ) ,
684+ client_cert_pem : std:: fs:: read_to_string ( & cert)
685+ . into_diagnostic ( )
686+ . wrap_err_with ( || format ! ( "failed to read {}" , cert. display( ) ) ) ?,
687+ client_key_pem : std:: fs:: read_to_string ( & key)
688+ . into_diagnostic ( )
689+ . wrap_err_with ( || format ! ( "failed to read {}" , key. display( ) ) ) ?,
690+ } ;
691+ openshell_bootstrap:: mtls:: store_pki_bundle ( name, & bundle)
692+ . wrap_err_with ( || format ! ( "failed to store mTLS bundle for gateway '{name}'" ) ) ?;
693+
694+ return Ok ( Some ( dir) ) ;
695+ }
696+
697+ Ok ( None )
698+ }
699+
646700fn plaintext_gateway_is_remote ( endpoint : & str , remote : Option < & str > , local : bool ) -> bool {
647701 if local {
648702 return false ;
@@ -924,16 +978,13 @@ pub async fn gateway_add(
924978
925979 // Verify the gateway is reachable.
926980 let tls = TlsOptions :: default ( ) ;
927- match http_health_check ( & endpoint, & tls) . await {
928- Ok ( Some ( status) ) if status. is_success ( ) => { }
929- _ => {
930- eprintln ! (
931- "{} Gateway is not reachable at {endpoint}" ,
932- "⚠" . yellow( ) . bold( ) ,
933- ) ;
934- if !has_mtls_certs {
935- eprintln ! ( " Verify the gateway is running and the endpoint is correct." ) ;
936- }
981+ if !gateway_reachable ( & endpoint, & tls) . await {
982+ eprintln ! (
983+ "{} Gateway is not reachable at {endpoint}" ,
984+ "⚠" . yellow( ) . bold( ) ,
985+ ) ;
986+ if !has_mtls_certs {
987+ eprintln ! ( " Verify the gateway is running and the endpoint is correct." ) ;
937988 }
938989 }
939990
@@ -951,7 +1002,13 @@ pub async fn gateway_add(
9511002
9521003 if remote. is_some ( ) || local {
9531004 // mTLS gateway (remote or local).
954- let certs_on_disk = mtls_certs_exist_for_endpoint ( name, & endpoint) ;
1005+ let imported_mtls_dir = if local {
1006+ import_local_package_mtls_bundle ( name) ?
1007+ } else {
1008+ None
1009+ } ;
1010+ let certs_on_disk =
1011+ imported_mtls_dir. is_some ( ) || mtls_certs_exist_for_endpoint ( name, & endpoint) ;
9551012 if !certs_on_disk {
9561013 return Err ( miette:: miette!(
9571014 "mTLS certificates for gateway '{name}' were not found.\n \
@@ -984,14 +1041,11 @@ pub async fn gateway_add(
9841041
9851042 // Verify the gateway is reachable over mTLS.
9861043 let tls = TlsOptions :: default ( ) . with_gateway_name ( name) ;
987- match http_health_check ( & endpoint, & tls) . await {
988- Ok ( Some ( status) ) if status. is_success ( ) => { }
989- _ => {
990- eprintln ! (
991- "{} Gateway is not reachable at {endpoint}. Verify the gateway is running." ,
992- "⚠" . yellow( ) . bold( ) ,
993- ) ;
994- }
1044+ if !gateway_reachable ( & endpoint, & tls) . await {
1045+ eprintln ! (
1046+ "{} Gateway is not reachable at {endpoint}. Verify the gateway is running." ,
1047+ "⚠" . yellow( ) . bold( ) ,
1048+ ) ;
9951049 }
9961050
9971051 eprintln ! (
@@ -1252,6 +1306,16 @@ async fn http_health_check(server: &str, tls: &TlsOptions) -> Result<Option<Stat
12521306 Ok ( Some ( resp. status ( ) ) )
12531307}
12541308
1309+ async fn gateway_reachable ( server : & str , tls : & TlsOptions ) -> bool {
1310+ if let Ok ( mut client) = grpc_client ( server, tls) . await
1311+ && client. health ( HealthRequest { } ) . await . is_ok ( )
1312+ {
1313+ return true ;
1314+ }
1315+
1316+ matches ! ( http_health_check( server, tls) . await , Ok ( Some ( status) ) if status. is_success( ) )
1317+ }
1318+
12551319fn remove_gateway_registration ( name : & str ) {
12561320 if let Err ( err) = openshell_bootstrap:: edge_token:: remove_edge_token ( name) {
12571321 tracing:: debug!( "failed to remove edge token: {err}" ) ;
@@ -5391,18 +5455,19 @@ mod tests {
53915455 TlsOptions , dockerfile_sources_supported_for_gateway, format_gateway_select_header,
53925456 format_gateway_select_items, format_provider_attachment_table, gateway_add,
53935457 gateway_auth_label, gateway_env_override_warning, gateway_select_with, gateway_type_label,
5394- git_sync_files, http_health_check, image_requests_gpu, inferred_provider_type,
5395- parse_cli_setting_value, parse_credential_pairs, plaintext_gateway_is_remote,
5396- provisioning_timeout_message, ready_false_condition_message, resolve_from,
5397- sandbox_should_persist,
5458+ git_sync_files, http_health_check, image_requests_gpu, import_local_package_mtls_bundle,
5459+ inferred_provider_type, package_managed_tls_dirs, parse_cli_setting_value,
5460+ parse_credential_pairs,
5461+ plaintext_gateway_is_remote, provisioning_timeout_message, ready_false_condition_message,
5462+ resolve_from, sandbox_should_persist,
53985463 } ;
53995464 use crate :: TEST_ENV_LOCK ;
54005465 use hyper:: StatusCode ;
54015466 use openshell_bootstrap:: { load_active_gateway, load_gateway_metadata, store_gateway_metadata} ;
54025467 use std:: fs;
54035468 use std:: io:: { Read , Write } ;
54045469 use std:: net:: TcpListener ;
5405- use std:: path:: Path ;
5470+ use std:: path:: { Path , PathBuf } ;
54065471 use std:: process:: Command ;
54075472 use std:: thread;
54085473
@@ -6023,6 +6088,52 @@ mod tests {
60236088 assert_eq ! ( gateway_auth_label( & gateway) , "mtls" ) ;
60246089 }
60256090
6091+ #[ test]
6092+ fn package_managed_tls_dirs_respects_override ( ) {
6093+ let _guard = TEST_ENV_LOCK
6094+ . lock ( )
6095+ . unwrap_or_else ( std:: sync:: PoisonError :: into_inner) ;
6096+ let _tls_dir = EnvVarGuard :: set ( "OPENSHELL_LOCAL_TLS_DIR" , "/tmp/openshell-test-tls" ) ;
6097+
6098+ assert_eq ! (
6099+ package_managed_tls_dirs( ) ,
6100+ vec![ PathBuf :: from( "/tmp/openshell-test-tls" ) ] ,
6101+ ) ;
6102+ }
6103+
6104+ #[ test]
6105+ fn import_local_package_mtls_bundle_copies_client_materials ( ) {
6106+ let tmpdir = tempfile:: tempdir ( ) . expect ( "create tmpdir" ) ;
6107+ let package_tls = tmpdir. path ( ) . join ( "package-tls" ) ;
6108+ fs:: create_dir_all ( package_tls. join ( "client" ) ) . expect ( "create package tls dir" ) ;
6109+ fs:: write ( package_tls. join ( "ca.crt" ) , "ca" ) . expect ( "write ca" ) ;
6110+ fs:: write ( package_tls. join ( "client/tls.crt" ) , "client cert" ) . expect ( "write cert" ) ;
6111+ fs:: write ( package_tls. join ( "client/tls.key" ) , "client key" ) . expect ( "write key" ) ;
6112+
6113+ with_tmp_xdg ( tmpdir. path ( ) , || {
6114+ let _tls_dir = EnvVarGuard :: set (
6115+ "OPENSHELL_LOCAL_TLS_DIR" ,
6116+ package_tls. to_str ( ) . expect ( "temp path should be utf-8" ) ,
6117+ ) ;
6118+
6119+ let imported =
6120+ import_local_package_mtls_bundle ( "openshell" ) . expect ( "import local bundle" ) ;
6121+
6122+ assert_eq ! ( imported. as_deref( ) , Some ( package_tls. as_path( ) ) ) ;
6123+
6124+ let mtls = tmpdir. path ( ) . join ( "openshell/gateways/openshell/mtls" ) ;
6125+ assert_eq ! ( fs:: read_to_string( mtls. join( "ca.crt" ) ) . unwrap( ) , "ca" ) ;
6126+ assert_eq ! (
6127+ fs:: read_to_string( mtls. join( "tls.crt" ) ) . unwrap( ) ,
6128+ "client cert" ,
6129+ ) ;
6130+ assert_eq ! (
6131+ fs:: read_to_string( mtls. join( "tls.key" ) ) . unwrap( ) ,
6132+ "client key" ,
6133+ ) ;
6134+ } ) ;
6135+ }
6136+
60266137 #[ test]
60276138 fn plaintext_gateway_locality_infers_loopback_endpoints_as_local ( ) {
60286139 assert ! ( !plaintext_gateway_is_remote(
0 commit comments