@@ -2670,7 +2670,7 @@ fn prepare_startup_configuration(
26702670 "Effective configuration admission rejected"
26712671 ) ) ;
26722672 }
2673- if provider. readiness_reason != ProviderReadinessReason :: Unspecified {
2673+ if ! provider_environment_is_installable ( provider. readiness_reason ) {
26742674 return Err ( miette:: miette!(
26752675 "Provider credentials are not ready for installation"
26762676 ) ) ;
@@ -2686,6 +2686,18 @@ fn prepare_startup_configuration(
26862686 Ok ( ( engine, process_policy, credentials) )
26872687}
26882688
2689+ /// Whether the provider response contains a complete environment snapshot that
2690+ /// can be installed. Withheld and expired credentials are intentionally absent,
2691+ /// so those responses remain valid fail-closed snapshots.
2692+ fn provider_environment_is_installable ( reason : ProviderReadinessReason ) -> bool {
2693+ matches ! (
2694+ reason,
2695+ ProviderReadinessReason :: Unspecified
2696+ | ProviderReadinessReason :: CredentialsWithheld
2697+ | ProviderReadinessReason :: CredentialExpired
2698+ )
2699+ }
2700+
26892701fn prepare_provider_environment (
26902702 provider : & openshell_core:: grpc_client:: ProviderEnvironmentResult ,
26912703) -> Result < ProviderCredentialState > {
@@ -4296,7 +4308,7 @@ async fn run_policy_poll_loop_with_client<C: PolicyGatewayClient>(
42964308 {
42974309 Ok ( provider)
42984310 if EnvironmentIdentity :: from_environment ( & provider) == desired_identity
4299- && provider. readiness_reason == ProviderReadinessReason :: Unspecified =>
4311+ && provider_environment_is_installable ( provider. readiness_reason ) =>
43004312 {
43014313 provider
43024314 }
@@ -5562,6 +5574,49 @@ network_policies:
55625574 assert_eq ! ( credentials. revision( ) , 10 ) ;
55635575 }
55645576
5577+ #[ test]
5578+ fn startup_configuration_accepts_fail_closed_provider_environment ( ) {
5579+ let policy = proto_policy_fixture ( ) ;
5580+ let mut snapshot = settings_poll_result (
5581+ Some ( policy. clone ( ) ) ,
5582+ 1 ,
5583+ openshell_core:: proto:: PolicySource :: Sandbox ,
5584+ ) ;
5585+ snapshot. provider_env_revision = 10 ;
5586+
5587+ for reason in [
5588+ ProviderReadinessReason :: CredentialsWithheld ,
5589+ ProviderReadinessReason :: CredentialExpired ,
5590+ ] {
5591+ let mut provider = startup_provider ( 10 ) ;
5592+ provider. readiness_reason = reason;
5593+ provider
5594+ . environment
5595+ . insert ( "PROJECT_ID" . to_string ( ) , "example-project" . to_string ( ) ) ;
5596+ provider
5597+ . non_secret_environment_keys
5598+ . push ( "PROJECT_ID" . to_string ( ) ) ;
5599+ let ( _, _, credentials) = prepare_startup_configuration ( & snapshot, & policy, & provider)
5600+ . expect ( "withheld credentials preserve fail-closed startup" ) ;
5601+ assert_eq ! ( credentials. revision( ) , 10 ) ;
5602+ assert ! ( credentials. snapshot( ) . child_env. contains_key( "PROJECT_ID" ) ) ;
5603+ }
5604+ }
5605+
5606+ #[ test]
5607+ fn startup_configuration_rejects_provider_installation_failure ( ) {
5608+ let policy = proto_policy_fixture ( ) ;
5609+ let snapshot = settings_poll_result (
5610+ Some ( policy. clone ( ) ) ,
5611+ 1 ,
5612+ openshell_core:: proto:: PolicySource :: Sandbox ,
5613+ ) ;
5614+ let mut provider = startup_provider ( 0 ) ;
5615+ provider. readiness_reason = ProviderReadinessReason :: CredentialInstallFailed ;
5616+
5617+ assert ! ( prepare_startup_configuration( & snapshot, & policy, & provider) . is_err( ) ) ;
5618+ }
5619+
55655620 #[ test]
55665621 fn startup_configuration_revalidates_on_restart_and_accepts_repair ( ) {
55675622 let policy = proto_policy_fixture ( ) ;
@@ -6153,6 +6208,69 @@ network_policies:
61536208 let _ = task. await ;
61546209 }
61556210
6211+ #[ tokio:: test]
6212+ async fn provider_poll_installs_fail_closed_environment_and_acknowledges_policy ( ) {
6213+ let policy = proto_policy_fixture ( ) ;
6214+ let initial = settings_poll_result (
6215+ Some ( policy. clone ( ) ) ,
6216+ 1 ,
6217+ openshell_core:: proto:: PolicySource :: Sandbox ,
6218+ ) ;
6219+ let engine = Arc :: new ( OpaEngine :: from_proto ( & policy) . unwrap ( ) ) ;
6220+ let ctx = policy_poll_test_context (
6221+ engine,
6222+ LoadedPolicyOrigin :: Gateway {
6223+ revision : Some ( LoadedPolicyRevision :: from_snapshot ( & initial) ) ,
6224+ has_last_valid_policy : true ,
6225+ } ,
6226+ default_middleware_connector ( ) ,
6227+ ) ;
6228+ let credentials = ctx. provider_credentials . clone ( ) ;
6229+ let ( policy_gateway, polls, mut reports) = scripted_policy_gateway ( ) ;
6230+ let ( requests, mut received) = tokio:: sync:: mpsc:: unbounded_channel ( ) ;
6231+ polls. send ( initial) . unwrap ( ) ;
6232+ let task = tokio:: spawn ( run_policy_poll_loop_with_client (
6233+ ctx,
6234+ ScriptedProviderGateway {
6235+ policy : policy_gateway,
6236+ requests,
6237+ } ,
6238+ ) ) ;
6239+ expect_policy_report ( & mut reports, 1 ) . await ;
6240+
6241+ let mut changed = settings_poll_result (
6242+ Some ( policy) ,
6243+ 2 ,
6244+ openshell_core:: proto:: PolicySource :: Sandbox ,
6245+ ) ;
6246+ changed. provider_env_revision = 1 ;
6247+ polls. send ( changed) . unwrap ( ) ;
6248+ let response = timeout ( Duration :: from_secs ( 1 ) , received. recv ( ) )
6249+ . await
6250+ . expect ( "provider refresh requested" )
6251+ . expect ( "poll loop active" ) ;
6252+ let mut withheld = static_provider_environment ( 1 , None ) ;
6253+ withheld. policy_hash = "hash-v2" . to_string ( ) ;
6254+ withheld. readiness_reason = ProviderReadinessReason :: CredentialsWithheld ;
6255+ withheld
6256+ . environment
6257+ . insert ( "PROJECT_ID" . to_string ( ) , "example-project" . to_string ( ) ) ;
6258+ withheld
6259+ . non_secret_environment_keys
6260+ . push ( "PROJECT_ID" . to_string ( ) ) ;
6261+ assert ! ( response. send( Ok ( withheld) ) . is_ok( ) ) ;
6262+
6263+ expect_policy_report ( & mut reports, 2 ) . await ;
6264+ assert_eq ! ( credentials. revision( ) , 1 ) ;
6265+ assert ! (
6266+ credentials. snapshot( ) . child_env. contains_key( "PROJECT_ID" ) ,
6267+ "the reduced snapshot retains non-secret provider configuration"
6268+ ) ;
6269+
6270+ task. abort ( ) ;
6271+ let _ = task. await ;
6272+ }
6273+
61566274 #[ tokio:: test]
61576275 async fn provider_poll_preserves_static_references_across_rotation_failure_and_detach ( ) {
61586276 let engine = Arc :: new ( OpaEngine :: from_proto ( & proto_policy_fixture ( ) ) . unwrap ( ) ) ;
0 commit comments