Skip to main content

alien_bindings/
provider.rs

1//! Unified BindingsProvider implementation that supports multiple cloud providers
2
3use crate::{
4    error::{binding_env_var, ErrorData, Result},
5    providers::postgres::runtime::PostgresRuntime,
6    traits::{
7        ArtifactRegistry, BindingsProviderApi, Build, Container, Key, Kv, Postgres, Queue,
8        ServiceAccount, Storage, Vault, Worker,
9    },
10};
11
12use crate::credential_source::{MintingCredentialSource, MintingResolver};
13use alien_client_config::ClientConfigExt;
14use alien_core::bindings::PostgresBinding;
15use alien_core::{ClientConfig, Platform, StackState, ENV_OPERATOR_BASE_PLATFORM};
16use alien_error::{AlienError, Context, IntoAlienError};
17use async_trait::async_trait;
18use std::{any::Any, collections::HashMap, sync::Arc};
19use tokio::sync::{OnceCell, RwLock};
20
21/// Direct platform-specific bindings provider.
22/// Routes to appropriate platform implementations based on binding configuration.
23///
24/// Caches loaded bindings by name. Each `load_*` call creates cloud clients
25/// (HTTP connection pools, token caches) which are expensive to initialize. Since
26/// the binding configuration is immutable for the provider's lifetime, the same
27/// binding name always produces the same client — so we cache on first load.
28///
29/// Postgres has different caching semantics because cloud handles contain a resolved
30/// password. Its dedicated runtime owns that policy and its secret-store clients.
31#[derive(Debug, Clone)]
32pub struct BindingsProvider {
33    client_config: ClientConfig,
34    bindings: HashMap<String, serde_json::Value>,
35    /// Per-binding-name cache of loaded binding instances. Keyed by
36    /// `"{trait_name}:{binding_name}"` to avoid collisions across types.
37    /// Each value is a `Box<Arc<dyn Trait>>` erased via `Any`.
38    cache: Arc<RwLock<HashMap<String, Box<dyn Any + Send + Sync>>>>,
39    postgres: Arc<PostgresRuntime>,
40}
41
42/// Environment-backed provider that defers cloud client configuration until the
43/// first binding is actually used.
44///
45/// Runtime-less Containers and Daemons resolve bindings in the application
46/// process. They should still start when no startup secret needs loading, even
47/// if cloud metadata is temporarily unavailable.
48///
49/// On first binding use it resolves credentials in a fixed order (see
50/// [`LazyEnvBindingsProvider::select`]): native/projected `ClientConfig::from_env`
51/// first, then minting-backed resolution if an external/bootstrap caller
52/// explicitly supplied the mint environment contract, otherwise the original
53/// `from_env` error is surfaced unchanged.
54pub struct LazyEnvBindingsProvider {
55    env: HashMap<String, String>,
56    /// Deployment platform parsed at construction on the runtime path
57    /// ([`BindingsProvider::from_env_lazy`] validates it eagerly and `select`
58    /// reuses it instead of re-parsing `env` on first use). `None` on the
59    /// app-facing deferred path ([`BindingsProvider::from_env_deferred`]),
60    /// which must construct with zero environment; `select` then resolves the
61    /// platform on first use of a configured binding.
62    platform: Option<Platform>,
63    /// Binding names present in `env`, parsed at construction. Kept separately
64    /// from the fully-resolved [`BindingsProvider`] so the app-facing kinds can
65    /// answer "is this binding configured?" without first resolving the platform
66    /// or cloud client config. Parsing here also makes malformed binding JSON
67    /// fail fast at construction, and `select` reuses the parse instead of
68    /// re-parsing `env` on first use.
69    bindings: HashMap<String, serde_json::Value>,
70    /// The credential resolution strategy, decided once on first use.
71    resolver: OnceCell<CredentialResolver>,
72}
73
74/// Manual `Debug`: `env` may hold live credential material (cloud secret keys,
75/// the deployment token). Print only the env var *names*, never their values.
76impl std::fmt::Debug for LazyEnvBindingsProvider {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        f.debug_struct("LazyEnvBindingsProvider")
79            .field("env_keys", &self.env.keys().collect::<Vec<_>>())
80            .field("resolver", &self.resolver.get())
81            .finish()
82    }
83}
84
85/// How a [`LazyEnvBindingsProvider`] obtains its [`BindingsProvider`], chosen
86/// once at first binding use.
87enum CredentialResolver {
88    /// Native/projected credentials resolved from the environment. The provider
89    /// (and its `ClientConfig`) never changes for the process lifetime.
90    Static(Arc<BindingsProvider>),
91    /// Minting-backed credentials fetched from the manager. The backing provider
92    /// is rebuilt on each re-mint; see [`MintingResolver`]. Boxed so this variant
93    /// doesn't bloat the common `Static` one.
94    Minting(Box<MintingResolver>),
95}
96
97/// Manual `Debug`: the `Static` provider embeds a live `ClientConfig` and the
98/// `Minting` resolver a bearer token / minted config. Never render either.
99impl std::fmt::Debug for CredentialResolver {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        match self {
102            CredentialResolver::Static(_) => f.write_str("Static(<redacted>)"),
103            CredentialResolver::Minting(resolver) => {
104                f.debug_tuple("Minting").field(resolver).finish()
105            }
106        }
107    }
108}
109
110impl BindingsProvider {
111    /// Creates a new BindingsProvider with explicit credentials and bindings.
112    ///
113    /// This is the base constructor used by all other convenience constructors.
114    pub fn new(
115        client_config: ClientConfig,
116        bindings: HashMap<String, serde_json::Value>,
117    ) -> Result<Self> {
118        let postgres = Arc::new(PostgresRuntime::new(client_config.clone()));
119        Ok(Self {
120            client_config,
121            bindings,
122            cache: Arc::new(RwLock::new(HashMap::new())),
123            postgres,
124        })
125    }
126
127    /// The workload's resolved cloud credentials (native/projected identity or Alien-minted
128    /// short-lived credentials). Consumers that need to authorize a request themselves — the
129    /// AI gateway signs upstream model calls with this — read it here rather than going
130    /// through a per-resource binding.
131    pub fn client_config(&self) -> &ClientConfig {
132        &self.client_config
133    }
134
135    /// Get a cached binding by type and name, or return None.
136    async fn get_cached<T: Clone + Send + Sync + 'static>(
137        &self,
138        trait_name: &str,
139        binding_name: &str,
140    ) -> Option<T> {
141        let cache_key = format!("{}:{}", trait_name, binding_name);
142        let cache = self.cache.read().await;
143        cache
144            .get(&cache_key)
145            .and_then(|boxed| boxed.downcast_ref::<T>())
146            .cloned()
147    }
148
149    /// Store a binding in the cache.
150    async fn put_cache<T: Clone + Send + Sync + 'static>(
151        &self,
152        trait_name: &str,
153        binding_name: &str,
154        value: T,
155    ) {
156        let cache_key = format!("{}:{}", trait_name, binding_name);
157        let mut cache = self.cache.write().await;
158        cache.insert(cache_key, Box::new(value));
159    }
160
161    /// Creates a BindingsProvider from environment variables (for runtime use).
162    ///
163    /// This parses the platform from ALIEN_DEPLOYMENT_TYPE, loads ClientConfig from environment,
164    /// and extracts all ALIEN_*_BINDING environment variables.
165    pub async fn from_env(env: HashMap<String, String>) -> Result<Self> {
166        // 1. Parse platform from ALIEN_DEPLOYMENT_TYPE
167        let platform = crate::get_platform_from_env(&env)?;
168
169        // 2. Load ClientConfig from environment
170        let client_config = Self::client_config_from_env(platform, &env).await?;
171
172        // 3. Parse all ALIEN_*_BINDING environment variables
173        let bindings = Self::parse_bindings_from_env(&env)?;
174
175        Self::new(client_config, bindings)
176    }
177
178    /// Creates an environment-backed provider without resolving cloud client
179    /// configuration yet.
180    ///
181    /// Startup still validates the platform and binding JSON, but credentials
182    /// are loaded only if application code asks for a binding or runtime-owned
183    /// startup secrets need to be fetched.
184    pub fn from_env_lazy(env: HashMap<String, String>) -> Result<LazyEnvBindingsProvider> {
185        // Runtime path: validate the deployment platform eagerly so a
186        // misconfigured worker fails at startup, not on first binding use.
187        let platform = crate::get_platform_from_env(&env)?;
188        let bindings = Self::parse_bindings_from_env(&env)?;
189
190        Ok(LazyEnvBindingsProvider {
191            env,
192            platform: Some(platform),
193            bindings,
194            resolver: OnceCell::new(),
195        })
196    }
197
198    /// Creates an environment-backed provider that defers *all* platform and
199    /// cloud-client-config resolution to the first use of a *configured*
200    /// binding.
201    ///
202    /// This is the app-facing (napi / [`crate::Bindings`]) path. Its contract:
203    /// constructing with zero environment must succeed, and the first operation
204    /// against a binding that has no `ALIEN_<NAME>_BINDING` must report
205    /// [`ErrorData::BindingNotConfigured`] *before* any platform or credential
206    /// resolution — a missing binding is an application error, not a deployment
207    /// misconfiguration. Binding JSON is still parsed here, so malformed config
208    /// fails fast at construction.
209    ///
210    /// Contrast with [`Self::from_env_lazy`], which eagerly validates the
211    /// deployment platform so long-running runtime processes fail fast at
212    /// startup.
213    pub fn from_env_deferred(env: HashMap<String, String>) -> Result<LazyEnvBindingsProvider> {
214        let bindings = Self::parse_bindings_from_env(&env)?;
215
216        Ok(LazyEnvBindingsProvider {
217            env,
218            // Deferred contract: platform resolution happens on first use of a
219            // configured binding, never at construction.
220            platform: None,
221            bindings,
222            resolver: OnceCell::new(),
223        })
224    }
225
226    async fn client_config_from_env(
227        platform: Platform,
228        env: &HashMap<String, String>,
229    ) -> Result<ClientConfig> {
230        if platform != Platform::Kubernetes {
231            return Self::load_client_config_from_env(platform, env).await;
232        }
233
234        let Some(base_platform) = Self::base_platform_from_env(env)? else {
235            return Self::load_client_config_from_env(platform, env).await;
236        };
237
238        let kubernetes = match Self::load_client_config_from_env(Platform::Kubernetes, env).await? {
239            ClientConfig::Kubernetes(kubernetes) => kubernetes,
240            _ => unreachable!("kubernetes platform must produce a Kubernetes client config"),
241        };
242        let cloud = Self::load_client_config_from_env(base_platform, env).await?;
243
244        Ok(ClientConfig::KubernetesCloud {
245            kubernetes,
246            cloud: Box::new(cloud),
247        })
248    }
249
250    fn base_platform_from_env(env: &HashMap<String, String>) -> Result<Option<Platform>> {
251        let Some(base_platform) = env.get(ENV_OPERATOR_BASE_PLATFORM) else {
252            return Ok(None);
253        };
254
255        let parsed: Platform = base_platform.parse().map_err(|reason| {
256            AlienError::new(ErrorData::InvalidEnvironmentVariable {
257                variable_name: ENV_OPERATOR_BASE_PLATFORM.to_string(),
258                value: base_platform.clone(),
259                reason,
260            })
261        })?;
262
263        if !matches!(parsed, Platform::Aws | Platform::Gcp | Platform::Azure) {
264            return Err(AlienError::new(ErrorData::InvalidEnvironmentVariable {
265                variable_name: ENV_OPERATOR_BASE_PLATFORM.to_string(),
266                value: base_platform.clone(),
267                reason: "Kubernetes base platform must be aws, gcp, or azure".to_string(),
268            }));
269        }
270
271        Ok(Some(parsed))
272    }
273
274    async fn load_client_config_from_env(
275        platform: Platform,
276        env: &HashMap<String, String>,
277    ) -> Result<ClientConfig> {
278        ClientConfig::from_env(platform, env).await.map_err(|e| {
279            AlienError::new(ErrorData::ClientConfigInvalid {
280                platform,
281                message: format!("Failed to load client config: {}", e),
282            })
283        })
284    }
285
286    /// Parses all ALIEN_*_BINDING environment variables into a map.
287    fn parse_bindings_from_env(
288        env: &HashMap<String, String>,
289    ) -> Result<HashMap<String, serde_json::Value>> {
290        let mut bindings = HashMap::new();
291        for (key, value) in env {
292            if key.starts_with("ALIEN_") && key.ends_with("_BINDING") {
293                let binding_name = key
294                    .strip_prefix("ALIEN_")
295                    .unwrap()
296                    .strip_suffix("_BINDING")
297                    .unwrap()
298                    .to_lowercase()
299                    .replace('_', "-");
300                let parsed: serde_json::Value = serde_json::from_str(value)
301                    .into_alien_error()
302                    .context(ErrorData::BindingConfigInvalid {
303                        env_var: key.clone(),
304                        binding_name: binding_name.clone(),
305                        reason: "Failed to parse binding JSON".to_string(),
306                    })?;
307                bindings.insert(binding_name, parsed);
308            }
309        }
310        Ok(bindings)
311    }
312
313    /// Look up a binding's JSON and parse it into its strongly-typed form.
314    ///
315    /// Maps a missing binding to [`ErrorData::not_configured`] and a malformed
316    /// one to [`ErrorData::config_invalid`], tagged with `type_label` (e.g.
317    /// `"storage"`, `"KV"`) so the message reads `Failed to parse <label> binding`.
318    fn parse_binding<T: serde::de::DeserializeOwned>(
319        &self,
320        binding_name: &str,
321        type_label: &str,
322    ) -> Result<T> {
323        let binding_json = self
324            .bindings
325            .get(binding_name)
326            .ok_or_else(|| AlienError::new(ErrorData::not_configured(binding_name)))?;
327        serde_json::from_value(binding_json.clone())
328            .into_alien_error()
329            .context(ErrorData::config_invalid(
330                binding_name,
331                format!("Failed to parse {type_label} binding"),
332            ))
333    }
334
335    /// Pattern 1: Creates a BindingsProvider from stack state and client config.
336    ///
337    /// Convenience helper that extracts bindings from stack state's remote_binding_params.
338    /// Only resources with `remote_access: true` will have binding params available.
339    ///
340    /// **When to use:** You already have `ClientConfig` and `StackState`.
341    ///
342    /// **Example use cases:**
343    /// - alien-deployment (has credentials from RemoteAccessResolver)
344    /// - Platform API backend (has stack state from DB)
345    pub fn from_stack_state(stack_state: &StackState, client_config: ClientConfig) -> Result<Self> {
346        let bindings = stack_state
347            .resources
348            .iter()
349            .filter_map(|(id, state)| {
350                state
351                    .remote_binding_params
352                    .as_ref()
353                    .map(|p| (id.clone(), p.clone()))
354            })
355            .collect();
356
357        Self::new(client_config, bindings)
358    }
359}
360
361impl LazyEnvBindingsProvider {
362    /// Resolve (once) which credential strategy to use, then return a provider
363    /// backed by fresh-enough credentials.
364    ///
365    /// The strategy selection happens exactly once (via `OnceCell`); on the
366    /// minting path each call additionally checks credential freshness and
367    /// re-mints on access if stale.
368    pub async fn provider(&self) -> Result<Arc<BindingsProvider>> {
369        let resolver = self
370            .resolver
371            .get_or_try_init(|| async { self.select().await })
372            .await?;
373
374        match resolver {
375            CredentialResolver::Static(provider) => Ok(provider.clone()),
376            CredentialResolver::Minting(minting) => minting.provider().await,
377        }
378    }
379
380    /// Decide the credential strategy at first use, in a fixed order:
381    ///
382    /// 1. Native/projected `ClientConfig::from_env` succeeds → use it, never mint.
383    /// 2. Else if the complete external/bootstrap mint env contract is present
384    ///    → mint.
385    /// 3. Else → surface the original `from_env` error unchanged.
386    async fn select(&self) -> Result<CredentialResolver> {
387        // Binding JSON (and, on the `from_env_lazy` path, the platform) was
388        // already parsed and validated at construction; `env` cannot have
389        // changed since, so re-parsing would just repeat that work for the
390        // same result. On the `from_env_deferred` path the platform was
391        // deliberately not resolved at construction, so resolve it now.
392        let platform = match self.platform {
393            Some(platform) => platform,
394            None => crate::get_platform_from_env(&self.env)?,
395        };
396        match BindingsProvider::client_config_from_env(platform, &self.env).await {
397            Ok(client_config) => Ok(CredentialResolver::Static(Arc::new(BindingsProvider::new(
398                client_config,
399                self.bindings.clone(),
400            )?))),
401            Err(from_env_error) => match MintingCredentialSource::from_env(&self.env)? {
402                Some(source) => Ok(CredentialResolver::Minting(Box::new(MintingResolver::new(
403                    source,
404                    self.bindings.clone(),
405                )))),
406                // No mint contract: the environment simply couldn't produce
407                // credentials. Preserve the exact original error.
408                None => Err(from_env_error),
409            },
410        }
411    }
412
413    /// Fail fast with [`ErrorData::BindingNotConfigured`] when `binding_name`
414    /// has no `ALIEN_<NAME>_BINDING` entry, *before* any platform / cloud
415    /// client-config resolution. This is what lets a zero-env app construct
416    /// bindings and get a clean BINDING_NOT_CONFIGURED (not a deployment error)
417    /// on the first op against a missing binding. A binding that *is* present
418    /// falls through to normal resolution, so an existing binding on a cloud
419    /// platform without credentials still surfaces the client-config error.
420    ///
421    /// Called at the entry of every `load_*` method below, app-facing or not:
422    /// `parse_binding` (in [`BindingsProvider`]) would return the identical
423    /// `not_configured` error for a missing name once resolution reaches it,
424    /// so this guard only *reorders* that error ahead of platform/credential
425    /// resolution — it never changes which bindings succeed or fail. Keeping
426    /// it uniform means a newly added `load_*` method can't silently regress
427    /// the zero-env contract by omission.
428    fn ensure_binding_present(&self, binding_name: &str) -> Result<()> {
429        if self.bindings.contains_key(binding_name) {
430            Ok(())
431        } else {
432            Err(AlienError::new(ErrorData::not_configured(binding_name)))
433        }
434    }
435}
436
437#[async_trait]
438impl BindingsProviderApi for LazyEnvBindingsProvider {
439    async fn load_storage(&self, binding_name: &str) -> Result<Arc<dyn Storage>> {
440        self.ensure_binding_present(binding_name)?;
441        self.provider().await?.load_storage(binding_name).await
442    }
443
444    async fn load_key(&self, binding_name: &str) -> Result<Arc<dyn Key>> {
445        self.ensure_binding_present(binding_name)?;
446        self.provider().await?.load_key(binding_name).await
447    }
448
449    async fn load_build(&self, binding_name: &str) -> Result<Arc<dyn Build>> {
450        self.ensure_binding_present(binding_name)?;
451        self.provider().await?.load_build(binding_name).await
452    }
453
454    async fn load_artifact_registry(
455        &self,
456        binding_name: &str,
457    ) -> Result<Arc<dyn ArtifactRegistry>> {
458        self.ensure_binding_present(binding_name)?;
459        self.provider()
460            .await?
461            .load_artifact_registry(binding_name)
462            .await
463    }
464
465    async fn load_vault(&self, binding_name: &str) -> Result<Arc<dyn Vault>> {
466        self.ensure_binding_present(binding_name)?;
467        self.provider().await?.load_vault(binding_name).await
468    }
469
470    async fn load_kv(&self, binding_name: &str) -> Result<Arc<dyn Kv>> {
471        self.ensure_binding_present(binding_name)?;
472        self.provider().await?.load_kv(binding_name).await
473    }
474
475    async fn load_postgres(&self, binding_name: &str) -> Result<Arc<dyn Postgres>> {
476        self.ensure_binding_present(binding_name)?;
477        self.provider().await?.load_postgres(binding_name).await
478    }
479
480    async fn load_queue(&self, binding_name: &str) -> Result<Arc<dyn Queue>> {
481        self.ensure_binding_present(binding_name)?;
482        self.provider().await?.load_queue(binding_name).await
483    }
484
485    async fn load_worker(&self, binding_name: &str) -> Result<Arc<dyn Worker>> {
486        self.ensure_binding_present(binding_name)?;
487        self.provider().await?.load_worker(binding_name).await
488    }
489
490    async fn load_container(&self, binding_name: &str) -> Result<Arc<dyn Container>> {
491        self.ensure_binding_present(binding_name)?;
492        self.provider().await?.load_container(binding_name).await
493    }
494
495    async fn load_service_account(&self, binding_name: &str) -> Result<Arc<dyn ServiceAccount>> {
496        self.ensure_binding_present(binding_name)?;
497        self.provider()
498            .await?
499            .load_service_account(binding_name)
500            .await
501    }
502
503    async fn load_sandbox(&self, binding_name: &str) -> Result<Arc<dyn crate::traits::Sandbox>> {
504        self.ensure_binding_present(binding_name)?;
505        self.provider().await?.load_sandbox(binding_name).await
506    }
507}
508
509#[async_trait]
510impl BindingsProviderApi for BindingsProvider {
511    async fn load_storage(&self, binding_name: &str) -> Result<Arc<dyn Storage>> {
512        if let Some(cached) = self
513            .get_cached::<Arc<dyn Storage>>("storage", binding_name)
514            .await
515        {
516            return Ok(cached);
517        }
518
519        use alien_core::bindings::StorageBinding;
520
521        // Get binding JSON from our pre-parsed map
522        let binding: StorageBinding = self.parse_binding(binding_name, "storage")?;
523
524        let result: Arc<dyn Storage> = match binding {
525            #[cfg(feature = "aws")]
526            StorageBinding::S3(config) => {
527                use crate::providers::storage::aws_s3::S3Storage;
528
529                // Get AWS config from our stored ClientConfig
530                let aws_config = self.client_config.aws_config().ok_or_else(|| {
531                    AlienError::new(ErrorData::ClientConfigInvalid {
532                        platform: Platform::Aws,
533                        message: "AWS config not available".to_string(),
534                    })
535                })?;
536
537                let credentials =
538                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
539                        .await
540                        .context(ErrorData::BindingSetupFailed {
541                            binding_type: "AWS S3 storage".to_string(),
542                            reason: "Failed to create credential provider".to_string(),
543                        })?;
544
545                // Extract bucket name from binding
546                let bucket_name = config
547                    .bucket_name
548                    .into_value(binding_name, "bucket_name")
549                    .context(ErrorData::config_invalid(
550                        binding_name,
551                        "Failed to extract bucket_name from S3 binding",
552                    ))?;
553
554                let storage: Arc<dyn Storage> = Arc::new(S3Storage::new(bucket_name, credentials)?);
555                Ok(storage)
556            }
557            #[cfg(not(feature = "aws"))]
558            StorageBinding::S3 { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
559                feature: "aws".to_string(),
560            })),
561
562            #[cfg(feature = "azure")]
563            StorageBinding::Blob(config) => {
564                use crate::providers::storage::azure_blob::BlobStorage;
565
566                let azure_config = self.client_config.azure_config().ok_or_else(|| {
567                    AlienError::new(ErrorData::ClientConfigInvalid {
568                        platform: Platform::Azure,
569                        message: "Azure config not available".to_string(),
570                    })
571                })?;
572
573                // Extract container and account names from binding
574                let container_name = config
575                    .container_name
576                    .into_value(binding_name, "container_name")
577                    .context(ErrorData::config_invalid(
578                        binding_name,
579                        "Failed to extract container_name from Blob binding",
580                    ))?;
581
582                let account_name = config
583                    .account_name
584                    .into_value(binding_name, "account_name")
585                    .context(ErrorData::config_invalid(
586                        binding_name,
587                        "Failed to extract account_name from Blob binding",
588                    ))?;
589
590                let storage: Arc<dyn Storage> = Arc::new(BlobStorage::new(
591                    container_name,
592                    account_name,
593                    azure_config,
594                )?);
595                Ok(storage)
596            }
597            #[cfg(not(feature = "azure"))]
598            StorageBinding::Blob { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
599                feature: "azure".to_string(),
600            })),
601
602            #[cfg(feature = "gcp")]
603            StorageBinding::Gcs(config) => {
604                use crate::providers::storage::gcp_gcs::GcsStorage;
605
606                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
607                    AlienError::new(ErrorData::ClientConfigInvalid {
608                        platform: Platform::Gcp,
609                        message: "GCP config not available".to_string(),
610                    })
611                })?;
612
613                // Extract bucket name from binding
614                let bucket_name = config
615                    .bucket_name
616                    .into_value(binding_name, "bucket_name")
617                    .context(ErrorData::config_invalid(
618                        binding_name,
619                        "Failed to extract bucket_name from Gcs binding",
620                    ))?;
621
622                let storage: Arc<dyn Storage> = Arc::new(GcsStorage::new(bucket_name, gcp_config)?);
623                Ok(storage)
624            }
625            #[cfg(not(feature = "gcp"))]
626            StorageBinding::Gcs { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
627                feature: "gcp".to_string(),
628            })),
629
630            #[cfg(feature = "local")]
631            StorageBinding::Local(config) => {
632                use crate::providers::storage::local::LocalStorage;
633
634                // Extract storage path from binding
635                let storage_path = config
636                    .storage_path
637                    .into_value(binding_name, "storage_path")
638                    .context(ErrorData::config_invalid(
639                        binding_name,
640                        "Failed to extract storage_path from Local binding",
641                    ))?;
642
643                let storage: Arc<dyn Storage> = Arc::new(LocalStorage::new(storage_path)?);
644                Ok(storage)
645            }
646            #[cfg(not(feature = "local"))]
647            StorageBinding::Local { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
648                feature: "local".to_string(),
649            })),
650        }?;
651
652        self.put_cache("storage", binding_name, result.clone())
653            .await;
654        Ok(result)
655    }
656
657    async fn load_key(&self, binding_name: &str) -> Result<Arc<dyn Key>> {
658        if let Some(cached) = self.get_cached::<Arc<dyn Key>>("key", binding_name).await {
659            return Ok(cached);
660        }
661
662        use alien_core::bindings::KeyBinding;
663        let binding: KeyBinding = self.parse_binding(binding_name, "key")?;
664        let key: Arc<dyn Key> = match binding {
665            #[cfg(feature = "aws")]
666            KeyBinding::AwsKms(config) => {
667                use crate::providers::key::aws::AwsKmsKey;
668                use alien_aws_clients::kms::KmsClient;
669                let mut aws_config = self.client_config.aws_config().cloned().ok_or_else(|| {
670                    AlienError::new(ErrorData::ClientConfigInvalid {
671                        platform: Platform::Aws,
672                        message: "AWS config not available".to_string(),
673                    })
674                })?;
675                if let Some(region) = config.region {
676                    aws_config.region = region.into_value(binding_name, "region").context(
677                        ErrorData::config_invalid(
678                            binding_name,
679                            "Failed to extract region from KMS binding",
680                        ),
681                    )?;
682                }
683                let credentials = alien_aws_clients::AwsCredentialProvider::from_config(aws_config)
684                    .await
685                    .context(ErrorData::BindingSetupFailed {
686                        binding_type: "AWS KMS key".to_string(),
687                        reason: "Failed to create credential provider".to_string(),
688                    })?;
689                let key_arn = config.key_arn.into_value(binding_name, "key_arn").context(
690                    ErrorData::config_invalid(
691                        binding_name,
692                        "Failed to extract key_arn from KMS binding",
693                    ),
694                )?;
695                Arc::new(AwsKmsKey::new(
696                    Arc::new(KmsClient::new(
697                        crate::http_client::create_http_client(),
698                        credentials,
699                    )),
700                    key_arn,
701                ))
702            }
703            #[cfg(not(feature = "aws"))]
704            KeyBinding::AwsKms(_) => {
705                return Err(AlienError::new(ErrorData::FeatureNotEnabled {
706                    feature: "aws".to_string(),
707                }))
708            }
709            #[cfg(feature = "gcp")]
710            KeyBinding::GcpCloudKms(config) => {
711                use crate::providers::key::gcp::GcpCloudKmsKey;
712                use alien_gcp_clients::cloud_kms::CloudKmsClient;
713                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
714                    AlienError::new(ErrorData::ClientConfigInvalid {
715                        platform: Platform::Gcp,
716                        message: "GCP config not available".to_string(),
717                    })
718                })?;
719                let crypto_key_name = config
720                    .crypto_key_name
721                    .into_value(binding_name, "crypto_key_name")
722                    .context(ErrorData::config_invalid(
723                        binding_name,
724                        "Failed to extract crypto_key_name from Cloud KMS binding",
725                    ))?;
726                Arc::new(GcpCloudKmsKey::new(
727                    Arc::new(CloudKmsClient::new(
728                        crate::http_client::create_http_client(),
729                        gcp_config.clone(),
730                    )),
731                    crypto_key_name,
732                ))
733            }
734            #[cfg(not(feature = "gcp"))]
735            KeyBinding::GcpCloudKms(_) => {
736                return Err(AlienError::new(ErrorData::FeatureNotEnabled {
737                    feature: "gcp".to_string(),
738                }))
739            }
740            #[cfg(feature = "azure")]
741            KeyBinding::AzureKeyVault(config) => {
742                use crate::providers::key::azure::AzureKeyVaultKey;
743                use alien_azure_clients::keyvault::AzureKeyVaultKeysClient;
744                use alien_azure_clients::AzureTokenCache;
745                let azure_config = self.client_config.azure_config().ok_or_else(|| {
746                    AlienError::new(ErrorData::ClientConfigInvalid {
747                        platform: Platform::Azure,
748                        message: "Azure config not available".to_string(),
749                    })
750                })?;
751                let key_id = config.key_id.into_value(binding_name, "key_id").context(
752                    ErrorData::config_invalid(
753                        binding_name,
754                        "Failed to extract key_id from Key Vault binding",
755                    ),
756                )?;
757                Arc::new(AzureKeyVaultKey::new(
758                    Arc::new(AzureKeyVaultKeysClient::new(
759                        crate::http_client::create_http_client(),
760                        AzureTokenCache::new(azure_config.clone()),
761                    )),
762                    key_id,
763                ))
764            }
765            #[cfg(not(feature = "azure"))]
766            KeyBinding::AzureKeyVault(_) => {
767                return Err(AlienError::new(ErrorData::FeatureNotEnabled {
768                    feature: "azure".to_string(),
769                }))
770            }
771        };
772
773        self.put_cache("key", binding_name, key.clone()).await;
774        Ok(key)
775    }
776
777    async fn load_build(&self, binding_name: &str) -> Result<Arc<dyn Build>> {
778        use alien_core::bindings::BuildBinding;
779
780        let binding: BuildBinding = self.parse_binding(binding_name, "build")?;
781
782        match binding {
783            #[cfg(feature = "aws")]
784            BuildBinding::Codebuild { .. } => {
785                use crate::providers::build::codebuild::CodebuildBuild;
786
787                let aws_config = self.client_config.aws_config().ok_or_else(|| {
788                    AlienError::new(ErrorData::ClientConfigInvalid {
789                        platform: Platform::Aws,
790                        message: "AWS config not available".to_string(),
791                    })
792                })?;
793                let credentials =
794                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
795                        .await
796                        .context(ErrorData::ClientConfigInvalid {
797                            platform: Platform::Aws,
798                            message: "Failed to create AWS credential provider".to_string(),
799                        })?;
800
801                let build = Arc::new(
802                    CodebuildBuild::new(binding_name.to_string(), binding, &credentials)
803                        .await
804                        .context(ErrorData::config_invalid(
805                            binding_name,
806                            "Failed to initialize AWS CodeBuild client",
807                        ))?,
808                );
809                Ok(build)
810            }
811            #[cfg(not(feature = "aws"))]
812            BuildBinding::Codebuild { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
813                feature: "aws".to_string(),
814            })),
815
816            #[cfg(feature = "azure")]
817            BuildBinding::Aca { .. } => {
818                use crate::providers::build::aca::AcaBuild;
819
820                let azure_config = self.client_config.azure_config().ok_or_else(|| {
821                    AlienError::new(ErrorData::ClientConfigInvalid {
822                        platform: Platform::Azure,
823                        message: "Azure config not available".to_string(),
824                    })
825                })?;
826
827                let build = Arc::new(
828                    AcaBuild::new(binding_name.to_string(), binding, azure_config)
829                        .await
830                        .context(ErrorData::config_invalid(
831                            binding_name,
832                            "Failed to initialize Azure Container Apps build",
833                        ))?,
834                );
835                Ok(build)
836            }
837            #[cfg(not(feature = "azure"))]
838            BuildBinding::Aca { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
839                feature: "azure".to_string(),
840            })),
841
842            #[cfg(feature = "gcp")]
843            BuildBinding::Cloudbuild { .. } => {
844                use crate::providers::build::cloudbuild::CloudbuildBuild;
845
846                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
847                    AlienError::new(ErrorData::ClientConfigInvalid {
848                        platform: Platform::Gcp,
849                        message: "GCP config not available".to_string(),
850                    })
851                })?;
852
853                let build = Arc::new(
854                    CloudbuildBuild::new(binding_name.to_string(), binding, gcp_config)
855                        .await
856                        .context(ErrorData::config_invalid(
857                            binding_name,
858                            "Failed to initialize GCP Cloud Build client",
859                        ))?,
860                );
861                Ok(build)
862            }
863            #[cfg(not(feature = "gcp"))]
864            BuildBinding::Cloudbuild { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
865                feature: "gcp".to_string(),
866            })),
867
868            #[cfg(feature = "local")]
869            BuildBinding::Local { .. } => {
870                use crate::providers::build::local::LocalBuild;
871
872                let build = Arc::new(LocalBuild::new(binding_name.to_string(), binding)?);
873                Ok(build)
874            }
875            #[cfg(not(feature = "local"))]
876            BuildBinding::Local { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
877                feature: "local".to_string(),
878            })),
879
880            #[cfg(feature = "kubernetes")]
881            BuildBinding::Kubernetes { .. } => {
882                use crate::providers::build::kubernetes::KubernetesBuild;
883
884                let build =
885                    Arc::new(KubernetesBuild::new(binding_name.to_string(), binding).await?);
886                Ok(build)
887            }
888            #[cfg(not(feature = "kubernetes"))]
889            BuildBinding::Kubernetes { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
890                feature: "kubernetes".to_string(),
891            })),
892        }
893    }
894
895    async fn load_artifact_registry(
896        &self,
897        binding_name: &str,
898    ) -> Result<Arc<dyn ArtifactRegistry>> {
899        if let Some(cached) = self
900            .get_cached::<Arc<dyn ArtifactRegistry>>("artifact_registry", binding_name)
901            .await
902        {
903            return Ok(cached);
904        }
905
906        use alien_core::bindings::ArtifactRegistryBinding;
907
908        let binding: ArtifactRegistryBinding =
909            self.parse_binding(binding_name, "artifact registry")?;
910
911        let registry: Arc<dyn ArtifactRegistry> = match binding {
912            #[cfg(feature = "aws")]
913            ArtifactRegistryBinding::Ecr { .. } => {
914                use crate::providers::artifact_registry::ecr::EcrArtifactRegistry;
915
916                let aws_config = self.client_config.aws_config().ok_or_else(|| {
917                    AlienError::new(ErrorData::ClientConfigInvalid {
918                        platform: Platform::Aws,
919                        message: "AWS config not available".to_string(),
920                    })
921                })?;
922                let credentials =
923                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
924                        .await
925                        .context(ErrorData::ClientConfigInvalid {
926                            platform: Platform::Aws,
927                            message: "Failed to create AWS credential provider".to_string(),
928                        })?;
929
930                let registry: Arc<dyn ArtifactRegistry> = Arc::new(
931                    EcrArtifactRegistry::new(binding_name.to_string(), binding, &credentials)
932                        .await
933                        .context(ErrorData::config_invalid(
934                            binding_name,
935                            "Failed to initialize AWS ECR artifact registry",
936                        ))?,
937                );
938                Ok(registry)
939            }
940            #[cfg(not(feature = "aws"))]
941            ArtifactRegistryBinding::Ecr { .. } => {
942                Err(AlienError::new(ErrorData::FeatureNotEnabled {
943                    feature: "aws".to_string(),
944                }))
945            }
946
947            #[cfg(feature = "azure")]
948            ArtifactRegistryBinding::Acr { .. } => {
949                use crate::providers::artifact_registry::acr::AcrArtifactRegistry;
950
951                let azure_config = self.client_config.azure_config().ok_or_else(|| {
952                    AlienError::new(ErrorData::ClientConfigInvalid {
953                        platform: Platform::Azure,
954                        message: "Azure config not available".to_string(),
955                    })
956                })?;
957
958                let registry: Arc<dyn ArtifactRegistry> = Arc::new(
959                    AcrArtifactRegistry::new(binding_name.to_string(), binding, azure_config)
960                        .await
961                        .context(ErrorData::config_invalid(
962                            binding_name,
963                            "Failed to initialize Azure ACR artifact registry",
964                        ))?,
965                );
966                Ok(registry)
967            }
968            #[cfg(not(feature = "azure"))]
969            ArtifactRegistryBinding::Acr { .. } => {
970                Err(AlienError::new(ErrorData::FeatureNotEnabled {
971                    feature: "azure".to_string(),
972                }))
973            }
974
975            #[cfg(feature = "gcp")]
976            ArtifactRegistryBinding::Gar { .. } => {
977                use crate::providers::artifact_registry::gar::GarArtifactRegistry;
978
979                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
980                    AlienError::new(ErrorData::ClientConfigInvalid {
981                        platform: Platform::Gcp,
982                        message: "GCP config not available".to_string(),
983                    })
984                })?;
985
986                let registry: Arc<dyn ArtifactRegistry> = Arc::new(
987                    GarArtifactRegistry::new(binding_name.to_string(), binding, gcp_config)
988                        .await
989                        .context(ErrorData::config_invalid(
990                            binding_name,
991                            "Failed to initialize GCP GAR artifact registry",
992                        ))?,
993                );
994                Ok(registry)
995            }
996            #[cfg(not(feature = "gcp"))]
997            ArtifactRegistryBinding::Gar { .. } => {
998                Err(AlienError::new(ErrorData::FeatureNotEnabled {
999                    feature: "gcp".to_string(),
1000                }))
1001            }
1002
1003            #[cfg(feature = "local")]
1004            ArtifactRegistryBinding::Local { .. } => {
1005                use crate::providers::artifact_registry::local::LocalArtifactRegistry;
1006
1007                let registry: Arc<dyn ArtifactRegistry> = Arc::new(
1008                    LocalArtifactRegistry::new(binding_name.to_string(), binding.clone()).await?,
1009                );
1010                Ok(registry)
1011            }
1012            #[cfg(not(feature = "local"))]
1013            ArtifactRegistryBinding::Local { .. } => {
1014                Err(AlienError::new(ErrorData::FeatureNotEnabled {
1015                    feature: "local".to_string(),
1016                }))
1017            }
1018        }?;
1019
1020        self.put_cache("artifact_registry", binding_name, registry.clone())
1021            .await;
1022        Ok(registry)
1023    }
1024
1025    async fn load_vault(&self, binding_name: &str) -> Result<Arc<dyn Vault>> {
1026        if let Some(cached) = self
1027            .get_cached::<Arc<dyn Vault>>("vault", binding_name)
1028            .await
1029        {
1030            return Ok(cached);
1031        }
1032
1033        use alien_core::bindings::VaultBinding;
1034
1035        let binding: VaultBinding = self.parse_binding(binding_name, "vault")?;
1036
1037        let result: Arc<dyn Vault> = match binding {
1038            #[cfg(feature = "aws")]
1039            VaultBinding::ParameterStore(config) => {
1040                use crate::providers::vault::aws_parameter_store::AwsParameterStoreVault;
1041                use alien_aws_clients::ssm::SsmClient;
1042
1043                let aws_config = self.client_config.aws_config().ok_or_else(|| {
1044                    AlienError::new(ErrorData::ClientConfigInvalid {
1045                        platform: Platform::Aws,
1046                        message: "AWS config not available".to_string(),
1047                    })
1048                })?;
1049                let credentials =
1050                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
1051                        .await
1052                        .context(ErrorData::ClientConfigInvalid {
1053                            platform: Platform::Aws,
1054                            message: "Failed to create AWS credential provider".to_string(),
1055                        })?;
1056
1057                let client = Arc::new(SsmClient::new(
1058                    crate::http_client::create_http_client(),
1059                    credentials,
1060                ));
1061
1062                // Extract the vault prefix from the binding configuration
1063                let vault_prefix = config
1064                    .vault_prefix
1065                    .into_value(&binding_name, "vault_prefix")
1066                    .context(ErrorData::config_invalid(
1067                        binding_name,
1068                        "Failed to extract vault_prefix from ParameterStore binding",
1069                    ))?;
1070
1071                let vault: Arc<dyn Vault> =
1072                    Arc::new(AwsParameterStoreVault::new(client, vault_prefix));
1073                Ok(vault)
1074            }
1075            #[cfg(not(feature = "aws"))]
1076            VaultBinding::ParameterStore(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1077                feature: "aws".to_string(),
1078            })),
1079
1080            #[cfg(feature = "azure")]
1081            VaultBinding::KeyVault(config) => {
1082                use crate::providers::vault::azure_key_vault::AzureKeyVault;
1083                use alien_azure_clients::keyvault::AzureKeyVaultSecretsClient;
1084                use alien_azure_clients::AzureTokenCache;
1085
1086                let azure_config = self.client_config.azure_config().ok_or_else(|| {
1087                    AlienError::new(ErrorData::ClientConfigInvalid {
1088                        platform: Platform::Azure,
1089                        message: "Azure config not available".to_string(),
1090                    })
1091                })?;
1092
1093                let client = Arc::new(AzureKeyVaultSecretsClient::new(
1094                    crate::http_client::create_http_client(),
1095                    AzureTokenCache::new(azure_config.clone()),
1096                ));
1097
1098                // Extract the vault name from the binding configuration
1099                let vault_name = config
1100                    .vault_name
1101                    .into_value(&binding_name, "vault_name")
1102                    .context(ErrorData::config_invalid(
1103                        binding_name,
1104                        "Failed to extract vault_name from KeyVault binding",
1105                    ))?;
1106
1107                // Construct the vault base URL
1108                // Azure Key Vault URLs typically follow: https://{vault-name}.vault.azure.net/
1109                let vault_base_url = format!("https://{}.vault.azure.net", vault_name);
1110
1111                let vault: Arc<dyn Vault> = Arc::new(AzureKeyVault::new(client, vault_base_url));
1112                Ok(vault)
1113            }
1114            #[cfg(not(feature = "azure"))]
1115            VaultBinding::KeyVault(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1116                feature: "azure".to_string(),
1117            })),
1118
1119            #[cfg(feature = "gcp")]
1120            VaultBinding::SecretManager(config) => {
1121                use crate::providers::vault::gcp_secret_manager::GcpSecretManagerVault;
1122                use alien_gcp_clients::secret_manager::SecretManagerClient;
1123
1124                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1125                    AlienError::new(ErrorData::ClientConfigInvalid {
1126                        platform: Platform::Gcp,
1127                        message: "GCP config not available".to_string(),
1128                    })
1129                })?;
1130
1131                let client = Arc::new(SecretManagerClient::new(
1132                    crate::http_client::create_http_client(),
1133                    gcp_config.clone(),
1134                ));
1135
1136                // Extract the vault prefix from the binding configuration
1137                let vault_prefix = config
1138                    .vault_prefix
1139                    .into_value(&binding_name, "vault_prefix")
1140                    .context(ErrorData::config_invalid(
1141                        binding_name,
1142                        "Failed to extract vault_prefix from SecretManager binding",
1143                    ))?;
1144
1145                let vault: Arc<dyn Vault> = Arc::new(GcpSecretManagerVault::new(
1146                    client,
1147                    vault_prefix,
1148                    gcp_config.project_id.clone(),
1149                ));
1150                Ok(vault)
1151            }
1152            #[cfg(not(feature = "gcp"))]
1153            VaultBinding::SecretManager(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1154                feature: "gcp".to_string(),
1155            })),
1156
1157            #[cfg(feature = "local")]
1158            VaultBinding::Local(config) => {
1159                use crate::providers::vault::local::LocalVault;
1160
1161                let vault_dir = config
1162                    .data_dir
1163                    .into_value(binding_name, "data_dir")
1164                    .context(ErrorData::config_invalid(
1165                        binding_name,
1166                        "Failed to extract data_dir from vault binding",
1167                    ))?;
1168
1169                let vault: Arc<dyn Vault> = Arc::new(LocalVault::new(
1170                    binding_name.to_string(),
1171                    std::path::PathBuf::from(vault_dir),
1172                ));
1173                Ok(vault)
1174            }
1175            #[cfg(not(feature = "local"))]
1176            VaultBinding::Local { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1177                feature: "local".to_string(),
1178            })),
1179
1180            #[cfg(feature = "kubernetes")]
1181            VaultBinding::KubernetesSecret(config) => {
1182                use crate::providers::vault::kubernetes_secret::KubernetesSecretVault;
1183                use alien_k8s_clients::{secrets::SecretsApi, KubernetesClient};
1184
1185                let kubernetes_config =
1186                    self.client_config.kubernetes_config().ok_or_else(|| {
1187                        AlienError::new(ErrorData::ClientConfigInvalid {
1188                            platform: Platform::Kubernetes,
1189                            message: "Kubernetes config not available".to_string(),
1190                        })
1191                    })?;
1192
1193                let kubernetes_client = KubernetesClient::new(kubernetes_config.clone())
1194                    .await
1195                    .context(ErrorData::CloudPlatformError {
1196                        message: "Failed to create Kubernetes client for vault".to_string(),
1197                        resource_id: None,
1198                    })?;
1199
1200                let client: Arc<dyn SecretsApi> = Arc::new(kubernetes_client);
1201
1202                // Extract namespace and vault prefix from binding
1203                let namespace = config
1204                    .namespace
1205                    .into_value(binding_name, "namespace")
1206                    .context(ErrorData::config_invalid(
1207                        binding_name,
1208                        "Failed to extract namespace from KubernetesSecret binding",
1209                    ))?;
1210
1211                let vault_prefix = config
1212                    .vault_prefix
1213                    .into_value(binding_name, "vault_prefix")
1214                    .context(ErrorData::config_invalid(
1215                        binding_name,
1216                        "Failed to extract vault_prefix from KubernetesSecret binding",
1217                    ))?;
1218
1219                let vault: Arc<dyn Vault> =
1220                    Arc::new(KubernetesSecretVault::new(client, namespace, vault_prefix));
1221                Ok(vault)
1222            }
1223            #[cfg(not(feature = "kubernetes"))]
1224            VaultBinding::KubernetesSecret(_) => {
1225                Err(AlienError::new(ErrorData::FeatureNotEnabled {
1226                    feature: "kubernetes".to_string(),
1227                }))
1228            }
1229        }?;
1230
1231        self.put_cache("vault", binding_name, result.clone()).await;
1232        Ok(result)
1233    }
1234
1235    async fn load_kv(&self, binding_name: &str) -> Result<Arc<dyn Kv>> {
1236        if let Some(cached) = self.get_cached::<Arc<dyn Kv>>("kv", binding_name).await {
1237            return Ok(cached);
1238        }
1239
1240        use alien_core::bindings::KvBinding;
1241
1242        let binding: KvBinding = self.parse_binding(binding_name, "KV")?;
1243
1244        let result: Arc<dyn Kv> = match binding {
1245            #[cfg(feature = "aws")]
1246            KvBinding::Dynamodb(config) => {
1247                use crate::providers::kv::aws_dynamodb::AwsDynamodbKv;
1248
1249                let table_name = config
1250                    .table_name
1251                    .into_value(binding_name, "table_name")
1252                    .context(ErrorData::config_invalid(
1253                        binding_name,
1254                        "Failed to extract table_name from DynamoDB binding",
1255                    ))?;
1256
1257                let aws_config = self.client_config.aws_config().ok_or_else(|| {
1258                    AlienError::new(ErrorData::ClientConfigInvalid {
1259                        platform: Platform::Aws,
1260                        message: "AWS config not available".to_string(),
1261                    })
1262                })?;
1263
1264                let credentials =
1265                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
1266                        .await
1267                        .context(ErrorData::ClientConfigInvalid {
1268                            platform: Platform::Aws,
1269                            message: "Failed to create AWS credential provider".to_string(),
1270                        })?;
1271                let dynamodb_client = alien_aws_clients::dynamodb::DynamoDbClient::new(
1272                    crate::http_client::create_http_client(),
1273                    credentials,
1274                );
1275                let kv_impl = AwsDynamodbKv::new(table_name, dynamodb_client);
1276                let kv: Arc<dyn Kv> = Arc::new(kv_impl);
1277                Ok(kv)
1278            }
1279            #[cfg(not(feature = "aws"))]
1280            KvBinding::Dynamodb(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1281                feature: "aws".to_string(),
1282            })),
1283
1284            #[cfg(feature = "gcp")]
1285            KvBinding::Firestore(config) => {
1286                use crate::providers::kv::gcp_firestore::GcpFirestoreKv;
1287                use alien_gcp_clients::firestore::FirestoreClient;
1288
1289                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1290                    AlienError::new(ErrorData::ClientConfigInvalid {
1291                        platform: Platform::Gcp,
1292                        message: "GCP config not available".to_string(),
1293                    })
1294                })?;
1295
1296                let client = FirestoreClient::new(
1297                    crate::http_client::create_http_client(),
1298                    gcp_config.clone(),
1299                );
1300
1301                let project_id = config
1302                    .project_id
1303                    .into_value(binding_name, "project_id")
1304                    .context(ErrorData::config_invalid(
1305                        binding_name,
1306                        "Failed to extract project_id from Firestore binding",
1307                    ))?;
1308
1309                let database_id = config
1310                    .database_id
1311                    .into_value(binding_name, "database_id")
1312                    .context(ErrorData::config_invalid(
1313                        binding_name,
1314                        "Failed to extract database_id from Firestore binding",
1315                    ))?;
1316
1317                let collection_name = config
1318                    .collection_name
1319                    .into_value(binding_name, "collection_name")
1320                    .context(ErrorData::config_invalid(
1321                        binding_name,
1322                        "Failed to extract collection_name from Firestore binding",
1323                    ))?;
1324
1325                let kv: Arc<dyn Kv> = Arc::new(GcpFirestoreKv::new(
1326                    client,
1327                    project_id,
1328                    database_id,
1329                    collection_name,
1330                )?);
1331                Ok(kv)
1332            }
1333            #[cfg(not(feature = "gcp"))]
1334            KvBinding::Firestore(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1335                feature: "gcp".to_string(),
1336            })),
1337
1338            #[cfg(feature = "azure")]
1339            KvBinding::TableStorage(config) => {
1340                use crate::providers::kv::azure_table_storage::AzureTableStorageKv;
1341                use alien_azure_clients::tables::AzureTableStorageClient;
1342                use alien_azure_clients::AzureTokenCache;
1343
1344                let azure_config = self.client_config.azure_config().ok_or_else(|| {
1345                    AlienError::new(ErrorData::ClientConfigInvalid {
1346                        platform: Platform::Azure,
1347                        message: "Azure config not available".to_string(),
1348                    })
1349                })?;
1350
1351                let resource_group_name = config
1352                    .resource_group_name
1353                    .into_value(binding_name, "resource_group_name")
1354                    .context(ErrorData::config_invalid(
1355                        binding_name,
1356                        "Failed to extract resource_group_name from TableStorage binding",
1357                    ))?;
1358
1359                let account_name = config
1360                    .account_name
1361                    .into_value(binding_name, "account_name")
1362                    .context(ErrorData::config_invalid(
1363                        binding_name,
1364                        "Failed to extract account_name from TableStorage binding",
1365                    ))?;
1366
1367                let table_name = config
1368                    .table_name
1369                    .into_value(binding_name, "table_name")
1370                    .context(ErrorData::config_invalid(
1371                        binding_name,
1372                        "Failed to extract table_name from TableStorage binding",
1373                    ))?;
1374
1375                let client = AzureTableStorageClient::new(
1376                    crate::http_client::create_http_client(),
1377                    AzureTokenCache::new(azure_config.clone()),
1378                );
1379
1380                let kv_impl =
1381                    AzureTableStorageKv::new(client, resource_group_name, account_name, table_name);
1382                let kv: Arc<dyn Kv> = Arc::new(kv_impl);
1383                Ok(kv)
1384            }
1385            #[cfg(not(feature = "azure"))]
1386            KvBinding::TableStorage(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1387                feature: "azure".to_string(),
1388            })),
1389
1390            #[cfg(feature = "local")]
1391            KvBinding::Local(local_binding) => {
1392                use crate::providers::kv::local::LocalKv;
1393                use std::path::PathBuf;
1394
1395                // Get data directory from binding
1396                let data_dir = PathBuf::from(
1397                    local_binding
1398                        .data_dir
1399                        .into_value(binding_name, "data_dir")
1400                        .context(ErrorData::config_invalid(
1401                            binding_name,
1402                            "Failed to extract data_dir from Local binding",
1403                        ))?,
1404                );
1405
1406                // Create local disk-persisted KV implementation
1407                let kv_impl = LocalKv::new(data_dir).await?;
1408
1409                let kv: Arc<dyn Kv> = Arc::new(kv_impl);
1410                Ok(kv)
1411            }
1412            #[cfg(not(feature = "local"))]
1413            KvBinding::Local { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1414                feature: "local".to_string(),
1415            })),
1416
1417            KvBinding::Redis(_) => Err(AlienError::new(ErrorData::UnsupportedBindingProvider {
1418                binding_name: binding_name.to_string(),
1419                env_var: binding_env_var(binding_name),
1420                provider: "redis".to_string(),
1421            })),
1422        }?;
1423
1424        self.put_cache("kv", binding_name, result.clone()).await;
1425        Ok(result)
1426    }
1427
1428    async fn load_postgres(&self, binding_name: &str) -> Result<Arc<dyn Postgres>> {
1429        let binding: PostgresBinding = self.parse_binding(binding_name, "Postgres")?;
1430        self.postgres.load(binding_name, &binding).await
1431    }
1432
1433    async fn load_queue(&self, binding_name: &str) -> Result<Arc<dyn Queue>> {
1434        if let Some(cached) = self
1435            .get_cached::<Arc<dyn Queue>>("queue", binding_name)
1436            .await
1437        {
1438            return Ok(cached);
1439        }
1440
1441        use alien_core::bindings::QueueBinding;
1442
1443        let binding: QueueBinding = self.parse_binding(binding_name, "Queue")?;
1444
1445        let result: Arc<dyn Queue> = match binding {
1446            #[cfg(feature = "aws")]
1447            QueueBinding::Sqs(config) => {
1448                use crate::providers::queue::aws_sqs::AwsSqsQueue;
1449
1450                let queue_url = config
1451                    .queue_url
1452                    .into_value(binding_name, "queue_url")
1453                    .context(ErrorData::config_invalid(
1454                        binding_name,
1455                        "Failed to extract queue_url from SQS binding",
1456                    ))?;
1457
1458                let aws_config = self.client_config.aws_config().ok_or_else(|| {
1459                    AlienError::new(ErrorData::ClientConfigInvalid {
1460                        platform: Platform::Aws,
1461                        message: "AWS config not available".to_string(),
1462                    })
1463                })?;
1464                let credentials =
1465                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
1466                        .await
1467                        .context(ErrorData::ClientConfigInvalid {
1468                            platform: Platform::Aws,
1469                            message: "Failed to create AWS credential provider".to_string(),
1470                        })?;
1471                let client = alien_aws_clients::sqs::SqsClient::new(
1472                    crate::http_client::create_http_client(),
1473                    credentials,
1474                );
1475                let q: Arc<dyn Queue> = Arc::new(AwsSqsQueue::new(queue_url, client));
1476                Ok(q)
1477            }
1478            #[cfg(not(feature = "aws"))]
1479            QueueBinding::Sqs(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1480                feature: "aws".to_string(),
1481            })),
1482
1483            #[cfg(feature = "gcp")]
1484            QueueBinding::Pubsub(config) => {
1485                use crate::providers::queue::gcp_pubsub::GcpPubSubQueue;
1486                let topic_name = config.topic.into_value(binding_name, "topic").context(
1487                    ErrorData::config_invalid(binding_name, "Failed to extract topic"),
1488                )?;
1489                let subscription_name = config
1490                    .subscription
1491                    .into_value(binding_name, "subscription")
1492                    .context(ErrorData::config_invalid(
1493                        binding_name,
1494                        "Failed to extract subscription",
1495                    ))?;
1496                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1497                    AlienError::new(ErrorData::ClientConfigInvalid {
1498                        platform: Platform::Gcp,
1499                        message: "GCP config not available".to_string(),
1500                    })
1501                })?;
1502
1503                // Pass short names — PubSubClient methods prepend the project prefix
1504                let topic = if let Some(short) =
1505                    topic_name.strip_prefix(&format!("projects/{}/topics/", gcp_config.project_id))
1506                {
1507                    short.to_string()
1508                } else {
1509                    topic_name
1510                };
1511                let subscription = if let Some(short) = subscription_name.strip_prefix(&format!(
1512                    "projects/{}/subscriptions/",
1513                    gcp_config.project_id
1514                )) {
1515                    short.to_string()
1516                } else {
1517                    subscription_name
1518                };
1519
1520                let q: Arc<dyn Queue> =
1521                    Arc::new(GcpPubSubQueue::new(topic, subscription, gcp_config.clone()).await?);
1522                Ok(q)
1523            }
1524            #[cfg(not(feature = "gcp"))]
1525            QueueBinding::Pubsub(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1526                feature: "gcp".to_string(),
1527            })),
1528
1529            #[cfg(feature = "azure")]
1530            QueueBinding::Servicebus(config) => {
1531                use crate::providers::queue::azure_service_bus::AzureServiceBusQueue;
1532                let namespace = config
1533                    .namespace
1534                    .into_value(binding_name, "namespace")
1535                    .context(ErrorData::config_invalid(
1536                        binding_name,
1537                        "Failed to extract namespace",
1538                    ))?;
1539                let queue_name = config
1540                    .queue_name
1541                    .into_value(binding_name, "queue_name")
1542                    .context(ErrorData::config_invalid(
1543                        binding_name,
1544                        "Failed to extract queue_name",
1545                    ))?;
1546                let azure_config = self.client_config.azure_config().ok_or_else(|| {
1547                    AlienError::new(ErrorData::ClientConfigInvalid {
1548                        platform: Platform::Azure,
1549                        message: "Azure config not available".to_string(),
1550                    })
1551                })?;
1552                let q: Arc<dyn Queue> = Arc::new(
1553                    AzureServiceBusQueue::new(namespace, queue_name, azure_config.clone()).await?,
1554                );
1555                Ok(q)
1556            }
1557            #[cfg(not(feature = "azure"))]
1558            QueueBinding::Servicebus(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1559                feature: "azure".to_string(),
1560            })),
1561
1562            #[cfg(feature = "local")]
1563            QueueBinding::Local(config) => {
1564                use crate::providers::queue::local::LocalQueue;
1565
1566                let queue = LocalQueue::from_binding(config).await?;
1567                let q: Arc<dyn Queue> = Arc::new(queue);
1568                Ok(q)
1569            }
1570            #[cfg(not(feature = "local"))]
1571            QueueBinding::Local(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1572                feature: "local".to_string(),
1573            })),
1574        }?;
1575
1576        self.put_cache("queue", binding_name, result.clone()).await;
1577        Ok(result)
1578    }
1579
1580    async fn load_worker(&self, binding_name: &str) -> Result<Arc<dyn Worker>> {
1581        use alien_core::bindings::WorkerBinding;
1582
1583        let binding: WorkerBinding = self.parse_binding(binding_name, "worker")?;
1584
1585        match binding {
1586            #[cfg(feature = "aws")]
1587            WorkerBinding::Lambda(lambda_binding) => {
1588                use crate::providers::worker::LambdaWorker;
1589
1590                let aws_config = self.client_config.aws_config().ok_or_else(|| {
1591                    AlienError::new(ErrorData::ClientConfigInvalid {
1592                        platform: Platform::Aws,
1593                        message: "AWS config not available".to_string(),
1594                    })
1595                })?;
1596                let credentials =
1597                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
1598                        .await
1599                        .context(ErrorData::ClientConfigInvalid {
1600                            platform: Platform::Aws,
1601                            message: "Failed to create AWS credential provider".to_string(),
1602                        })?;
1603                let client = crate::http_client::create_http_client();
1604
1605                let function_impl = LambdaWorker::new(client, credentials, lambda_binding);
1606                let function: Arc<dyn Worker> = Arc::new(function_impl);
1607                Ok(function)
1608            }
1609            #[cfg(not(feature = "aws"))]
1610            WorkerBinding::Lambda(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1611                feature: "aws".to_string(),
1612            })),
1613
1614            #[cfg(feature = "gcp")]
1615            WorkerBinding::CloudRun(cloudrun_binding) => {
1616                use crate::providers::worker::CloudRunWorker;
1617
1618                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1619                    AlienError::new(ErrorData::ClientConfigInvalid {
1620                        platform: Platform::Gcp,
1621                        message: "GCP config not available".to_string(),
1622                    })
1623                })?;
1624                let client = crate::http_client::create_http_client();
1625
1626                let function_impl =
1627                    CloudRunWorker::new(client, gcp_config.clone(), cloudrun_binding);
1628                let function: Arc<dyn Worker> = Arc::new(function_impl);
1629                Ok(function)
1630            }
1631            #[cfg(not(feature = "gcp"))]
1632            WorkerBinding::CloudRun(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1633                feature: "gcp".to_string(),
1634            })),
1635
1636            #[cfg(feature = "azure")]
1637            WorkerBinding::ContainerApp(container_app_binding) => {
1638                use crate::providers::worker::ContainerAppWorker;
1639
1640                let azure_config = self.client_config.azure_config().ok_or_else(|| {
1641                    AlienError::new(ErrorData::ClientConfigInvalid {
1642                        platform: Platform::Azure,
1643                        message: "Azure config not available".to_string(),
1644                    })
1645                })?;
1646                let client = crate::http_client::create_http_client();
1647
1648                let function_impl =
1649                    ContainerAppWorker::new(client, azure_config.clone(), container_app_binding);
1650                let function: Arc<dyn Worker> = Arc::new(function_impl);
1651                Ok(function)
1652            }
1653            #[cfg(not(feature = "azure"))]
1654            WorkerBinding::ContainerApp(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1655                feature: "azure".to_string(),
1656            })),
1657
1658            #[cfg(feature = "local")]
1659            WorkerBinding::Local(local_binding) => {
1660                use crate::providers::worker::LocalWorker;
1661
1662                let function_impl = LocalWorker::new(local_binding);
1663                let function: Arc<dyn Worker> = Arc::new(function_impl);
1664                Ok(function)
1665            }
1666            #[cfg(not(feature = "local"))]
1667            WorkerBinding::Local(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1668                feature: "local".to_string(),
1669            })),
1670
1671            #[cfg(feature = "kubernetes")]
1672            WorkerBinding::Kubernetes(kubernetes_binding) => {
1673                use crate::providers::worker::KubernetesWorker;
1674
1675                let function_impl =
1676                    KubernetesWorker::new(binding_name.to_string(), kubernetes_binding)?;
1677                let function: Arc<dyn Worker> = Arc::new(function_impl);
1678                Ok(function)
1679            }
1680            #[cfg(not(feature = "kubernetes"))]
1681            WorkerBinding::Kubernetes(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1682                feature: "kubernetes".to_string(),
1683            })),
1684        }
1685    }
1686
1687    async fn load_container(
1688        &self,
1689        binding_name: &str,
1690    ) -> Result<Arc<dyn crate::traits::Container>> {
1691        use alien_core::bindings::ContainerBinding;
1692
1693        let binding: ContainerBinding = self.parse_binding(binding_name, "container")?;
1694
1695        match binding {
1696            ContainerBinding::Horizon(horizon_binding) => {
1697                use crate::providers::container::HorizonContainer;
1698
1699                let container_impl = HorizonContainer::new(horizon_binding)?;
1700                let container: Arc<dyn crate::traits::Container> = Arc::new(container_impl);
1701                Ok(container)
1702            }
1703
1704            #[cfg(feature = "local")]
1705            ContainerBinding::Local(local_binding) => {
1706                use crate::providers::container::LocalContainer;
1707
1708                let container_impl = LocalContainer::new(local_binding)?;
1709                let container: Arc<dyn crate::traits::Container> = Arc::new(container_impl);
1710                Ok(container)
1711            }
1712            #[cfg(not(feature = "local"))]
1713            ContainerBinding::Local(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1714                feature: "local".to_string(),
1715            })),
1716
1717            #[cfg(feature = "kubernetes")]
1718            ContainerBinding::Kubernetes(kubernetes_binding) => {
1719                use crate::providers::container::KubernetesContainer;
1720
1721                let container_impl =
1722                    KubernetesContainer::new(binding_name.to_string(), kubernetes_binding)?;
1723                let container: Arc<dyn crate::traits::Container> = Arc::new(container_impl);
1724                Ok(container)
1725            }
1726            #[cfg(not(feature = "kubernetes"))]
1727            ContainerBinding::Kubernetes(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1728                feature: "kubernetes".to_string(),
1729            })),
1730        }
1731    }
1732
1733    async fn load_service_account(
1734        &self,
1735        binding_name: &str,
1736    ) -> Result<Arc<dyn crate::traits::ServiceAccount>> {
1737        use alien_core::bindings::ServiceAccountBinding;
1738
1739        let binding: ServiceAccountBinding = self.parse_binding(binding_name, "service account")?;
1740
1741        match binding {
1742            #[cfg(feature = "aws")]
1743            ServiceAccountBinding::AwsIam(aws_binding) => {
1744                use crate::providers::service_account::aws_iam::AwsIamServiceAccount;
1745
1746                let aws_config = self.client_config.aws_config().ok_or_else(|| {
1747                    AlienError::new(ErrorData::ClientConfigInvalid {
1748                        platform: Platform::Aws,
1749                        message: "AWS config not available".to_string(),
1750                    })
1751                })?;
1752                let client = crate::http_client::create_http_client();
1753
1754                let service_account_impl =
1755                    AwsIamServiceAccount::new(client, aws_config.clone(), aws_binding);
1756                let service_account: Arc<dyn crate::traits::ServiceAccount> =
1757                    Arc::new(service_account_impl);
1758                Ok(service_account)
1759            }
1760            #[cfg(not(feature = "aws"))]
1761            ServiceAccountBinding::AwsIam(_) => {
1762                Err(AlienError::new(ErrorData::FeatureNotEnabled {
1763                    feature: "aws".to_string(),
1764                }))
1765            }
1766
1767            #[cfg(feature = "gcp")]
1768            ServiceAccountBinding::GcpServiceAccount(gcp_binding) => {
1769                use crate::providers::service_account::gcp_service_account::GcpServiceAccount;
1770
1771                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1772                    AlienError::new(ErrorData::ClientConfigInvalid {
1773                        platform: Platform::Gcp,
1774                        message: "GCP config not available".to_string(),
1775                    })
1776                })?;
1777                let client = crate::http_client::create_http_client();
1778
1779                let service_account_impl =
1780                    GcpServiceAccount::new(client, gcp_config.clone(), gcp_binding);
1781                let service_account: Arc<dyn crate::traits::ServiceAccount> =
1782                    Arc::new(service_account_impl);
1783                Ok(service_account)
1784            }
1785            #[cfg(not(feature = "gcp"))]
1786            ServiceAccountBinding::GcpServiceAccount(_) => {
1787                Err(AlienError::new(ErrorData::FeatureNotEnabled {
1788                    feature: "gcp".to_string(),
1789                }))
1790            }
1791
1792            #[cfg(feature = "azure")]
1793            ServiceAccountBinding::AzureManagedIdentity(azure_binding) => {
1794                use crate::providers::service_account::azure_managed_identity::AzureManagedIdentityServiceAccount;
1795
1796                let azure_config = self.client_config.azure_config().ok_or_else(|| {
1797                    AlienError::new(ErrorData::ClientConfigInvalid {
1798                        platform: Platform::Azure,
1799                        message: "Azure config not available".to_string(),
1800                    })
1801                })?;
1802
1803                let service_account_impl =
1804                    AzureManagedIdentityServiceAccount::new(azure_config.clone(), azure_binding);
1805                let service_account: Arc<dyn crate::traits::ServiceAccount> =
1806                    Arc::new(service_account_impl);
1807                Ok(service_account)
1808            }
1809            #[cfg(not(feature = "azure"))]
1810            ServiceAccountBinding::AzureManagedIdentity(_) => {
1811                Err(AlienError::new(ErrorData::FeatureNotEnabled {
1812                    feature: "azure".to_string(),
1813                }))
1814            }
1815        }
1816    }
1817
1818    async fn load_sandbox(&self, binding_name: &str) -> Result<Arc<dyn crate::traits::Sandbox>> {
1819        use alien_core::bindings::SandboxBinding;
1820
1821        // Parsed before dispatch so a malformed binding fails as a config error rather than as
1822        // a missing backend, which would send a reader looking in the wrong place.
1823        let binding: SandboxBinding = self.parse_binding(binding_name, "sandbox")?;
1824
1825        match binding {
1826            #[cfg(feature = "local")]
1827            SandboxBinding::Local(local_binding) => {
1828                use crate::providers::sandbox::local::LocalSandbox;
1829
1830                let sandbox: Arc<dyn crate::traits::Sandbox> =
1831                    Arc::new(LocalSandbox::new(binding_name, &local_binding).await?);
1832                Ok(sandbox)
1833            }
1834            #[cfg(feature = "kubernetes")]
1835            SandboxBinding::Kubernetes(kubernetes_binding) => {
1836                use crate::providers::sandbox::kubernetes::KubernetesSandbox;
1837
1838                let sandbox: Arc<dyn crate::traits::Sandbox> = Arc::new(KubernetesSandbox::new(
1839                    binding_name,
1840                    &kubernetes_binding,
1841                    binding_name,
1842                )?);
1843                Ok(sandbox)
1844            }
1845            #[cfg(feature = "gcp")]
1846            SandboxBinding::Gcp(gcp_binding) => {
1847                use crate::providers::sandbox::gcp::GcpSandbox;
1848
1849                let sandbox: Arc<dyn crate::traits::Sandbox> =
1850                    Arc::new(GcpSandbox::new(binding_name, &gcp_binding)?);
1851                Ok(sandbox)
1852            }
1853            #[cfg(feature = "azure")]
1854            SandboxBinding::Azure(azure_binding) => {
1855                use crate::providers::sandbox::azure::AzureSandbox;
1856                use alien_azure_clients::azure::sandbox_data_plane::AzureSandboxDataPlaneClient;
1857                use alien_azure_clients::azure::token_cache::AzureTokenCache;
1858
1859                let azure_config = self.client_config.azure_config().ok_or_else(|| {
1860                    AlienError::new(ErrorData::ClientConfigInvalid {
1861                        platform: Platform::Azure,
1862                        message: "Azure config not available".to_string(),
1863                    })
1864                })?;
1865
1866                let invalid = |field: &str| {
1867                    AlienError::new(ErrorData::BindingConfigInvalid {
1868                        binding_name: binding_name.to_string(),
1869                        env_var: alien_core::bindings::binding_env_var_name(binding_name),
1870                        reason: format!("sandbox binding field '{field}' is not a concrete value"),
1871                    })
1872                };
1873
1874                let group = azure_binding
1875                    .sandbox_group
1876                    .into_value(binding_name, "sandboxGroup")
1877                    .map_err(|_| invalid("sandboxGroup"))?;
1878                let region = azure_binding
1879                    .region
1880                    .into_value(binding_name, "region")
1881                    .map_err(|_| invalid("region"))?;
1882                let resource_group = azure_binding
1883                    .resource_group
1884                    .into_value(binding_name, "resourceGroup")
1885                    .map_err(|_| invalid("resourceGroup"))?;
1886
1887                let client = AzureSandboxDataPlaneClient::new(
1888                    reqwest::Client::new(),
1889                    &region,
1890                    &resource_group,
1891                    AzureTokenCache::new(azure_config.clone()),
1892                );
1893
1894                // Session ceilings come from the resource, not the caller — an application must
1895                // not be able to raise its own by asking.
1896                let sandbox: Arc<dyn crate::traits::Sandbox> = Arc::new(AzureSandbox::new(
1897                    Arc::new(client),
1898                    group,
1899                    "ubuntu".to_string(),
1900                    "1000m".to_string(),
1901                    "2048Mi".to_string(),
1902                ));
1903                Ok(sandbox)
1904            }
1905            #[cfg(feature = "aws")]
1906            SandboxBinding::Aws(aws_binding) => {
1907                use crate::providers::sandbox::aws::AwsSandbox;
1908                use alien_aws_clients::aws::lambda_microvms::LambdaMicrovmsClient;
1909
1910                let aws_config = self.client_config.aws_config().ok_or_else(|| {
1911                    AlienError::new(ErrorData::ClientConfigInvalid {
1912                        platform: Platform::Aws,
1913                        message: "AWS config not available".to_string(),
1914                    })
1915                })?;
1916
1917                let invalid = |field: &str| {
1918                    AlienError::new(ErrorData::BindingConfigInvalid {
1919                        binding_name: binding_name.to_string(),
1920                        env_var: alien_core::bindings::binding_env_var_name(binding_name),
1921                        reason: format!("sandbox binding field '{field}' is not a concrete value"),
1922                    })
1923                };
1924
1925                let image_arn = aws_binding
1926                    .image_arn
1927                    .into_value(binding_name, "imageArn")
1928                    .map_err(|_| invalid("imageArn"))?;
1929                let image_version = aws_binding
1930                    .image_version
1931                    .into_value(binding_name, "imageVersion")
1932                    .map_err(|_| invalid("imageVersion"))?;
1933                let region = aws_binding
1934                    .region
1935                    .into_value(binding_name, "region")
1936                    .map_err(|_| invalid("region"))?;
1937                if aws_binding.execution_role_arn.is_some() {
1938                    // A MicroVM started with an execution role serves that role's live
1939                    // credentials to the session over link-local instance metadata, which no
1940                    // egress connector governs — measured under `deny` and `allow` alike. A
1941                    // sandbox runs untrusted code, so the role is refused rather than attached.
1942                    return Err(AlienError::new(ErrorData::BindingConfigInvalid {
1943                        binding_name: binding_name.to_string(),
1944                        env_var: alien_core::bindings::binding_env_var_name(binding_name),
1945                        reason: "sandbox binding field 'executionRoleArn' is set; a session can \
1946                                 read that role's credentials from instance metadata, which the \
1947                                 egress connector does not reach"
1948                            .to_string(),
1949                    }));
1950                }
1951
1952                // The sandbox lives where its image was built, which is not necessarily where
1953                // the workload runs; signing against the workload's region would 404.
1954                let mut config = aws_config.clone();
1955                config.region = region;
1956
1957                let credentials = alien_aws_clients::AwsCredentialProvider::from_config(config)
1958                    .await
1959                    .context(ErrorData::BindingSetupFailed {
1960                        binding_type: "AWS sandbox".to_string(),
1961                        reason: "Failed to create credential provider".to_string(),
1962                    })?;
1963
1964                let client = LambdaMicrovmsClient::new(reqwest::Client::new(), credentials);
1965
1966                let egress_connector_arns = aws_binding
1967                    .egress_connector_arns
1968                    .into_iter()
1969                    .map(|value| {
1970                        value
1971                            .into_value(binding_name, "egressConnectorArns")
1972                            .map_err(|_| invalid("egressConnectorArns"))
1973                    })
1974                    .collect::<Result<Vec<_>>>()?;
1975                // A MicroVM started with no connector reaches the internet, so the two fields have
1976                // to agree: under `deny` setup always emits a connector, and an empty list is a
1977                // binding that did not come from a generated module rather than an open sandbox.
1978                if egress_connector_arns.is_empty() != aws_binding.allow_egress {
1979                    let reason = if aws_binding.allow_egress {
1980                        "sandbox binding declares open egress and also names egress connectors; \
1981                         a session cannot be both open and routed through a denying connector"
1982                    } else {
1983                        "sandbox binding field 'egressConnectorArns' is empty; a MicroVM started \
1984                         with no egress connector reaches the public internet"
1985                    };
1986                    return Err(AlienError::new(ErrorData::BindingConfigInvalid {
1987                        binding_name: binding_name.to_string(),
1988                        env_var: alien_core::bindings::binding_env_var_name(binding_name),
1989                        reason: reason.to_string(),
1990                    }));
1991                }
1992
1993                let sandbox: Arc<dyn crate::traits::Sandbox> = Arc::new(AwsSandbox::new(
1994                    Arc::new(client),
1995                    image_arn,
1996                    image_version,
1997                    egress_connector_arns,
1998                    aws_binding.preview_ports,
1999                    aws_binding.idle_suspend_seconds,
2000                    aws_binding.max_lifetime_seconds,
2001                ));
2002                Ok(sandbox)
2003            }
2004            // A backend whose feature is off reports which one, rather than a bare
2005            // "unsupported" that sends a reader looking at the platform instead of the build.
2006            #[cfg(not(feature = "aws"))]
2007            SandboxBinding::Aws(_) => Err(not_built("aws")),
2008            #[cfg(not(feature = "azure"))]
2009            SandboxBinding::Azure(_) => Err(not_built("azure")),
2010            #[cfg(not(feature = "gcp"))]
2011            SandboxBinding::Gcp(_) => Err(not_built("gcp")),
2012            #[cfg(not(feature = "kubernetes"))]
2013            SandboxBinding::Kubernetes(_) => Err(not_built("kubernetes")),
2014            #[cfg(not(feature = "local"))]
2015            SandboxBinding::Local(_) => Err(not_built("local")),
2016        }
2017    }
2018}
2019
2020/// A binding whose backend was compiled out.
2021///
2022/// Unused when every backend feature is on, which is the build that ships.
2023#[allow(dead_code)]
2024///
2025/// Names the backend rather than reporting a bare "unsupported", which would send a reader
2026/// looking at the platform instead of at the build.
2027fn not_built(backend: &str) -> AlienError<ErrorData> {
2028    AlienError::new(ErrorData::OperationNotSupported {
2029        operation: format!("load_sandbox({backend})"),
2030        reason: "this build does not include the sandbox backend for that platform".to_string(),
2031    })
2032}
2033
2034#[cfg(test)]
2035mod tests {
2036    use super::*;
2037    use alien_core::ENV_ALIEN_DEPLOYMENT_TYPE;
2038
2039    fn kubernetes_aws_env() -> HashMap<String, String> {
2040        HashMap::from([
2041            (
2042                ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2043                Platform::Kubernetes.as_str().to_string(),
2044            ),
2045            (
2046                ENV_OPERATOR_BASE_PLATFORM.to_string(),
2047                Platform::Aws.as_str().to_string(),
2048            ),
2049            (
2050                "KUBERNETES_SERVICE_HOST".to_string(),
2051                "10.0.0.1".to_string(),
2052            ),
2053            ("KUBERNETES_SERVICE_PORT".to_string(), "443".to_string()),
2054            ("AWS_REGION".to_string(), "us-east-1".to_string()),
2055            ("AWS_ACCOUNT_ID".to_string(), "123456789012".to_string()),
2056            ("AWS_ACCESS_KEY_ID".to_string(), "test".to_string()),
2057            ("AWS_SECRET_ACCESS_KEY".to_string(), "test".to_string()),
2058        ])
2059    }
2060
2061    #[cfg(feature = "kubernetes")]
2062    fn kubernetes_azure_env() -> HashMap<String, String> {
2063        HashMap::from([
2064            (
2065                ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2066                Platform::Kubernetes.as_str().to_string(),
2067            ),
2068            (
2069                ENV_OPERATOR_BASE_PLATFORM.to_string(),
2070                Platform::Azure.as_str().to_string(),
2071            ),
2072            (
2073                "KUBERNETES_SERVICE_HOST".to_string(),
2074                "10.0.0.1".to_string(),
2075            ),
2076            ("KUBERNETES_SERVICE_PORT".to_string(), "443".to_string()),
2077            (
2078                "AZURE_SUBSCRIPTION_ID".to_string(),
2079                "00000000-0000-0000-0000-000000000000".to_string(),
2080            ),
2081            (
2082                "AZURE_TENANT_ID".to_string(),
2083                "11111111-1111-1111-1111-111111111111".to_string(),
2084            ),
2085            ("AZURE_REGION".to_string(), "eastus".to_string()),
2086            (
2087                "AZURE_CLIENT_ID".to_string(),
2088                "22222222-2222-2222-2222-222222222222".to_string(),
2089            ),
2090            (
2091                "AZURE_FEDERATED_TOKEN_FILE".to_string(),
2092                "/var/run/secrets/azure/tokens/azure-identity-token".to_string(),
2093            ),
2094            (
2095                "AZURE_AUTHORITY_HOST".to_string(),
2096                "https://login.microsoftonline.com/".to_string(),
2097            ),
2098        ])
2099    }
2100
2101    #[tokio::test]
2102    async fn lazy_env_provider_defers_cloud_client_config_until_binding_use() {
2103        let env = HashMap::from([
2104            (
2105                ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2106                Platform::Aws.as_str().to_string(),
2107            ),
2108            ("AWS_EC2_METADATA_DISABLED".to_string(), "true".to_string()),
2109            (
2110                "AWS_PROFILE".to_string(),
2111                "__alien_missing_test_profile__".to_string(),
2112            ),
2113            (
2114                "ALIEN_SECRETS_BINDING".to_string(),
2115                r#"{"service":"parameter-store","vaultPrefix":"test-secrets"}"#.to_string(),
2116            ),
2117        ]);
2118
2119        let provider = BindingsProvider::from_env_lazy(env)
2120            .expect("lazy provider construction should validate binding JSON without AWS config");
2121
2122        let error = provider
2123            .load_vault("secrets")
2124            .await
2125            .expect_err("binding use should still require AWS client config");
2126
2127        assert_eq!(error.code, "CLIENT_CONFIG_INVALID");
2128    }
2129
2130    /// The construction-time binding-JSON parse is load-bearing: `select`
2131    /// reuses it instead of re-parsing `env` on first use, so malformed JSON
2132    /// must fail at construction — for BOTH lazy constructors.
2133    #[test]
2134    fn malformed_binding_json_fails_at_construction_for_both_lazy_constructors() {
2135        let env = HashMap::from([
2136            (
2137                ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2138                Platform::Aws.as_str().to_string(),
2139            ),
2140            ("ALIEN_FILES_BINDING".to_string(), "not-json".to_string()),
2141        ]);
2142
2143        let error = BindingsProvider::from_env_lazy(env.clone())
2144            .expect_err("from_env_lazy must reject malformed binding JSON at construction");
2145        assert_eq!(error.code, "BINDING_CONFIG_INVALID");
2146
2147        let error = BindingsProvider::from_env_deferred(env)
2148            .expect_err("from_env_deferred must reject malformed binding JSON at construction");
2149        assert_eq!(error.code, "BINDING_CONFIG_INVALID");
2150    }
2151
2152    // Building the KubernetesCloud client config requires kubernetes support
2153    // to be compiled in; without the feature, `from_env` rejects the config.
2154    #[cfg(feature = "kubernetes")]
2155    #[tokio::test]
2156    async fn from_env_builds_kubernetes_cloud_config_when_base_platform_is_set() {
2157        let provider = BindingsProvider::from_env(kubernetes_aws_env())
2158            .await
2159            .unwrap();
2160
2161        assert!(provider.client_config.kubernetes_config().is_some());
2162        assert!(provider.client_config.aws_config().is_some());
2163        assert!(matches!(
2164            provider.client_config,
2165            ClientConfig::KubernetesCloud { .. }
2166        ));
2167    }
2168
2169    #[cfg(feature = "kubernetes")]
2170    #[tokio::test]
2171    async fn from_env_builds_kubernetes_cloud_config_for_azure_workload_identity() {
2172        let provider = BindingsProvider::from_env(kubernetes_azure_env())
2173            .await
2174            .unwrap();
2175
2176        assert!(provider.client_config.kubernetes_config().is_some());
2177        assert!(provider.client_config.azure_config().is_some());
2178        assert!(matches!(
2179            provider.client_config,
2180            ClientConfig::KubernetesCloud { .. }
2181        ));
2182    }
2183
2184    #[tokio::test]
2185    async fn from_env_rejects_non_cloud_kubernetes_base_platform() {
2186        let mut env = kubernetes_aws_env();
2187        env.insert(
2188            ENV_OPERATOR_BASE_PLATFORM.to_string(),
2189            Platform::Kubernetes.as_str().to_string(),
2190        );
2191
2192        let error = BindingsProvider::from_env(env).await.unwrap_err();
2193
2194        assert!(error.to_string().contains(ENV_OPERATOR_BASE_PLATFORM));
2195    }
2196
2197    #[tokio::test]
2198    async fn load_storage_for_unconfigured_binding_returns_binding_not_configured() {
2199        let env = HashMap::from([(
2200            ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2201            Platform::Local.as_str().to_string(),
2202        )]);
2203        let provider = BindingsProvider::from_env(env)
2204            .await
2205            .expect("provider with no bindings configured should still construct");
2206
2207        let error = provider
2208            .load_storage("files")
2209            .await
2210            .expect_err("binding that was never configured should error");
2211
2212        assert_eq!(error.code, "BINDING_NOT_CONFIGURED");
2213        assert!(
2214            error.to_string().contains("ALIEN_FILES_BINDING"),
2215            "message should name the derived env var, got: {error}"
2216        );
2217    }
2218
2219    /// A MicroVM with no egress connector reaches the internet, so the binding's two egress
2220    /// fields have to agree: an empty list is how `allow` travels, and it is a fail-open default
2221    /// unless `allowEgress` says so. Both disagreements are refused, and `deny` still loads.
2222    #[cfg(feature = "aws")]
2223    #[tokio::test]
2224    async fn a_sandbox_binding_whose_egress_fields_disagree_is_refused() {
2225        const CONNECTOR: &str = "arn:aws:lambda:us-east-1:123456789012:network-connector:nc-1";
2226
2227        async fn load(connectors: &str, allow_egress: bool) -> Result<()> {
2228            let env = HashMap::from([
2229                (
2230                    ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2231                    Platform::Aws.as_str().to_string(),
2232                ),
2233                ("AWS_REGION".to_string(), "us-east-1".to_string()),
2234                ("AWS_ACCOUNT_ID".to_string(), "123456789012".to_string()),
2235                ("AWS_ACCESS_KEY_ID".to_string(), "test".to_string()),
2236                ("AWS_SECRET_ACCESS_KEY".to_string(), "test".to_string()),
2237                (
2238                    "ALIEN_BOX_BINDING".to_string(),
2239                    format!(
2240                        r#"{{"service":"sandbox-aws",
2241                            "imageArn":"arn:aws:lambda:us-east-1:123456789012:microvm-image:box",
2242                            "imageVersion":"1.0",
2243                            "region":"us-east-1",
2244                            "allowEgress":{allow_egress},
2245                            "egressConnectorArns":[{connectors}]}}"#
2246                    ),
2247                ),
2248            ]);
2249            BindingsProvider::from_env(env)
2250                .await
2251                .expect("the binding JSON parses")
2252                .load_sandbox("box")
2253                .await
2254                .map(|_| ())
2255        }
2256
2257        let fail_open = load("", false)
2258            .await
2259            .expect_err("an empty connector list with allowEgress false is a fail-open default");
2260        assert_eq!(fail_open.code, "BINDING_CONFIG_INVALID");
2261        assert!(
2262            fail_open.to_string().contains("egressConnectorArns"),
2263            "the message should name the field that was refused, got: {fail_open}"
2264        );
2265
2266        let contradiction = load(&format!(r#""{CONNECTOR}""#), true)
2267            .await
2268            .expect_err("open egress and a denying connector cannot both be declared");
2269        assert_eq!(contradiction.code, "BINDING_CONFIG_INVALID");
2270
2271        // The control: without it the two assertions above would pass against a `load_sandbox`
2272        // that refused every AWS sandbox binding.
2273        load(&format!(r#""{CONNECTOR}""#), false)
2274            .await
2275            .expect("a deny binding names a connector and must load");
2276        load("", true)
2277            .await
2278            .expect("an allow binding names none and must load");
2279    }
2280
2281    /// A sandbox binding naming an execution role is refused rather than honoured — see the
2282    /// `execution_role_arn` check in `load_sandbox` for why. Nothing emits the field today, so
2283    /// this is what keeps it that way.
2284    #[cfg(feature = "aws")]
2285    #[tokio::test]
2286    async fn a_sandbox_binding_naming_an_execution_role_is_refused() {
2287        let env = HashMap::from([
2288            (
2289                ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2290                Platform::Aws.as_str().to_string(),
2291            ),
2292            ("AWS_REGION".to_string(), "us-east-1".to_string()),
2293            ("AWS_ACCOUNT_ID".to_string(), "123456789012".to_string()),
2294            ("AWS_ACCESS_KEY_ID".to_string(), "test".to_string()),
2295            ("AWS_SECRET_ACCESS_KEY".to_string(), "test".to_string()),
2296            (
2297                "ALIEN_BOX_BINDING".to_string(),
2298                r#"{"service":"sandbox-aws",
2299                    "imageArn":"arn:aws:lambda:us-east-1:123456789012:microvm-image:box",
2300                    "imageVersion":"1.0",
2301                    "region":"us-east-1",
2302                    "executionRoleArn":"arn:aws:iam::123456789012:role/box",
2303                    "egressConnectorArns":["arn:aws:lambda:us-east-1:123456789012:network-connector:nc-1"]}"#
2304                    .to_string(),
2305            ),
2306        ]);
2307        let provider = BindingsProvider::from_env(env)
2308            .await
2309            .expect("provider construction only validates that the binding JSON parses");
2310
2311        let error = provider
2312            .load_sandbox("box")
2313            .await
2314            .expect_err("a sandbox binding naming an execution role should be refused");
2315
2316        assert_eq!(error.code, "BINDING_CONFIG_INVALID");
2317        assert!(
2318            error.to_string().contains("executionRoleArn"),
2319            "the message should name the field that was refused, got: {error}"
2320        );
2321    }
2322
2323    #[tokio::test]
2324    async fn load_kv_for_malformed_binding_json_returns_binding_config_invalid_with_env_var() {
2325        let env = HashMap::from([
2326            (
2327                ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2328                Platform::Local.as_str().to_string(),
2329            ),
2330            (
2331                "ALIEN_CACHE_BINDING".to_string(),
2332                r#"{"service":"local-kv"}"#.to_string(), // missing required dataDir field
2333            ),
2334        ]);
2335        let provider = BindingsProvider::from_env(env)
2336            .await
2337            .expect("provider construction only validates JSON parses, not field completeness");
2338
2339        let error = provider
2340            .load_kv("cache")
2341            .await
2342            .expect_err("binding missing a required field should error");
2343
2344        assert_eq!(error.code, "BINDING_CONFIG_INVALID");
2345        assert!(
2346            error.to_string().contains("ALIEN_CACHE_BINDING"),
2347            "message should name the env var, got: {error}"
2348        );
2349    }
2350
2351    // --- Selection order on the lazy path (native-vs-mint) ---
2352
2353    mod selection {
2354        use super::*;
2355        use crate::traits::BindingsProviderApi;
2356        use alien_core::{
2357            ENV_ALIEN_DEPLOYMENT_ID, ENV_ALIEN_DEPLOYMENT_SERVICE_ACCOUNT,
2358            ENV_ALIEN_DEPLOYMENT_TOKEN, ENV_ALIEN_MANAGER_URL, ENV_ALIEN_RESOURCE_ID,
2359        };
2360        use axum::{extract::State, routing::post, Json, Router};
2361        use std::net::SocketAddr;
2362        use std::sync::atomic::{AtomicUsize, Ordering};
2363        use tempfile::TempDir;
2364
2365        /// Fake mint endpoint that counts requests and returns a `Local` config.
2366        async fn mint_handler(State(calls): State<Arc<AtomicUsize>>) -> Json<serde_json::Value> {
2367            calls.fetch_add(1, Ordering::SeqCst);
2368            let expires_at = (chrono::Utc::now() + chrono::Duration::seconds(3600)).to_rfc3339();
2369            Json(serde_json::json!({
2370                "clientConfig": { "platform": "local", "state_directory": "/tmp/alien-sel-test" },
2371                "expiresAt": expires_at,
2372                "principal": "local:mint-test",
2373            }))
2374        }
2375
2376        async fn spawn_mint_server() -> (String, Arc<AtomicUsize>) {
2377            let calls = Arc::new(AtomicUsize::new(0));
2378            let app = Router::new()
2379                .route("/v1/credentials/mint", post(mint_handler))
2380                .with_state(calls.clone());
2381            let listener = tokio::net::TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))
2382                .await
2383                .expect("bind");
2384            let addr = listener.local_addr().expect("addr");
2385            tokio::spawn(async move {
2386                axum::serve(listener, app).await.expect("serve");
2387            });
2388            (format!("http://{addr}"), calls)
2389        }
2390
2391        fn local_storage_binding(dir: &TempDir) -> String {
2392            format!(
2393                r#"{{"service":"local-storage","storagePath":"{}"}}"#,
2394                dir.path().display()
2395            )
2396        }
2397
2398        /// Full mint env contract pointing at `manager_url`.
2399        fn mint_env(manager_url: &str) -> HashMap<String, String> {
2400            HashMap::from([
2401                (ENV_ALIEN_MANAGER_URL.to_string(), manager_url.to_string()),
2402                (
2403                    ENV_ALIEN_DEPLOYMENT_TOKEN.to_string(),
2404                    "ax_deploy_tok".to_string(),
2405                ),
2406                (ENV_ALIEN_DEPLOYMENT_ID.to_string(), "dep_1".to_string()),
2407                (
2408                    ENV_ALIEN_DEPLOYMENT_SERVICE_ACCOUNT.to_string(),
2409                    "management".to_string(),
2410                ),
2411                (ENV_ALIEN_RESOURCE_ID.to_string(), "api".to_string()),
2412            ])
2413        }
2414
2415        #[tokio::test]
2416        async fn native_config_wins_and_never_mints() {
2417            // Local platform resolves `ClientConfig::from_env` successfully, so
2418            // even with a full mint contract present the resolver must pick the
2419            // native path and never call the manager.
2420            let (base_url, calls) = spawn_mint_server().await;
2421            let dir = TempDir::new().expect("tempdir");
2422
2423            let mut env = mint_env(&base_url);
2424            env.insert(
2425                ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2426                Platform::Local.as_str().to_string(),
2427            );
2428            env.insert(
2429                "ALIEN_FILES_BINDING".to_string(),
2430                local_storage_binding(&dir),
2431            );
2432
2433            let provider = BindingsProvider::from_env_lazy(env).expect("lazy construct");
2434            provider
2435                .load_storage("files")
2436                .await
2437                .expect("native local storage should load");
2438
2439            assert_eq!(
2440                calls.load(Ordering::SeqCst),
2441                0,
2442                "native credentials must never trigger a mint"
2443            );
2444        }
2445
2446        #[tokio::test]
2447        async fn mints_when_native_config_unavailable() {
2448            // AWS platform with no usable credentials makes `from_env` fail; with
2449            // the mint contract present the resolver falls through to minting and
2450            // resolves the (Local) minted config, which then serves the binding.
2451            let (base_url, calls) = spawn_mint_server().await;
2452            let dir = TempDir::new().expect("tempdir");
2453
2454            let mut env = mint_env(&base_url);
2455            env.insert(
2456                ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2457                Platform::Aws.as_str().to_string(),
2458            );
2459            env.insert("AWS_EC2_METADATA_DISABLED".to_string(), "true".to_string());
2460            env.insert(
2461                "AWS_PROFILE".to_string(),
2462                "__alien_missing_test_profile__".to_string(),
2463            );
2464            env.insert(
2465                "ALIEN_FILES_BINDING".to_string(),
2466                local_storage_binding(&dir),
2467            );
2468
2469            let provider = BindingsProvider::from_env_lazy(env).expect("lazy construct");
2470            provider
2471                .load_storage("files")
2472                .await
2473                .expect("mint path should resolve a usable config");
2474
2475            assert_eq!(
2476                calls.load(Ordering::SeqCst),
2477                1,
2478                "unavailable native credentials must trigger exactly one mint"
2479            );
2480        }
2481
2482        #[tokio::test]
2483        async fn no_mint_contract_preserves_original_from_env_error() {
2484            // AWS platform, no usable creds, and no mint contract: the resolver
2485            // must surface the original `from_env` error unchanged (path 3).
2486            let dir = TempDir::new().expect("tempdir");
2487            let env = HashMap::from([
2488                (
2489                    ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
2490                    Platform::Aws.as_str().to_string(),
2491                ),
2492                ("AWS_EC2_METADATA_DISABLED".to_string(), "true".to_string()),
2493                (
2494                    "AWS_PROFILE".to_string(),
2495                    "__alien_missing_test_profile__".to_string(),
2496                ),
2497                (
2498                    "ALIEN_FILES_BINDING".to_string(),
2499                    local_storage_binding(&dir),
2500                ),
2501            ]);
2502
2503            let provider = BindingsProvider::from_env_lazy(env).expect("lazy construct");
2504            let error = provider
2505                .load_storage("files")
2506                .await
2507                .expect_err("no creds and no mint contract must error");
2508
2509            assert_eq!(error.code, "CLIENT_CONFIG_INVALID");
2510        }
2511    }
2512}