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