@@ -19,10 +19,10 @@ use hyper_util::{client::legacy::Client, rt::TokioExecutor};
1919use indicatif:: { MultiProgress , ProgressBar , ProgressStyle } ;
2020use miette:: { IntoDiagnostic , Result , WrapErr , miette} ;
2121use openshell_bootstrap:: {
22- GatewayMetadata , clear_active_gateway, clear_last_sandbox_if_matches,
23- extract_host_from_ssh_destination, get_gateway_metadata , list_gateways , load_active_gateway ,
24- remove_gateway_metadata , resolve_ssh_hostname , save_active_gateway , save_last_sandbox ,
25- store_gateway_metadata,
22+ GatewayMetadata , GatewayMetadataSource , clear_active_gateway, clear_last_sandbox_if_matches,
23+ extract_host_from_ssh_destination, gateway_metadata_source , get_gateway_metadata ,
24+ list_gateways , load_active_gateway , remove_gateway_metadata , resolve_ssh_hostname ,
25+ save_active_gateway , save_last_sandbox , store_gateway_metadata,
2626} ;
2727use openshell_core:: progress:: {
2828 PROGRESS_ACTIVE_DETAIL_KEY , PROGRESS_ACTIVE_STEP_KEY , PROGRESS_COMPLETE_LABEL_KEY ,
@@ -658,6 +658,19 @@ fn gateway_select_column_widths(gateways: &[GatewayMetadata]) -> (usize, usize,
658658
659659 ( name_width, endpoint_width, type_width)
660660}
661+ fn listed_gateway_source ( name : & str ) -> Result < GatewayMetadataSource > {
662+ gateway_metadata_source ( name) ?
663+ . ok_or_else ( || miette ! ( "gateway '{name}' disappeared while resolving its config source" ) )
664+ }
665+
666+ fn listed_gateways_with_source (
667+ gateways : & [ GatewayMetadata ] ,
668+ ) -> Result < Vec < ( & GatewayMetadata , GatewayMetadataSource ) > > {
669+ gateways
670+ . iter ( )
671+ . map ( |gateway| Ok ( ( gateway, listed_gateway_source ( & gateway. name ) ?) ) )
672+ . collect ( )
673+ }
661674
662675fn gateway_type_label ( gateway : & GatewayMetadata ) -> & ' static str {
663676 match gateway. auth_mode . as_deref ( ) {
@@ -1291,13 +1304,14 @@ pub fn gateway_logout(name: &str) -> Result<()> {
12911304/// List all registered gateways.
12921305pub fn gateway_list ( gateway_flag : & Option < String > , output : & str ) -> Result < ( ) > {
12931306 let gateways = list_gateways ( ) ?;
1307+ let gateway_sources = listed_gateways_with_source ( & gateways) ?;
12941308 let active = gateway_flag. clone ( ) . or_else ( load_active_gateway) ;
12951309
12961310 match output {
12971311 "json" => {
1298- let items: Vec < serde_json:: Value > = gateways
1312+ let items: Vec < serde_json:: Value > = gateway_sources
12991313 . iter ( )
1300- . map ( |g | gateway_to_json ( g , & active) )
1314+ . map ( |( gateway , source ) | gateway_to_json ( gateway , & active, * source ) )
13011315 . collect ( ) ;
13021316 println ! (
13031317 "{}" ,
@@ -1306,9 +1320,9 @@ pub fn gateway_list(gateway_flag: &Option<String>, output: &str) -> Result<()> {
13061320 return Ok ( ( ) ) ;
13071321 }
13081322 "yaml" => {
1309- let items: Vec < serde_json:: Value > = gateways
1323+ let items: Vec < serde_json:: Value > = gateway_sources
13101324 . iter ( )
1311- . map ( |g | gateway_to_json ( g , & active) )
1325+ . map ( |( gateway , source ) | gateway_to_json ( gateway , & active, * source ) )
13121326 . collect ( ) ;
13131327 print ! ( "{}" , serde_yml:: to_string( & items) . into_diagnostic( ) ?) ;
13141328 return Ok ( ( ) ) ;
@@ -1327,7 +1341,6 @@ pub fn gateway_list(gateway_flag: &Option<String>, output: &str) -> Result<()> {
13271341 return Ok ( ( ) ) ;
13281342 }
13291343
1330- // Calculate column widths
13311344 let name_width = gateways
13321345 . iter ( )
13331346 . map ( |g| g. name . len ( ) )
@@ -1346,25 +1359,33 @@ pub fn gateway_list(gateway_flag: &Option<String>, output: &str) -> Result<()> {
13461359 . max ( )
13471360 . unwrap_or ( 4 )
13481361 . max ( 4 ) ;
1362+ let source_width = gateway_sources
1363+ . iter ( )
1364+ . map ( |( _, source) | source. label ( ) . len ( ) )
1365+ . max ( )
1366+ . unwrap_or ( 6 )
1367+ . max ( 6 ) ;
13491368
1350- // Print header
13511369 println ! (
1352- " {:<name_width$} {:<endpoint_width$} {:<type_width$} {}" ,
1370+ " {:<name_width$} {:<endpoint_width$} {:<type_width$} {:<source_width$} { }" ,
13531371 "NAME" . bold( ) ,
13541372 "ENDPOINT" . bold( ) ,
13551373 "TYPE" . bold( ) ,
1374+ "SOURCE" . bold( ) ,
13561375 "AUTH" . bold( ) ,
13571376 ) ;
13581377
1359- // Print rows
1360- for gateway in & gateways {
1378+ for ( gateway, source) in gateway_sources {
13611379 let is_active = active. as_deref ( ) == Some ( & gateway. name ) ;
13621380 let marker = if is_active { "*" } else { " " } ;
13631381 let gw_type = gateway_type_label ( gateway) ;
13641382 let gw_auth = gateway_auth_label ( gateway) ;
13651383 let line = format ! (
1366- "{marker} {:<name_width$} {:<endpoint_width$} {:<type_width$} {gw_auth}" ,
1367- gateway. name, gateway. gateway_endpoint, gw_type,
1384+ "{marker} {:<name_width$} {:<endpoint_width$} {:<type_width$} {:<source_width$} {gw_auth}" ,
1385+ gateway. name,
1386+ gateway. gateway_endpoint,
1387+ gw_type,
1388+ source. label( ) ,
13681389 ) ;
13691390 if is_active {
13701391 println ! ( "{}" , line. green( ) ) ;
@@ -1376,11 +1397,16 @@ pub fn gateway_list(gateway_flag: &Option<String>, output: &str) -> Result<()> {
13761397 Ok ( ( ) )
13771398}
13781399
1379- fn gateway_to_json ( gateway : & GatewayMetadata , active : & Option < String > ) -> serde_json:: Value {
1400+ fn gateway_to_json (
1401+ gateway : & GatewayMetadata ,
1402+ active : & Option < String > ,
1403+ source : GatewayMetadataSource ,
1404+ ) -> serde_json:: Value {
13801405 serde_json:: json!( {
13811406 "name" : gateway. name,
13821407 "endpoint" : gateway. gateway_endpoint,
13831408 "type" : gateway_type_label( gateway) ,
1409+ "source" : source. label( ) ,
13841410 "auth" : gateway_auth_label( gateway) ,
13851411 "active" : active. as_deref( ) == Some ( & gateway. name) ,
13861412 } )
@@ -1458,11 +1484,20 @@ fn remove_gateway_registration(name: &str) {
14581484
14591485/// Remove a local gateway registration without touching the gateway service.
14601486pub fn gateway_remove ( name : & str ) -> Result < ( ) > {
1461- if get_gateway_metadata ( name) . is_none ( ) {
1462- return Err ( miette:: miette!(
1463- "No gateway metadata found for '{name}'.\n \
1464- List available gateways: openshell gateway select"
1465- ) ) ;
1487+ match gateway_metadata_source ( name) ? {
1488+ Some ( GatewayMetadataSource :: User ) => { }
1489+ Some ( GatewayMetadataSource :: System ) => {
1490+ return Err ( miette:: miette!(
1491+ "Gateway registration '{name}' is installed by the system and cannot be removed from user config.\n \
1492+ Register a per-user gateway with the same name to override it, or select another gateway."
1493+ ) ) ;
1494+ }
1495+ None => {
1496+ return Err ( miette:: miette!(
1497+ "No gateway metadata found for '{name}'.\n \
1498+ List available gateways: openshell gateway select"
1499+ ) ) ;
1500+ }
14661501 }
14671502
14681503 remove_gateway_registration ( name) ;
@@ -6983,19 +7018,21 @@ mod tests {
69837018 ProvisioningDisplay , ProvisioningStep , TlsOptions , build_sandbox_resource_limits,
69847019 dockerfile_sources_supported_for_gateway, format_endpoint, format_gateway_select_header,
69857020 format_gateway_select_items, format_provider_attachment_table, gateway_add,
6986- gateway_auth_label, gateway_env_override_warning, gateway_select_with, gateway_type_label ,
6987- git_sync_files, http_health_check, image_requests_gpu, import_local_package_mtls_bundle ,
6988- inferred_provider_type , local_upload_path_exists , local_upload_path_is_symlink ,
6989- package_managed_tls_dirs , parse_cli_setting_value , parse_credential_expiry_cli_value ,
6990- parse_credential_expiry_pairs , parse_credential_pairs , plaintext_gateway_is_remote ,
6991- progress_step_from_metadata , provider_profile_allows_refresh_bootstrap ,
6992- provisioning_timeout_message , ready_false_condition_message , refresh_status_header ,
6993- refresh_status_row , resolve_from , sandbox_should_persist , service_expose_status_error ,
6994- service_url_for_gateway,
7021+ gateway_auth_label, gateway_env_override_warning, gateway_select_with, gateway_to_json ,
7022+ gateway_type_label , git_sync_files, http_health_check, image_requests_gpu,
7023+ import_local_package_mtls_bundle , inferred_provider_type , local_upload_path_exists ,
7024+ local_upload_path_is_symlink , package_managed_tls_dirs , parse_cli_setting_value ,
7025+ parse_credential_expiry_cli_value , parse_credential_expiry_pairs , parse_credential_pairs ,
7026+ plaintext_gateway_is_remote , progress_step_from_metadata ,
7027+ provider_profile_allows_refresh_bootstrap , provisioning_timeout_message ,
7028+ ready_false_condition_message , refresh_status_header , refresh_status_row , resolve_from ,
7029+ sandbox_should_persist , service_expose_status_error , service_url_for_gateway,
69957030 } ;
69967031 use crate :: TEST_ENV_LOCK ;
69977032 use hyper:: StatusCode ;
6998- use openshell_bootstrap:: { load_active_gateway, load_gateway_metadata, store_gateway_metadata} ;
7033+ use openshell_bootstrap:: {
7034+ GatewayMetadataSource , load_active_gateway, load_gateway_metadata, store_gateway_metadata,
7035+ } ;
69997036 use std:: fs;
70007037 use std:: io:: { Read , Write } ;
70017038 use std:: net:: TcpListener ;
@@ -7935,6 +7972,27 @@ mod tests {
79357972 assert ! ( items[ 1 ] . contains( "http://127.0.0.1:8080" ) ) ;
79367973 }
79377974
7975+ #[ test]
7976+ fn gateway_to_json_includes_config_source ( ) {
7977+ let gateway = GatewayMetadata {
7978+ name : "local-vm" . to_string ( ) ,
7979+ gateway_endpoint : "http://127.0.0.1:17670" . to_string ( ) ,
7980+ auth_mode : Some ( "plaintext" . to_string ( ) ) ,
7981+ ..Default :: default ( )
7982+ } ;
7983+
7984+ let json = gateway_to_json (
7985+ & gateway,
7986+ & Some ( "local-vm" . to_string ( ) ) ,
7987+ GatewayMetadataSource :: System ,
7988+ ) ;
7989+
7990+ assert_eq ! ( json[ "source" ] , "system" ) ;
7991+ assert_eq ! ( json[ "type" ] , "local" ) ;
7992+ assert_eq ! ( json[ "auth" ] , "plaintext" ) ;
7993+ assert_eq ! ( json[ "active" ] , true ) ;
7994+ }
7995+
79387996 #[ test]
79397997 fn gateway_auth_label_defaults_https_gateways_to_mtls ( ) {
79407998 let gateway = GatewayMetadata {
0 commit comments