@@ -1510,11 +1510,23 @@ pub async fn sandbox_get(
15101510 sandbox. object_id ( ) . to_string ( )
15111511 } ;
15121512
1513- let config = client
1513+ let config_result = client
15141514 . get_sandbox_config ( GetSandboxConfigRequest { sandbox_id } )
1515- . await
1516- . into_diagnostic ( ) ?
1517- . into_inner ( ) ;
1515+ . await ;
1516+ let config = match config_result {
1517+ Ok ( response) => response. into_inner ( ) ,
1518+ Err ( _) if !policy_only && configuration_failure_message ( & sandbox) . is_some ( ) => {
1519+ // An invalid desired policy must not hide the status needed to
1520+ // repair it. Keep payload-only reads strict.
1521+ GetSandboxConfigResponse {
1522+ configuration_error : configuration_failure_message ( & sandbox)
1523+ . unwrap_or_default ( )
1524+ . to_string ( ) ,
1525+ ..Default :: default ( )
1526+ }
1527+ }
1528+ Err ( error) => return Err ( error) . into_diagnostic ( ) ,
1529+ } ;
15181530
15191531 if policy_only {
15201532 let Some ( ref policy) = config. policy else {
@@ -1548,6 +1560,22 @@ pub async fn sandbox_get(
15481560 println ! ( " {} {}" , "Id:" . dimmed( ) , id) ;
15491561 println ! ( " {} {}" , "Name:" . dimmed( ) , name) ;
15501562 println ! ( " {} {}" , "Phase:" . dimmed( ) , phase_name( sandbox. phase( ) ) ) ;
1563+ if let Some ( status) = sandbox. status . as_ref ( ) {
1564+ for condition in & status. conditions {
1565+ if matches ! (
1566+ condition. r#type. as_str( ) ,
1567+ "ConfigurationReady" | "DesiredConfigurationReady"
1568+ ) && condition. status . eq_ignore_ascii_case ( "false" )
1569+ {
1570+ println ! (
1571+ " {} {}: {}" ,
1572+ "Configuration:" . dimmed( ) ,
1573+ condition. reason,
1574+ condition. message
1575+ ) ;
1576+ }
1577+ }
1578+ }
15511579 if let Some ( exit_code) = sandbox. status . as_ref ( ) . and_then ( |status| status. exit_code ) {
15521580 println ! ( " {} {}" , "Exit Code:" . dimmed( ) , exit_code) ;
15531581 }
@@ -2375,7 +2403,19 @@ pub async fn sandbox_list(
23752403 Ok ( ( ) )
23762404}
23772405
2406+ fn configuration_failure_message ( sandbox : & Sandbox ) -> Option < & str > {
2407+ sandbox
2408+ . status
2409+ . as_ref ( ) ?
2410+ . configuration_admission
2411+ . as_ref ( )
2412+ . map ( |admission| admission. error . as_str ( ) )
2413+ . filter ( |message| !message. is_empty ( ) )
2414+ }
2415+
23782416fn sandbox_to_json ( sandbox : & Sandbox ) -> serde_json:: Value {
2417+ use openshell_core:: proto:: ConfigurationAdmissionState ;
2418+
23792419 let meta = sandbox. metadata . as_ref ( ) ;
23802420 let labels = meta. map_or_else ( || serde_json:: json!( { } ) , |m| serde_json:: json!( m. labels) ) ;
23812421 let annotations = meta. map_or_else (
@@ -2392,6 +2432,39 @@ fn sandbox_to_json(sandbox: &Sandbox) -> serde_json::Value {
23922432 "resource_version" : provenance. resource_version,
23932433 } )
23942434 } ) ;
2435+ let conditions: Vec < _ > = sandbox
2436+ . status
2437+ . as_ref ( )
2438+ . into_iter ( )
2439+ . flat_map ( |status| & status. conditions )
2440+ . map ( |condition| {
2441+ serde_json:: json!( {
2442+ "type" : condition. r#type,
2443+ "status" : condition. status,
2444+ "reason" : condition. reason,
2445+ "message" : condition. message,
2446+ } )
2447+ } )
2448+ . collect ( ) ;
2449+ let admission = sandbox
2450+ . status
2451+ . as_ref ( )
2452+ . and_then ( |status| status. configuration_admission . as_ref ( ) )
2453+ . map ( |admission| {
2454+ serde_json:: json!( {
2455+ "state" : match ConfigurationAdmissionState :: try_from( admission. state) {
2456+ Ok ( ConfigurationAdmissionState :: Pending ) => "pending" ,
2457+ Ok ( ConfigurationAdmissionState :: Accepted ) => "accepted" ,
2458+ Ok ( ConfigurationAdmissionState :: Rejected ) => "rejected" ,
2459+ _ => "unknown" ,
2460+ } ,
2461+ "error" : admission. error,
2462+ "policy_version" : admission. policy_version,
2463+ "policy_hash" : admission. policy_hash,
2464+ "config_revision" : admission. config_revision,
2465+ "provider_env_revision" : admission. provider_env_revision,
2466+ } )
2467+ } ) ;
23952468 serde_json:: json!( {
23962469 "id" : sandbox. object_id( ) ,
23972470 "name" : sandbox. object_name( ) ,
@@ -2403,6 +2476,8 @@ fn sandbox_to_json(sandbox: &Sandbox) -> serde_json::Value {
24032476 "phase" : phase_name( sandbox. phase( ) ) ,
24042477 "current_policy_version" : sandbox. current_policy_version( ) ,
24052478 "exit_code" : sandbox. status. as_ref( ) . and_then( |status| status. exit_code) ,
2479+ "conditions" : conditions,
2480+ "configuration_admission" : admission,
24062481 "created_from_workload_template" : created_from_workload_template,
24072482 } )
24082483}
@@ -7081,6 +7156,45 @@ mod tests {
70817156 assert_eq ! ( json[ "resources" ] [ "gpu" ] , 2 ) ;
70827157 }
70837158
7159+ #[ test]
7160+ fn sandbox_json_exposes_repair_diagnostic_and_accepted_generation ( ) {
7161+ use openshell_core:: proto:: { ConfigurationAdmissionState , SandboxConfigurationAdmission } ;
7162+
7163+ let mut sandbox = Sandbox :: default ( ) ;
7164+ sandbox. set_phase ( SandboxPhase :: Provisioning as i32 ) ;
7165+ let status = sandbox. status . as_mut ( ) . unwrap ( ) ;
7166+ status. configuration_admission = Some ( SandboxConfigurationAdmission {
7167+ state : ConfigurationAdmissionState :: Rejected as i32 ,
7168+ error : "rule image_api requires L7 inspection" . to_string ( ) ,
7169+ policy_hash : "candidate-hash" . to_string ( ) ,
7170+ ..Default :: default ( )
7171+ } ) ;
7172+ status. conditions . push ( SandboxCondition {
7173+ r#type : "ConfigurationReady" . to_string ( ) ,
7174+ status : "False" . to_string ( ) ,
7175+ reason : "ConfigurationInvalid" . to_string ( ) ,
7176+ message : "rule image_api requires L7 inspection" . to_string ( ) ,
7177+ ..Default :: default ( )
7178+ } ) ;
7179+ let json = super :: sandbox_to_json ( & sandbox) ;
7180+ assert_eq ! ( json[ "configuration_admission" ] [ "state" ] , "rejected" ) ;
7181+ assert_eq ! ( json[ "conditions" ] [ 0 ] [ "reason" ] , "ConfigurationInvalid" ) ;
7182+ assert_eq ! (
7183+ super :: configuration_failure_message( & sandbox) ,
7184+ Some ( "rule image_api requires L7 inspection" )
7185+ ) ;
7186+ sandbox
7187+ . status
7188+ . as_mut ( )
7189+ . unwrap ( )
7190+ . configuration_admission
7191+ . as_mut ( )
7192+ . unwrap ( )
7193+ . error
7194+ . clear ( ) ;
7195+ assert_eq ! ( super :: configuration_failure_message( & sandbox) , None ) ;
7196+ }
7197+
70847198 #[ test]
70857199 fn sandbox_detail_to_json_includes_policy_fields ( ) {
70867200 let mut sandbox = Sandbox {
0 commit comments