@@ -1560,11 +1560,23 @@ where
15601560 sandbox. object_id ( ) . to_string ( )
15611561 } ;
15621562
1563- let config = client
1563+ let config_result = client
15641564 . get_sandbox_config ( GetSandboxConfigRequest { sandbox_id } )
1565- . await
1566- . into_diagnostic ( ) ?
1567- . into_inner ( ) ;
1565+ . await ;
1566+ let config = match config_result {
1567+ Ok ( response) => response. into_inner ( ) ,
1568+ Err ( _) if !policy_only && configuration_failure_message ( & sandbox) . is_some ( ) => {
1569+ // An invalid desired policy must not hide the status needed to
1570+ // repair it. Keep payload-only reads strict.
1571+ GetSandboxConfigResponse {
1572+ configuration_error : configuration_failure_message ( & sandbox)
1573+ . unwrap_or_default ( )
1574+ . to_string ( ) ,
1575+ ..Default :: default ( )
1576+ }
1577+ }
1578+ Err ( error) => return Err ( error) . into_diagnostic ( ) ,
1579+ } ;
15681580
15691581 if policy_only {
15701582 let Some ( ref policy) = config. policy else {
@@ -1598,6 +1610,22 @@ where
15981610 println ! ( " {} {}" , "Id:" . dimmed( ) , id) ;
15991611 println ! ( " {} {}" , "Name:" . dimmed( ) , name) ;
16001612 println ! ( " {} {}" , "Phase:" . dimmed( ) , phase_name( sandbox. phase( ) ) ) ;
1613+ if let Some ( status) = sandbox. status . as_ref ( ) {
1614+ for condition in & status. conditions {
1615+ if matches ! (
1616+ condition. r#type. as_str( ) ,
1617+ "ConfigurationReady" | "DesiredConfigurationReady"
1618+ ) && condition. status . eq_ignore_ascii_case ( "false" )
1619+ {
1620+ println ! (
1621+ " {} {}: {}" ,
1622+ "Configuration:" . dimmed( ) ,
1623+ condition. reason,
1624+ condition. message
1625+ ) ;
1626+ }
1627+ }
1628+ }
16011629 if let Some ( exit_code) = sandbox. status . as_ref ( ) . and_then ( |status| status. exit_code ) {
16021630 println ! ( " {} {}" , "Exit Code:" . dimmed( ) , exit_code) ;
16031631 }
@@ -2460,7 +2488,19 @@ pub async fn sandbox_list(
24602488 Ok ( ( ) )
24612489}
24622490
2491+ fn configuration_failure_message ( sandbox : & Sandbox ) -> Option < & str > {
2492+ sandbox
2493+ . status
2494+ . as_ref ( ) ?
2495+ . configuration_admission
2496+ . as_ref ( )
2497+ . map ( |admission| admission. error . as_str ( ) )
2498+ . filter ( |message| !message. is_empty ( ) )
2499+ }
2500+
24632501fn sandbox_to_json ( sandbox : & Sandbox ) -> serde_json:: Value {
2502+ use openshell_core:: proto:: ConfigurationAdmissionState ;
2503+
24642504 let meta = sandbox. metadata . as_ref ( ) ;
24652505 let labels = meta. map_or_else ( || serde_json:: json!( { } ) , |m| serde_json:: json!( m. labels) ) ;
24662506 let annotations = meta. map_or_else (
@@ -2500,6 +2540,25 @@ fn sandbox_to_json(sandbox: &Sandbox) -> serde_json::Value {
25002540 } )
25012541 . collect :: < Vec < _ > > ( )
25022542 } ) ;
2543+ let admission = sandbox
2544+ . status
2545+ . as_ref ( )
2546+ . and_then ( |status| status. configuration_admission . as_ref ( ) )
2547+ . map ( |admission| {
2548+ serde_json:: json!( {
2549+ "state" : match ConfigurationAdmissionState :: try_from( admission. state) {
2550+ Ok ( ConfigurationAdmissionState :: Pending ) => "pending" ,
2551+ Ok ( ConfigurationAdmissionState :: Accepted ) => "accepted" ,
2552+ Ok ( ConfigurationAdmissionState :: Rejected ) => "rejected" ,
2553+ _ => "unknown" ,
2554+ } ,
2555+ "error" : admission. error,
2556+ "policy_version" : admission. policy_version,
2557+ "policy_hash" : admission. policy_hash,
2558+ "config_revision" : admission. config_revision,
2559+ "provider_env_revision" : admission. provider_env_revision,
2560+ } )
2561+ } ) ;
25032562 serde_json:: json!( {
25042563 "id" : sandbox. object_id( ) ,
25052564 "name" : sandbox. object_name( ) ,
@@ -2513,6 +2572,7 @@ fn sandbox_to_json(sandbox: &Sandbox) -> serde_json::Value {
25132572 "exit_code" : sandbox. status. as_ref( ) . and_then( |status| status. exit_code) ,
25142573 "conditions" : conditions,
25152574 "endpoint_statuses" : endpoint_statuses,
2575+ "configuration_admission" : admission,
25162576 "created_from_workload_template" : created_from_workload_template,
25172577 } )
25182578}
@@ -7348,6 +7408,45 @@ mod tests {
73487408 assert_eq ! ( json[ "resources" ] [ "gpu" ] , 2 ) ;
73497409 }
73507410
7411+ #[ test]
7412+ fn sandbox_json_exposes_repair_diagnostic_and_accepted_generation ( ) {
7413+ use openshell_core:: proto:: { ConfigurationAdmissionState , SandboxConfigurationAdmission } ;
7414+
7415+ let mut sandbox = Sandbox :: default ( ) ;
7416+ sandbox. set_phase ( SandboxPhase :: Provisioning as i32 ) ;
7417+ let status = sandbox. status . as_mut ( ) . unwrap ( ) ;
7418+ status. configuration_admission = Some ( SandboxConfigurationAdmission {
7419+ state : ConfigurationAdmissionState :: Rejected as i32 ,
7420+ error : "rule image_api requires L7 inspection" . to_string ( ) ,
7421+ policy_hash : "candidate-hash" . to_string ( ) ,
7422+ ..Default :: default ( )
7423+ } ) ;
7424+ status. conditions . push ( SandboxCondition {
7425+ r#type : "ConfigurationReady" . to_string ( ) ,
7426+ status : "False" . to_string ( ) ,
7427+ reason : "ConfigurationInvalid" . to_string ( ) ,
7428+ message : "rule image_api requires L7 inspection" . to_string ( ) ,
7429+ ..Default :: default ( )
7430+ } ) ;
7431+ let json = super :: sandbox_to_json ( & sandbox) ;
7432+ assert_eq ! ( json[ "configuration_admission" ] [ "state" ] , "rejected" ) ;
7433+ assert_eq ! ( json[ "conditions" ] [ 0 ] [ "reason" ] , "ConfigurationInvalid" ) ;
7434+ assert_eq ! (
7435+ super :: configuration_failure_message( & sandbox) ,
7436+ Some ( "rule image_api requires L7 inspection" )
7437+ ) ;
7438+ sandbox
7439+ . status
7440+ . as_mut ( )
7441+ . unwrap ( )
7442+ . configuration_admission
7443+ . as_mut ( )
7444+ . unwrap ( )
7445+ . error
7446+ . clear ( ) ;
7447+ assert_eq ! ( super :: configuration_failure_message( & sandbox) , None ) ;
7448+ }
7449+
73517450 #[ test]
73527451 fn sandbox_detail_to_json_includes_policy_fields ( ) {
73537452 let mut sandbox = Sandbox {
0 commit comments