@@ -36,11 +36,14 @@ pub struct ChildEnvironmentSnapshot {
3636 pub revision : u64 ,
3737 /// Prepared environment used by future workload processes.
3838 pub environment : HashMap < String , String > ,
39+ /// Complete desired set of non-secret files for this installation.
40+ pub files : HashMap < String , String > ,
3941}
4042
4143#[ derive( Debug ) ]
4244struct ProviderCredentialStateInner {
4345 current : Arc < ProviderCredentialSnapshot > ,
46+ files : HashMap < String , String > ,
4447 generations : VecDeque < Arc < SecretResolver > > ,
4548 current_resolver : Option < Arc < SecretResolver > > ,
4649 combined_resolver : Option < Arc < SecretResolver > > ,
@@ -117,6 +120,7 @@ impl ProviderCredentialState {
117120 Self {
118121 inner : Arc :: new ( RwLock :: new ( ProviderCredentialStateInner {
119122 current : snapshot. clone ( ) ,
123+ files : HashMap :: new ( ) ,
120124 generations,
121125 current_resolver,
122126 combined_resolver,
@@ -172,6 +176,7 @@ impl ProviderCredentialState {
172176 Ok ( Self {
173177 inner : Arc :: new ( RwLock :: new ( ProviderCredentialStateInner {
174178 current : snapshot. clone ( ) ,
179+ files : HashMap :: new ( ) ,
175180 generations,
176181 current_resolver,
177182 combined_resolver,
@@ -205,6 +210,7 @@ impl ProviderCredentialState {
205210 Self {
206211 inner : Arc :: new ( RwLock :: new ( ProviderCredentialStateInner {
207212 current : snapshot. clone ( ) ,
213+ files : HashMap :: new ( ) ,
208214 generations : VecDeque :: new ( ) ,
209215 current_resolver : None ,
210216 combined_resolver : None ,
@@ -410,22 +416,19 @@ impl ProviderCredentialState {
410416 inner. current = Arc :: new ( env) ;
411417 }
412418
413- /// Return `child_env` with GCP static config vars resolved to real values .
419+ /// Return `child_env` with explicitly non-secret config vars resolved.
414420 ///
415- /// The credential pipeline placeholderizes ALL env values, but GCP SDKs
416- /// and coding agents read certain vars (project ID, region, metadata host)
417- /// at process startup before any HTTP request flows through the proxy.
418- /// This method overrides those vars with resolved real values while
419- /// keeping secret credentials (like `GCP_ACCESS_TOKEN`) as placeholders.
421+ /// The credential pipeline placeholderizes all env values. Workloads need
422+ /// non-secret configuration, including provider file paths, at process
423+ /// startup before any HTTP request flows through the proxy. Credential
424+ /// values remain placeholders.
420425 ///
421426 /// Three layers of env var injection:
422427 /// 1. **Synthetic vars** (`GCE_METADATA_IP`, `METADATA_SERVER_DETECTION`)
423428 /// — sandbox-internal config not from user
424429 /// input, inserted directly here with real values.
425- /// 2. **`google_cloud::STATIC_CONFIG_KEYS`** — user-provided non-secret config
426- /// (project ID, region, SA email) that was placeholderized by
427- /// `ProviderPlugin::inject_env` → `SecretResolver`; un-placeholderized
428- /// here so SDKs can read them at startup.
430+ /// 2. **Classified non-secret keys** — user-provided config and provider
431+ /// file paths un-placeholderized so workloads can read them at startup.
429432 /// 3. Everything else stays as placeholders for proxy-time resolution.
430433 pub fn child_env_with_gcp_resolved ( & self ) -> HashMap < String , String > {
431434 let inner = self
@@ -463,9 +466,18 @@ impl ProviderCredentialState {
463466 installation_id : inner. current . installation_id . clone ( ) ,
464467 revision,
465468 environment,
469+ files : inner. files . clone ( ) ,
466470 } )
467471 }
468472
473+ /// Attach the complete file set to a prepared provider snapshot.
474+ pub fn set_managed_files ( & self , files : HashMap < String , String > ) {
475+ self . inner
476+ . write ( )
477+ . expect ( "provider credential state poisoned" )
478+ . files = files;
479+ }
480+
469481 fn resolve_child_env_snapshot (
470482 inner : & ProviderCredentialStateInner ,
471483 ) -> ( u64 , HashMap < String , String > ) {
@@ -477,11 +489,7 @@ impl ProviderCredentialState {
477489 && inner
478490 . non_secret_environment_keys
479491 . contains ( "GCE_METADATA_HOST" ) ;
480- let has_gcp_config = google_cloud:: STATIC_CONFIG_KEYS
481- . iter ( )
482- . any ( |key| env. contains_key ( * key) && inner. non_secret_environment_keys . contains ( * key) ) ;
483-
484- if !has_gcp_metadata && !has_gcp_config {
492+ if !has_gcp_metadata && inner. non_secret_environment_keys . is_empty ( ) {
485493 return ( inner. current . revision , env) ;
486494 }
487495
@@ -506,16 +514,15 @@ impl ProviderCredentialState {
506514 ) ;
507515 }
508516
509- // Un-placeholderize non-secret config vars so SDKs can read them
510- // at process startup before any HTTP flows through the proxy.
517+ // Only explicitly classified non-secret values may be unwrapped.
511518 if let Some ( ref resolver) = inner. combined_resolver {
512- for key in google_cloud :: STATIC_CONFIG_KEYS
513- . iter ( )
514- . filter ( |key| inner . non_secret_environment_keys . contains ( * * key ) )
515- {
519+ for key in & inner . non_secret_environment_keys {
520+ if !env . contains_key ( key ) || ( has_gcp_metadata && key == "GCE_METADATA_HOST" ) {
521+ continue ;
522+ }
516523 let placeholder = crate :: secrets:: placeholder_for_env_key ( key) ;
517524 if let Some ( value) = resolver. resolve_placeholder ( & placeholder) {
518- env. insert ( key. to_string ( ) , value. to_string ( ) ) ;
525+ env. insert ( key. clone ( ) , value. to_string ( ) ) ;
519526 }
520527 }
521528 }
@@ -738,7 +745,7 @@ impl ProviderCredentialState {
738745 pub fn install_prepared ( & self , prepared : & Self ) -> usize {
739746 // Release the candidate lock before taking the live lock, including
740747 // when a caller passes another handle to the same state.
741- let ( snapshot, generations, current_resolver, bindings, non_secret_keys) = {
748+ let ( snapshot, generations, current_resolver, bindings, non_secret_keys, files ) = {
742749 let candidate = prepared
743750 . inner
744751 . read ( )
@@ -749,6 +756,7 @@ impl ProviderCredentialState {
749756 candidate. current_resolver . clone ( ) ,
750757 candidate. static_credential_bindings . clone ( ) ,
751758 candidate. non_secret_environment_keys . clone ( ) ,
759+ candidate. files . clone ( ) ,
752760 )
753761 } ;
754762 let mut inner = self
@@ -781,6 +789,7 @@ impl ProviderCredentialState {
781789 ) ;
782790 inner. static_credential_bindings = bindings;
783791 inner. non_secret_environment_keys = non_secret_keys;
792+ inner. files = files;
784793 inner. body_inventory_available = true ;
785794 inner
786795 . known_body_keys
@@ -2138,6 +2147,33 @@ mod tests {
21382147 assert ! ( !env. contains_key( "CLAUDE_CODE_USE_VERTEX" ) ) ;
21392148 }
21402149
2150+ #[ test]
2151+ fn child_env_resolves_provider_file_path_but_keeps_credentials_placeholderized ( ) {
2152+ let path = "/run/openshell/providers/acme/client.toml" ;
2153+ let state = ProviderCredentialState :: from_bound_environment (
2154+ 1 ,
2155+ HashMap :: from ( [
2156+ ( "ACME_CONFIG_FILE" . to_string ( ) , path. to_string ( ) ) ,
2157+ ( "ACME_TOKEN" . to_string ( ) , "secret-token" . to_string ( ) ) ,
2158+ ] ) ,
2159+ HashMap :: new ( ) ,
2160+ HashMap :: new ( ) ,
2161+ HashMap :: from ( [ (
2162+ "ACME_TOKEN" . to_string ( ) ,
2163+ binding ( "api.acme.example" , 443 , "/**" ) ,
2164+ ) ] ) ,
2165+ vec ! [ "ACME_CONFIG_FILE" . to_string( ) ] ,
2166+ )
2167+ . expect ( "classified provider environment" ) ;
2168+
2169+ let env = state. child_env_with_gcp_resolved ( ) ;
2170+ assert_eq ! ( env. get( "ACME_CONFIG_FILE" ) . map( String :: as_str) , Some ( path) ) ;
2171+ assert_eq ! (
2172+ env. get( "ACME_TOKEN" ) . map( String :: as_str) ,
2173+ Some ( "openshell:resolve:env:v1_ACME_TOKEN" )
2174+ ) ;
2175+ }
2176+
21412177 #[ test]
21422178 fn child_env_with_gcp_resolved_overrides_gcp_static_vars ( ) {
21432179 let state = ProviderCredentialState :: from_bound_environment (
0 commit comments