@@ -3948,47 +3948,12 @@ async fn run_policy_poll_loop_with_client<C: PolicyGatewayClient>(
39483948 {
39493949 Ok ( env_result) => {
39503950 let provider_env_revision = env_result. provider_env_revision ;
3951- let install_result = ctx. provider_credentials . install_bound_environment (
3952- provider_env_revision,
3953- env_result. environment ,
3954- env_result. credential_expires_at_ms ,
3955- env_result. dynamic_credentials ,
3956- env_result. static_credential_bindings ,
3957- env_result. non_secret_environment_keys ,
3958- ) ;
3959- if let Err ( error) = install_result {
3960- ocsf_emit ! (
3961- ConfigStateChangeBuilder :: new( ocsf_ctx( ) )
3962- . severity( SeverityId :: High )
3963- . status( StatusId :: Failure )
3964- . state( StateId :: Disabled , "fail_closed" )
3965- . message( format!(
3966- "Rejected provider environment refresh; static provider credentials were revoked; fetched dynamic token grants remain active: {error}"
3967- ) )
3968- . build( )
3969- ) ;
3970- } else {
3971- let child_env = ctx. provider_credentials . child_env_with_gcp_resolved ( ) ;
3972- let env_count = child_env. len ( ) ;
3973- if let Some ( publisher) = ctx. sidecar_control_publisher . as_ref ( ) {
3974- publisher
3975- . publish_provider_env ( provider_env_revision, child_env. clone ( ) ) ;
3976- }
3951+ if apply_provider_environment_snapshot (
3952+ & ctx. provider_credentials ,
3953+ env_result,
3954+ ctx. sidecar_control_publisher . as_ref ( ) ,
3955+ ) {
39773956 current_provider_env_revision = provider_env_revision;
3978- ocsf_emit ! (
3979- ConfigStateChangeBuilder :: new( ocsf_ctx( ) )
3980- . severity( SeverityId :: Informational )
3981- . status( StatusId :: Success )
3982- . state( StateId :: Enabled , "loaded" )
3983- . unmapped(
3984- "provider_env_revision" ,
3985- serde_json:: json!( provider_env_revision)
3986- )
3987- . message( format!(
3988- "Provider environment refreshed [revision:{provider_env_revision} env_count:{env_count}]"
3989- ) )
3990- . build( )
3991- ) ;
39923957 }
39933958 }
39943959 Err ( e) => {
@@ -4262,6 +4227,61 @@ async fn run_policy_poll_loop_with_client<C: PolicyGatewayClient>(
42624227 }
42634228}
42644229
4230+ /// Apply one complete provider-environment snapshot to the live credential state.
4231+ ///
4232+ /// The caller remains responsible for serializing snapshots and deciding whether
4233+ /// a failed application should be retried. Keeping transport outside this helper
4234+ /// lets polling and supervisor-session updates share the same installation path.
4235+ fn apply_provider_environment_snapshot (
4236+ provider_credentials : & ProviderCredentialState ,
4237+ snapshot : openshell_core:: grpc_client:: ProviderEnvironmentResult ,
4238+ sidecar_control_publisher : Option < & sidecar_control:: Publisher > ,
4239+ ) -> bool {
4240+ let provider_env_revision = snapshot. provider_env_revision ;
4241+ let install_result = provider_credentials. install_bound_environment (
4242+ provider_env_revision,
4243+ snapshot. environment ,
4244+ snapshot. credential_expires_at_ms ,
4245+ snapshot. dynamic_credentials ,
4246+ snapshot. static_credential_bindings ,
4247+ snapshot. non_secret_environment_keys ,
4248+ ) ;
4249+ if let Err ( error) = install_result {
4250+ ocsf_emit ! (
4251+ ConfigStateChangeBuilder :: new( ocsf_ctx( ) )
4252+ . severity( SeverityId :: High )
4253+ . status( StatusId :: Failure )
4254+ . state( StateId :: Disabled , "fail_closed" )
4255+ . message( format!(
4256+ "Rejected provider environment refresh; static provider credentials were revoked; fetched dynamic token grants remain active: {error}"
4257+ ) )
4258+ . build( )
4259+ ) ;
4260+ return false ;
4261+ }
4262+
4263+ let child_env = provider_credentials. child_env_with_gcp_resolved ( ) ;
4264+ let env_count = child_env. len ( ) ;
4265+ if let Some ( publisher) = sidecar_control_publisher {
4266+ publisher. publish_provider_env ( provider_env_revision, child_env) ;
4267+ }
4268+ ocsf_emit ! (
4269+ ConfigStateChangeBuilder :: new( ocsf_ctx( ) )
4270+ . severity( SeverityId :: Informational )
4271+ . status( StatusId :: Success )
4272+ . state( StateId :: Enabled , "loaded" )
4273+ . unmapped(
4274+ "provider_env_revision" ,
4275+ serde_json:: json!( provider_env_revision)
4276+ )
4277+ . message( format!(
4278+ "Provider environment refreshed [revision:{provider_env_revision} env_count:{env_count}]"
4279+ ) )
4280+ . build( )
4281+ ) ;
4282+ true
4283+ }
4284+
42654285fn apply_ocsf_json_setting (
42664286 enabled : & AtomicBool ,
42674287 settings : & std:: collections:: HashMap < String , openshell_core:: proto:: EffectiveSetting > ,
@@ -4498,6 +4518,35 @@ mod tests {
44984518 ) ;
44994519 }
45004520
4521+ #[ test]
4522+ fn provider_environment_snapshot_apply_installs_complete_snapshot ( ) {
4523+ let provider_credentials =
4524+ ProviderCredentialState :: from_child_env_snapshot ( 1 , std:: collections:: HashMap :: new ( ) ) ;
4525+ let applied = apply_provider_environment_snapshot (
4526+ & provider_credentials,
4527+ openshell_core:: grpc_client:: ProviderEnvironmentResult {
4528+ environment : std:: collections:: HashMap :: from ( [ (
4529+ "API_BASE" . to_string ( ) ,
4530+ "https://example.test" . to_string ( ) ,
4531+ ) ] ) ,
4532+ provider_env_revision : 7 ,
4533+ credential_expires_at_ms : std:: collections:: HashMap :: new ( ) ,
4534+ dynamic_credentials : std:: collections:: HashMap :: new ( ) ,
4535+ static_credential_bindings : std:: collections:: HashMap :: new ( ) ,
4536+ non_secret_environment_keys : vec ! [ "API_BASE" . to_string( ) ] ,
4537+ } ,
4538+ None ,
4539+ ) ;
4540+
4541+ assert ! ( applied) ;
4542+ let snapshot = provider_credentials. snapshot ( ) ;
4543+ assert_eq ! ( snapshot. revision, 7 ) ;
4544+ assert_eq ! (
4545+ snapshot. child_env. get( "API_BASE" ) . map( String :: as_str) ,
4546+ Some ( "openshell:resolve:env:v7_API_BASE" )
4547+ ) ;
4548+ }
4549+
45014550 #[ tokio:: test]
45024551 async fn sidecar_control_provider_env_update_orders_by_generation ( ) {
45034552 let ( tx, rx) = tokio:: sync:: mpsc:: unbounded_channel ( ) ;
0 commit comments