@@ -25,18 +25,19 @@ use openshell_bootstrap::{
2525} ;
2626use openshell_core:: proto:: ProviderProfileCategory ;
2727use openshell_core:: proto:: {
28- ApproveAllDraftChunksRequest , ApproveDraftChunkRequest , ClearDraftChunksRequest ,
29- CreateProviderRequest , CreateSandboxRequest , DeleteProviderProfileRequest ,
30- DeleteProviderRequest , DeleteSandboxRequest , ExecSandboxRequest , GetClusterInferenceRequest ,
28+ ApproveAllDraftChunksRequest , ApproveDraftChunkRequest , AttachSandboxProviderRequest ,
29+ ClearDraftChunksRequest , CreateProviderRequest , CreateSandboxRequest ,
30+ DeleteProviderProfileRequest , DeleteProviderRequest , DeleteSandboxRequest ,
31+ DetachSandboxProviderRequest , ExecSandboxRequest , GetClusterInferenceRequest ,
3132 GetDraftHistoryRequest , GetDraftPolicyRequest , GetGatewayConfigRequest ,
3233 GetProviderProfileRequest , GetProviderRequest , GetSandboxConfigRequest , GetSandboxLogsRequest ,
3334 GetSandboxPolicyStatusRequest , GetSandboxRequest , HealthRequest , ImportProviderProfilesRequest ,
3435 LintProviderProfilesRequest , ListProviderProfilesRequest , ListProvidersRequest ,
35- ListSandboxPoliciesRequest , ListSandboxesRequest , PolicySource , PolicyStatus , Provider ,
36- ProviderProfile , ProviderProfileDiagnostic , ProviderProfileImportItem , RejectDraftChunkRequest ,
37- Sandbox , SandboxPhase , SandboxPolicy , SandboxSpec , SandboxTemplate , SetClusterInferenceRequest ,
38- SettingScope , SettingValue , UpdateConfigRequest , UpdateProviderRequest , WatchSandboxRequest ,
39- exec_sandbox_event, setting_value,
36+ ListSandboxPoliciesRequest , ListSandboxProvidersRequest , ListSandboxesRequest , PolicySource ,
37+ PolicyStatus , Provider , ProviderProfile , ProviderProfileDiagnostic , ProviderProfileImportItem ,
38+ RejectDraftChunkRequest , Sandbox , SandboxPhase , SandboxPolicy , SandboxSpec , SandboxTemplate ,
39+ SetClusterInferenceRequest , SettingScope , SettingValue , UpdateConfigRequest ,
40+ UpdateProviderRequest , WatchSandboxRequest , exec_sandbox_event, setting_value,
4041} ;
4142use openshell_core:: settings:: { self , SettingValueKind } ;
4243use openshell_core:: { ObjectId , ObjectName } ;
@@ -2512,6 +2513,143 @@ pub async fn sandbox_list(
25122513 Ok ( ( ) )
25132514}
25142515
2516+ pub async fn sandbox_provider_list ( server : & str , name : & str , tls : & TlsOptions ) -> Result < ( ) > {
2517+ let mut client = grpc_client ( server, tls) . await ?;
2518+ let response = client
2519+ . list_sandbox_providers ( ListSandboxProvidersRequest {
2520+ sandbox_name : name. to_string ( ) ,
2521+ } )
2522+ . await
2523+ . into_diagnostic ( ) ?;
2524+ let providers = response. into_inner ( ) . providers ;
2525+
2526+ if providers. is_empty ( ) {
2527+ println ! ( "No providers attached to sandbox {name}." ) ;
2528+ return Ok ( ( ) ) ;
2529+ }
2530+
2531+ print_provider_attachment_table ( & providers) ;
2532+ Ok ( ( ) )
2533+ }
2534+
2535+ pub async fn sandbox_provider_attach (
2536+ server : & str ,
2537+ name : & str ,
2538+ provider : & str ,
2539+ tls : & TlsOptions ,
2540+ ) -> Result < ( ) > {
2541+ let mut client = grpc_client ( server, tls) . await ?;
2542+ let response = client
2543+ . attach_sandbox_provider ( AttachSandboxProviderRequest {
2544+ sandbox_name : name. to_string ( ) ,
2545+ provider_name : provider. to_string ( ) ,
2546+ } )
2547+ . await
2548+ . into_diagnostic ( ) ?
2549+ . into_inner ( ) ;
2550+
2551+ if response. attached {
2552+ println ! (
2553+ "{} Attached provider {} to sandbox {}" ,
2554+ "✓" . green( ) . bold( ) ,
2555+ provider,
2556+ name
2557+ ) ;
2558+ } else {
2559+ println ! ( "Provider {provider} is already attached to sandbox {name}." ) ;
2560+ }
2561+ Ok ( ( ) )
2562+ }
2563+
2564+ pub async fn sandbox_provider_detach (
2565+ server : & str ,
2566+ name : & str ,
2567+ provider : & str ,
2568+ tls : & TlsOptions ,
2569+ ) -> Result < ( ) > {
2570+ let mut client = grpc_client ( server, tls) . await ?;
2571+ let response = client
2572+ . detach_sandbox_provider ( DetachSandboxProviderRequest {
2573+ sandbox_name : name. to_string ( ) ,
2574+ provider_name : provider. to_string ( ) ,
2575+ } )
2576+ . await
2577+ . into_diagnostic ( ) ?
2578+ . into_inner ( ) ;
2579+
2580+ if response. detached {
2581+ println ! (
2582+ "{} Detached provider {} from sandbox {}" ,
2583+ "✓" . green( ) . bold( ) ,
2584+ provider,
2585+ name
2586+ ) ;
2587+ } else {
2588+ println ! ( "Provider {provider} was not attached to sandbox {name}." ) ;
2589+ }
2590+ Ok ( ( ) )
2591+ }
2592+
2593+ fn print_provider_attachment_table ( providers : & [ Provider ] ) {
2594+ print ! ( "{}" , format_provider_attachment_table( providers, true ) ) ;
2595+ }
2596+
2597+ fn format_provider_attachment_table ( providers : & [ Provider ] , color : bool ) -> String {
2598+ use std:: fmt:: Write as _;
2599+
2600+ let name_width = providers
2601+ . iter ( )
2602+ . map ( |provider| provider. object_name ( ) . len ( ) )
2603+ . max ( )
2604+ . unwrap_or ( 4 )
2605+ . max ( 4 ) ;
2606+ let type_width = providers
2607+ . iter ( )
2608+ . map ( |provider| provider. r#type . len ( ) )
2609+ . max ( )
2610+ . unwrap_or ( 4 )
2611+ . max ( 4 ) ;
2612+
2613+ let name_header = if color {
2614+ "NAME" . bold ( ) . to_string ( )
2615+ } else {
2616+ "NAME" . to_string ( )
2617+ } ;
2618+ let type_header = if color {
2619+ "TYPE" . bold ( ) . to_string ( )
2620+ } else {
2621+ "TYPE" . to_string ( )
2622+ } ;
2623+ let credential_keys_header = if color {
2624+ "CREDENTIAL_KEYS" . bold ( ) . to_string ( )
2625+ } else {
2626+ "CREDENTIAL_KEYS" . to_string ( )
2627+ } ;
2628+ let config_keys_header = if color {
2629+ "CONFIG_KEYS" . bold ( ) . to_string ( )
2630+ } else {
2631+ "CONFIG_KEYS" . to_string ( )
2632+ } ;
2633+
2634+ let mut output = String :: new ( ) ;
2635+ let _ = writeln ! (
2636+ output,
2637+ "{name_header:<name_width$} {type_header:<type_width$} {credential_keys_header:<16} {config_keys_header}" ,
2638+ ) ;
2639+
2640+ for provider in providers {
2641+ let provider_name = provider. object_name ( ) ;
2642+ let provider_type = & provider. r#type ;
2643+ let credential_keys = provider. credentials . len ( ) ;
2644+ let config_keys = provider. config . len ( ) ;
2645+ let _ = writeln ! (
2646+ output,
2647+ "{provider_name:<name_width$} {provider_type:<type_width$} {credential_keys:<16} {config_keys}" ,
2648+ ) ;
2649+ }
2650+ output
2651+ }
2652+
25152653/// Delete a sandbox by name, or all sandboxes when `all` is true.
25162654pub async fn sandbox_delete (
25172655 server : & str ,
@@ -5251,11 +5389,12 @@ fn format_timestamp_ms(ms: i64) -> String {
52515389mod tests {
52525390 use super :: {
52535391 TlsOptions , dockerfile_sources_supported_for_gateway, format_gateway_select_header,
5254- format_gateway_select_items, gateway_add, gateway_auth_label, gateway_env_override_warning,
5255- gateway_select_with, gateway_type_label, git_sync_files, http_health_check,
5256- image_requests_gpu, inferred_provider_type, parse_cli_setting_value,
5257- parse_credential_pairs, plaintext_gateway_is_remote, provisioning_timeout_message,
5258- ready_false_condition_message, resolve_from, sandbox_should_persist,
5392+ format_gateway_select_items, format_provider_attachment_table, gateway_add,
5393+ 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,
52595398 } ;
52605399 use crate :: TEST_ENV_LOCK ;
52615400 use hyper:: StatusCode ;
@@ -5268,7 +5407,9 @@ mod tests {
52685407 use std:: thread;
52695408
52705409 use openshell_bootstrap:: GatewayMetadata ;
5271- use openshell_core:: proto:: { SandboxCondition , SandboxStatus } ;
5410+ use openshell_core:: proto:: {
5411+ Provider , SandboxCondition , SandboxStatus , datamodel:: v1:: ObjectMeta ,
5412+ } ;
52725413
52735414 struct EnvVarGuard {
52745415 key : & ' static str ,
@@ -5371,6 +5512,40 @@ mod tests {
53715512 ) ) ;
53725513 }
53735514
5515+ #[ test]
5516+ fn provider_attachment_table_formats_provider_counts ( ) {
5517+ let output = format_provider_attachment_table (
5518+ & [ Provider {
5519+ metadata : Some ( ObjectMeta {
5520+ name : "work-custom" . to_string ( ) ,
5521+ ..Default :: default ( )
5522+ } ) ,
5523+ r#type : "custom-api" . to_string ( ) ,
5524+ credentials : [
5525+ ( "CUSTOM_API_KEY" . to_string ( ) , "REDACTED" . to_string ( ) ) ,
5526+ ( "CUSTOM_API_SECRET" . to_string ( ) , "REDACTED" . to_string ( ) ) ,
5527+ ]
5528+ . into_iter ( )
5529+ . collect ( ) ,
5530+ config : std:: iter:: once ( (
5531+ "BASE_URL" . to_string ( ) ,
5532+ "https://api.custom.example" . to_string ( ) ,
5533+ ) )
5534+ . collect ( ) ,
5535+ } ] ,
5536+ false ,
5537+ ) ;
5538+
5539+ assert ! ( output. contains( "NAME" ) ) ;
5540+ assert ! ( output. contains( "TYPE" ) ) ;
5541+ assert ! ( output. contains( "CREDENTIAL_KEYS" ) ) ;
5542+ assert ! ( output. contains( "CONFIG_KEYS" ) ) ;
5543+ assert ! ( output. contains( "work-custom" ) ) ;
5544+ assert ! ( output. contains( "custom-api" ) ) ;
5545+ assert ! ( output. contains( '2' ) ) ;
5546+ assert ! ( output. contains( '1' ) ) ;
5547+ }
5548+
53745549 #[ cfg( feature = "dev-settings" ) ]
53755550 #[ test]
53765551 fn parse_cli_setting_value_parses_bool_aliases ( ) {
0 commit comments