Skip to main content

alien_bindings/
provider.rs

1//! Unified BindingsProvider implementation that supports multiple cloud providers
2
3use crate::{
4    error::{ErrorData, Result},
5    traits::{
6        ArtifactRegistry, BindingsProviderApi, Build, Container, Kv, Postgres, Queue,
7        ServiceAccount, Storage, Vault, Worker,
8    },
9};
10
11use alien_client_config::ClientConfigExt;
12use alien_core::bindings::PostgresBinding;
13use alien_core::{ClientConfig, Platform, StackState, ENV_OPERATOR_BASE_PLATFORM};
14use alien_error::{AlienError, Context, IntoAlienError};
15use async_trait::async_trait;
16use std::{any::Any, collections::HashMap, sync::Arc};
17use tokio::sync::{OnceCell, RwLock};
18
19/// Direct platform-specific bindings provider.
20/// Routes to appropriate platform implementations based on binding configuration.
21///
22/// Caches all loaded bindings by name. Each `load_*` call creates cloud clients
23/// (HTTP connection pools, token caches) which are expensive to initialize. Since
24/// the binding configuration is immutable for the provider's lifetime, the same
25/// binding name always produces the same client — so we cache on first load.
26#[derive(Debug, Clone)]
27pub struct BindingsProvider {
28    client_config: ClientConfig,
29    bindings: HashMap<String, serde_json::Value>,
30    /// Per-binding-name cache of loaded binding instances. Keyed by
31    /// `"{trait_name}:{binding_name}"` to avoid collisions across types.
32    /// Each value is a `Box<Arc<dyn Trait>>` erased via `Any`.
33    cache: Arc<RwLock<HashMap<String, Box<dyn Any + Send + Sync>>>>,
34}
35
36/// Environment-backed provider that defers cloud client configuration until the
37/// first binding is actually used.
38///
39/// Long-running HTTP daemons can be wrapped by alien-runtime only for commands,
40/// logs, or future binding access. They should still start when no startup
41/// secret needs loading, even if cloud metadata is temporarily unavailable.
42#[derive(Debug)]
43pub struct LazyEnvBindingsProvider {
44    env: HashMap<String, String>,
45    provider: OnceCell<BindingsProvider>,
46}
47
48impl BindingsProvider {
49    /// Creates a new BindingsProvider with explicit credentials and bindings.
50    ///
51    /// This is the base constructor used by all other convenience constructors.
52    pub fn new(
53        client_config: ClientConfig,
54        bindings: HashMap<String, serde_json::Value>,
55    ) -> Result<Self> {
56        Ok(Self {
57            client_config,
58            bindings,
59            cache: Arc::new(RwLock::new(HashMap::new())),
60        })
61    }
62
63    /// Get a cached binding by type and name, or return None.
64    async fn get_cached<T: Clone + Send + Sync + 'static>(
65        &self,
66        trait_name: &str,
67        binding_name: &str,
68    ) -> Option<T> {
69        let cache_key = format!("{}:{}", trait_name, binding_name);
70        let cache = self.cache.read().await;
71        cache
72            .get(&cache_key)
73            .and_then(|boxed| boxed.downcast_ref::<T>())
74            .cloned()
75    }
76
77    /// Store a binding in the cache.
78    async fn put_cache<T: Clone + Send + Sync + 'static>(
79        &self,
80        trait_name: &str,
81        binding_name: &str,
82        value: T,
83    ) {
84        let cache_key = format!("{}:{}", trait_name, binding_name);
85        let mut cache = self.cache.write().await;
86        cache.insert(cache_key, Box::new(value));
87    }
88
89    /// Creates a BindingsProvider from environment variables (for runtime use).
90    ///
91    /// This parses the platform from ALIEN_DEPLOYMENT_TYPE, loads ClientConfig from environment,
92    /// and extracts all ALIEN_*_BINDING environment variables.
93    pub async fn from_env(env: HashMap<String, String>) -> Result<Self> {
94        // 1. Parse platform from ALIEN_DEPLOYMENT_TYPE
95        let platform = crate::get_platform_from_env(&env)?;
96
97        // 2. Load ClientConfig from environment
98        let client_config = Self::client_config_from_env(platform, &env).await?;
99
100        // 3. Parse all ALIEN_*_BINDING environment variables
101        let bindings = Self::parse_bindings_from_env(&env)?;
102
103        Self::new(client_config, bindings)
104    }
105
106    /// Creates an environment-backed provider without resolving cloud client
107    /// configuration yet.
108    ///
109    /// Startup still validates the platform and binding JSON, but credentials
110    /// are loaded only if application code asks for a binding or runtime-owned
111    /// startup secrets need to be fetched.
112    pub fn from_env_lazy(env: HashMap<String, String>) -> Result<LazyEnvBindingsProvider> {
113        crate::get_platform_from_env(&env)?;
114        Self::parse_bindings_from_env(&env)?;
115
116        Ok(LazyEnvBindingsProvider {
117            env,
118            provider: OnceCell::new(),
119        })
120    }
121
122    async fn client_config_from_env(
123        platform: Platform,
124        env: &HashMap<String, String>,
125    ) -> Result<ClientConfig> {
126        if platform != Platform::Kubernetes {
127            return Self::load_client_config_from_env(platform, env).await;
128        }
129
130        let Some(base_platform) = Self::base_platform_from_env(env)? else {
131            return Self::load_client_config_from_env(platform, env).await;
132        };
133
134        let kubernetes = match Self::load_client_config_from_env(Platform::Kubernetes, env).await? {
135            ClientConfig::Kubernetes(kubernetes) => kubernetes,
136            _ => unreachable!("kubernetes platform must produce a Kubernetes client config"),
137        };
138        let cloud = Self::load_client_config_from_env(base_platform, env).await?;
139
140        Ok(ClientConfig::KubernetesCloud {
141            kubernetes,
142            cloud: Box::new(cloud),
143        })
144    }
145
146    fn base_platform_from_env(env: &HashMap<String, String>) -> Result<Option<Platform>> {
147        let Some(base_platform) = env.get(ENV_OPERATOR_BASE_PLATFORM) else {
148            return Ok(None);
149        };
150
151        let parsed: Platform = base_platform.parse().map_err(|reason| {
152            AlienError::new(ErrorData::InvalidEnvironmentVariable {
153                variable_name: ENV_OPERATOR_BASE_PLATFORM.to_string(),
154                value: base_platform.clone(),
155                reason,
156            })
157        })?;
158
159        if !matches!(parsed, Platform::Aws | Platform::Gcp | Platform::Azure) {
160            return Err(AlienError::new(ErrorData::InvalidEnvironmentVariable {
161                variable_name: ENV_OPERATOR_BASE_PLATFORM.to_string(),
162                value: base_platform.clone(),
163                reason: "Kubernetes base platform must be aws, gcp, or azure".to_string(),
164            }));
165        }
166
167        Ok(Some(parsed))
168    }
169
170    async fn load_client_config_from_env(
171        platform: Platform,
172        env: &HashMap<String, String>,
173    ) -> Result<ClientConfig> {
174        ClientConfig::from_env(platform, env).await.map_err(|e| {
175            AlienError::new(ErrorData::ClientConfigInvalid {
176                platform,
177                message: format!("Failed to load client config: {}", e),
178            })
179        })
180    }
181
182    /// Parses all ALIEN_*_BINDING environment variables into a map.
183    fn parse_bindings_from_env(
184        env: &HashMap<String, String>,
185    ) -> Result<HashMap<String, serde_json::Value>> {
186        let mut bindings = HashMap::new();
187        for (key, value) in env {
188            if key.starts_with("ALIEN_") && key.ends_with("_BINDING") {
189                let binding_name = key
190                    .strip_prefix("ALIEN_")
191                    .unwrap()
192                    .strip_suffix("_BINDING")
193                    .unwrap()
194                    .to_lowercase()
195                    .replace('_', "-");
196                let parsed: serde_json::Value = serde_json::from_str(value)
197                    .into_alien_error()
198                    .context(ErrorData::BindingConfigInvalid {
199                        binding_name: binding_name.clone(),
200                        reason: "Failed to parse binding JSON".to_string(),
201                    })?;
202                bindings.insert(binding_name, parsed);
203            }
204        }
205        Ok(bindings)
206    }
207
208    /// Pattern 1: Creates a BindingsProvider from stack state and client config.
209    ///
210    /// Convenience helper that extracts bindings from stack state's remote_binding_params.
211    /// Only resources with `remote_access: true` will have binding params available.
212    ///
213    /// **When to use:** You already have `ClientConfig` and `StackState`.
214    ///
215    /// **Example use cases:**
216    /// - alien-deployment (has credentials from RemoteAccessResolver)
217    /// - Platform API backend (has stack state from DB)
218    pub fn from_stack_state(stack_state: &StackState, client_config: ClientConfig) -> Result<Self> {
219        let bindings = stack_state
220            .resources
221            .iter()
222            .filter_map(|(id, state)| {
223                state
224                    .remote_binding_params
225                    .as_ref()
226                    .map(|p| (id.clone(), p.clone()))
227            })
228            .collect();
229
230        Self::new(client_config, bindings)
231    }
232
233    /// Pattern 2: Creates a BindingsProvider for remote deployment access.
234    ///
235    /// Fetches credentials remotely using deployment ID and auth token, then creates the provider.
236    ///
237    /// **When to use:** You have a deployment ID and auth token, but no credentials yet.
238    ///
239    /// **Example use cases:**
240    /// - CLI commands (e.g., `alien secrets set`)
241    /// - External applications connecting to deployment
242    /// - Local development tools
243    ///
244    /// **What it does internally:**
245    /// 1. GET /api/deployments/{id} - Returns deployment info (stackState, platform, managerId)
246    /// 2. GET /api/managers/{managerId} - Returns manager URL
247    /// 3. POST {managerUrl}/v1/deployment/resolve-credentials - Resolves credentials
248    /// 4. Creates BindingsProvider using from_stack_state()
249    #[cfg(feature = "platform-sdk")]
250    pub async fn for_remote_deployment(
251        deployment_id: &str,
252        token: &str,
253        api_base_url: Option<&str>,
254    ) -> Result<Self> {
255        let base_url = api_base_url.unwrap_or("https://api.alien.dev");
256
257        // Build an authenticated SDK client so deployment/manager lookups are
258        // scoped to the caller's permissions.
259        let auth_value = format!("Bearer {}", token);
260        let mut headers = reqwest::header::HeaderMap::new();
261        headers.insert(
262            reqwest::header::AUTHORIZATION,
263            reqwest::header::HeaderValue::from_str(&auth_value)
264                .into_alien_error()
265                .context(ErrorData::RemoteAccessFailed {
266                    operation: "build Platform API client with token".to_string(),
267                })?,
268        );
269
270        let authed_http_client = reqwest::Client::builder()
271            .default_headers(headers)
272            .build()
273            .into_alien_error()
274            .context(ErrorData::RemoteAccessFailed {
275                operation: "build Platform API HTTP client".to_string(),
276            })?;
277
278        let sdk_client = alien_platform_api::Client::new_with_client(base_url, authed_http_client);
279
280        // 1. Get deployment info (caller-scoped)
281        let deployment_response = sdk_client
282            .get_deployment()
283            .id(deployment_id)
284            .send()
285            .await
286            .into_alien_error()
287            .context(ErrorData::RemoteAccessFailed {
288                operation: "fetch deployment from Platform API".to_string(),
289            })?
290            .into_inner();
291
292        // 2. Get manager URL (caller-scoped)
293        let manager_id = deployment_response.manager_id.ok_or_else(|| {
294            AlienError::new(ErrorData::RemoteAccessFailed {
295                operation: "fetch manager from Platform API".to_string(),
296            })
297        })?;
298
299        let manager_response = sdk_client
300            .get_manager()
301            .id(&manager_id.to_string())
302            .send()
303            .await
304            .into_alien_error()
305            .context(ErrorData::RemoteAccessFailed {
306                operation: "fetch manager from Platform API".to_string(),
307            })?
308            .into_inner();
309
310        // 3. Convert SDK stack state to alien-core StackState (used locally to extract
311        // binding configuration; the manager re-fetches the canonical stack state itself
312        // when resolving credentials).
313        let stack_state = deployment_response.stack_state.as_ref().ok_or_else(|| {
314            AlienError::new(ErrorData::RemoteAccessFailed {
315                operation: "Deployment has no stack state (not deployed yet)".to_string(),
316            })
317        })?;
318
319        let alien_stack_state = conversions::convert_stack_state(stack_state)?;
320
321        // 4. Resolve client config from manager. We send only the deploymentId; the
322        // manager fetches stackState/platform from Platform API using our forwarded
323        // bearer token so an attacker cannot direct it at an arbitrary cloud identity.
324        let manager_url = manager_response.url.ok_or_else(|| {
325            AlienError::new(ErrorData::RemoteAccessFailed {
326                operation: "fetch manager URL from Platform API".to_string(),
327            })
328        })?;
329
330        let http_client = reqwest::Client::new();
331        let client_config = http_client
332            .post(format!("{}/v1/deployment/resolve-credentials", manager_url))
333            .bearer_auth(token)
334            .json(&serde_json::json!({
335                "deploymentId": deployment_id,
336            }))
337            .send()
338            .await
339            .into_alien_error()
340            .context(ErrorData::RemoteAccessFailed {
341                operation: "resolve credentials from manager".to_string(),
342            })?
343            .json::<ResolveCredentialsResponse>()
344            .await
345            .into_alien_error()
346            .context(ErrorData::RemoteAccessFailed {
347                operation: "parse credentials response".to_string(),
348            })?
349            .client_config;
350
351        // 5. Create provider using from_stack_state (which extracts bindings from stack_state)
352        Self::from_stack_state(&alien_stack_state, client_config)
353    }
354}
355
356impl LazyEnvBindingsProvider {
357    async fn provider(&self) -> Result<&BindingsProvider> {
358        self.provider
359            .get_or_try_init(|| async { BindingsProvider::from_env(self.env.clone()).await })
360            .await
361    }
362}
363
364#[async_trait]
365impl BindingsProviderApi for LazyEnvBindingsProvider {
366    async fn load_storage(&self, binding_name: &str) -> Result<Arc<dyn Storage>> {
367        self.provider().await?.load_storage(binding_name).await
368    }
369
370    async fn load_build(&self, binding_name: &str) -> Result<Arc<dyn Build>> {
371        self.provider().await?.load_build(binding_name).await
372    }
373
374    async fn load_artifact_registry(
375        &self,
376        binding_name: &str,
377    ) -> Result<Arc<dyn ArtifactRegistry>> {
378        self.provider()
379            .await?
380            .load_artifact_registry(binding_name)
381            .await
382    }
383
384    async fn load_vault(&self, binding_name: &str) -> Result<Arc<dyn Vault>> {
385        self.provider().await?.load_vault(binding_name).await
386    }
387
388    async fn load_kv(&self, binding_name: &str) -> Result<Arc<dyn Kv>> {
389        self.provider().await?.load_kv(binding_name).await
390    }
391
392    async fn load_postgres(&self, binding_name: &str) -> Result<Arc<dyn Postgres>> {
393        self.provider().await?.load_postgres(binding_name).await
394    }
395
396    async fn load_queue(&self, binding_name: &str) -> Result<Arc<dyn Queue>> {
397        self.provider().await?.load_queue(binding_name).await
398    }
399
400    async fn load_worker(&self, binding_name: &str) -> Result<Arc<dyn Worker>> {
401        self.provider().await?.load_worker(binding_name).await
402    }
403
404    async fn load_container(&self, binding_name: &str) -> Result<Arc<dyn Container>> {
405        self.provider().await?.load_container(binding_name).await
406    }
407
408    async fn load_service_account(&self, binding_name: &str) -> Result<Arc<dyn ServiceAccount>> {
409        self.provider()
410            .await?
411            .load_service_account(binding_name)
412            .await
413    }
414}
415
416#[cfg(feature = "platform-sdk")]
417#[derive(serde::Deserialize)]
418#[serde(rename_all = "camelCase")]
419struct ResolveCredentialsResponse {
420    client_config: ClientConfig,
421}
422
423#[async_trait]
424impl BindingsProviderApi for BindingsProvider {
425    async fn load_storage(&self, binding_name: &str) -> Result<Arc<dyn Storage>> {
426        if let Some(cached) = self
427            .get_cached::<Arc<dyn Storage>>("storage", binding_name)
428            .await
429        {
430            return Ok(cached);
431        }
432
433        use alien_core::bindings::StorageBinding;
434
435        // Get binding JSON from our pre-parsed map
436        let binding_json = self.bindings.get(binding_name).ok_or_else(|| {
437            AlienError::new(ErrorData::BindingConfigInvalid {
438                binding_name: binding_name.to_string(),
439                reason: "Binding not found".to_string(),
440            })
441        })?;
442
443        // Parse to strongly-typed binding
444        let binding: StorageBinding = serde_json::from_value(binding_json.clone())
445            .into_alien_error()
446            .context(ErrorData::BindingConfigInvalid {
447                binding_name: binding_name.to_string(),
448                reason: "Failed to parse storage binding".to_string(),
449            })?;
450
451        let result: Arc<dyn Storage> = match binding {
452            #[cfg(feature = "aws")]
453            StorageBinding::S3(config) => {
454                use crate::providers::storage::aws_s3::S3Storage;
455
456                // Get AWS config from our stored ClientConfig
457                let aws_config = self.client_config.aws_config().ok_or_else(|| {
458                    AlienError::new(ErrorData::ClientConfigInvalid {
459                        platform: Platform::Aws,
460                        message: "AWS config not available".to_string(),
461                    })
462                })?;
463
464                let credentials =
465                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
466                        .await
467                        .context(ErrorData::BindingSetupFailed {
468                            binding_type: "AWS S3 storage".to_string(),
469                            reason: "Failed to create credential provider".to_string(),
470                        })?;
471
472                // Extract bucket name from binding
473                let bucket_name = config
474                    .bucket_name
475                    .into_value(binding_name, "bucket_name")
476                    .context(ErrorData::BindingConfigInvalid {
477                        binding_name: binding_name.to_string(),
478                        reason: "Failed to extract bucket_name from S3 binding".to_string(),
479                    })?;
480
481                let storage: Arc<dyn Storage> = Arc::new(S3Storage::new(bucket_name, credentials)?);
482                Ok(storage)
483            }
484            #[cfg(not(feature = "aws"))]
485            StorageBinding::S3 { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
486                feature: "aws".to_string(),
487            })),
488
489            #[cfg(feature = "azure")]
490            StorageBinding::Blob(config) => {
491                use crate::providers::storage::azure_blob::BlobStorage;
492
493                let azure_config = self.client_config.azure_config().ok_or_else(|| {
494                    AlienError::new(ErrorData::ClientConfigInvalid {
495                        platform: Platform::Azure,
496                        message: "Azure config not available".to_string(),
497                    })
498                })?;
499
500                // Extract container and account names from binding
501                let container_name = config
502                    .container_name
503                    .into_value(binding_name, "container_name")
504                    .context(ErrorData::BindingConfigInvalid {
505                        binding_name: binding_name.to_string(),
506                        reason: "Failed to extract container_name from Blob binding".to_string(),
507                    })?;
508
509                let account_name = config
510                    .account_name
511                    .into_value(binding_name, "account_name")
512                    .context(ErrorData::BindingConfigInvalid {
513                        binding_name: binding_name.to_string(),
514                        reason: "Failed to extract account_name from Blob binding".to_string(),
515                    })?;
516
517                let storage: Arc<dyn Storage> = Arc::new(BlobStorage::new(
518                    container_name,
519                    account_name,
520                    azure_config,
521                )?);
522                Ok(storage)
523            }
524            #[cfg(not(feature = "azure"))]
525            StorageBinding::Blob { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
526                feature: "azure".to_string(),
527            })),
528
529            #[cfg(feature = "gcp")]
530            StorageBinding::Gcs(config) => {
531                use crate::providers::storage::gcp_gcs::GcsStorage;
532
533                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
534                    AlienError::new(ErrorData::ClientConfigInvalid {
535                        platform: Platform::Gcp,
536                        message: "GCP config not available".to_string(),
537                    })
538                })?;
539
540                // Extract bucket name from binding
541                let bucket_name = config
542                    .bucket_name
543                    .into_value(binding_name, "bucket_name")
544                    .context(ErrorData::BindingConfigInvalid {
545                        binding_name: binding_name.to_string(),
546                        reason: "Failed to extract bucket_name from Gcs binding".to_string(),
547                    })?;
548
549                let storage: Arc<dyn Storage> = Arc::new(GcsStorage::new(bucket_name, gcp_config)?);
550                Ok(storage)
551            }
552            #[cfg(not(feature = "gcp"))]
553            StorageBinding::Gcs { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
554                feature: "gcp".to_string(),
555            })),
556
557            #[cfg(feature = "local")]
558            StorageBinding::Local(config) => {
559                use crate::providers::storage::local::LocalStorage;
560
561                // Extract storage path from binding
562                let storage_path = config
563                    .storage_path
564                    .into_value(binding_name, "storage_path")
565                    .context(ErrorData::BindingConfigInvalid {
566                        binding_name: binding_name.to_string(),
567                        reason: "Failed to extract storage_path from Local binding".to_string(),
568                    })?;
569
570                let storage: Arc<dyn Storage> = Arc::new(LocalStorage::new(storage_path)?);
571                Ok(storage)
572            }
573            #[cfg(not(feature = "local"))]
574            StorageBinding::Local { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
575                feature: "local".to_string(),
576            })),
577        }?;
578
579        self.put_cache("storage", binding_name, result.clone())
580            .await;
581        Ok(result)
582    }
583
584    async fn load_build(&self, binding_name: &str) -> Result<Arc<dyn Build>> {
585        use alien_core::bindings::BuildBinding;
586
587        let binding_json = self.bindings.get(binding_name).ok_or_else(|| {
588            AlienError::new(ErrorData::BindingConfigInvalid {
589                binding_name: binding_name.to_string(),
590                reason: "Binding not found".to_string(),
591            })
592        })?;
593
594        let binding: BuildBinding = serde_json::from_value(binding_json.clone())
595            .into_alien_error()
596            .context(ErrorData::BindingConfigInvalid {
597                binding_name: binding_name.to_string(),
598                reason: "Failed to parse build binding".to_string(),
599            })?;
600
601        match binding {
602            #[cfg(feature = "aws")]
603            BuildBinding::Codebuild { .. } => {
604                use crate::providers::build::codebuild::CodebuildBuild;
605
606                let aws_config = self.client_config.aws_config().ok_or_else(|| {
607                    AlienError::new(ErrorData::ClientConfigInvalid {
608                        platform: Platform::Aws,
609                        message: "AWS config not available".to_string(),
610                    })
611                })?;
612                let credentials =
613                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
614                        .await
615                        .context(ErrorData::ClientConfigInvalid {
616                            platform: Platform::Aws,
617                            message: "Failed to create AWS credential provider".to_string(),
618                        })?;
619
620                let build = Arc::new(
621                    CodebuildBuild::new(binding_name.to_string(), binding, &credentials)
622                        .await
623                        .context(ErrorData::BindingConfigInvalid {
624                            binding_name: binding_name.to_string(),
625                            reason: "Failed to initialize AWS CodeBuild client".to_string(),
626                        })?,
627                );
628                Ok(build)
629            }
630            #[cfg(not(feature = "aws"))]
631            BuildBinding::Codebuild { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
632                feature: "aws".to_string(),
633            })),
634
635            #[cfg(feature = "azure")]
636            BuildBinding::Aca { .. } => {
637                use crate::providers::build::aca::AcaBuild;
638
639                let azure_config = self.client_config.azure_config().ok_or_else(|| {
640                    AlienError::new(ErrorData::ClientConfigInvalid {
641                        platform: Platform::Azure,
642                        message: "Azure config not available".to_string(),
643                    })
644                })?;
645
646                let build = Arc::new(
647                    AcaBuild::new(binding_name.to_string(), binding, azure_config)
648                        .await
649                        .context(ErrorData::BindingConfigInvalid {
650                            binding_name: binding_name.to_string(),
651                            reason: "Failed to initialize Azure Container Apps build".to_string(),
652                        })?,
653                );
654                Ok(build)
655            }
656            #[cfg(not(feature = "azure"))]
657            BuildBinding::Aca { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
658                feature: "azure".to_string(),
659            })),
660
661            #[cfg(feature = "gcp")]
662            BuildBinding::Cloudbuild { .. } => {
663                use crate::providers::build::cloudbuild::CloudbuildBuild;
664
665                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
666                    AlienError::new(ErrorData::ClientConfigInvalid {
667                        platform: Platform::Gcp,
668                        message: "GCP config not available".to_string(),
669                    })
670                })?;
671
672                let build = Arc::new(
673                    CloudbuildBuild::new(binding_name.to_string(), binding, gcp_config)
674                        .await
675                        .context(ErrorData::BindingConfigInvalid {
676                            binding_name: binding_name.to_string(),
677                            reason: "Failed to initialize GCP Cloud Build client".to_string(),
678                        })?,
679                );
680                Ok(build)
681            }
682            #[cfg(not(feature = "gcp"))]
683            BuildBinding::Cloudbuild { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
684                feature: "gcp".to_string(),
685            })),
686
687            #[cfg(feature = "local")]
688            BuildBinding::Local { .. } => {
689                use crate::providers::build::local::LocalBuild;
690
691                let build = Arc::new(LocalBuild::new(binding_name.to_string(), binding)?);
692                Ok(build)
693            }
694            #[cfg(not(feature = "local"))]
695            BuildBinding::Local { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
696                feature: "local".to_string(),
697            })),
698
699            #[cfg(feature = "kubernetes")]
700            BuildBinding::Kubernetes { .. } => {
701                use crate::providers::build::kubernetes::KubernetesBuild;
702
703                let build =
704                    Arc::new(KubernetesBuild::new(binding_name.to_string(), binding).await?);
705                Ok(build)
706            }
707            #[cfg(not(feature = "kubernetes"))]
708            BuildBinding::Kubernetes { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
709                feature: "kubernetes".to_string(),
710            })),
711        }
712    }
713
714    async fn load_artifact_registry(
715        &self,
716        binding_name: &str,
717    ) -> Result<Arc<dyn ArtifactRegistry>> {
718        if let Some(cached) = self
719            .get_cached::<Arc<dyn ArtifactRegistry>>("artifact_registry", binding_name)
720            .await
721        {
722            return Ok(cached);
723        }
724
725        use alien_core::bindings::ArtifactRegistryBinding;
726
727        let binding_json = self.bindings.get(binding_name).ok_or_else(|| {
728            AlienError::new(ErrorData::BindingConfigInvalid {
729                binding_name: binding_name.to_string(),
730                reason: "Binding not found".to_string(),
731            })
732        })?;
733
734        let binding: ArtifactRegistryBinding = serde_json::from_value(binding_json.clone())
735            .into_alien_error()
736            .context(ErrorData::BindingConfigInvalid {
737                binding_name: binding_name.to_string(),
738                reason: "Failed to parse artifact registry binding".to_string(),
739            })?;
740
741        let registry: Arc<dyn ArtifactRegistry> = match binding {
742            #[cfg(feature = "aws")]
743            ArtifactRegistryBinding::Ecr { .. } => {
744                use crate::providers::artifact_registry::ecr::EcrArtifactRegistry;
745
746                let aws_config = self.client_config.aws_config().ok_or_else(|| {
747                    AlienError::new(ErrorData::ClientConfigInvalid {
748                        platform: Platform::Aws,
749                        message: "AWS config not available".to_string(),
750                    })
751                })?;
752                let credentials =
753                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
754                        .await
755                        .context(ErrorData::ClientConfigInvalid {
756                            platform: Platform::Aws,
757                            message: "Failed to create AWS credential provider".to_string(),
758                        })?;
759
760                let registry: Arc<dyn ArtifactRegistry> = Arc::new(
761                    EcrArtifactRegistry::new(binding_name.to_string(), binding, &credentials)
762                        .await
763                        .context(ErrorData::BindingConfigInvalid {
764                            binding_name: binding_name.to_string(),
765                            reason: "Failed to initialize AWS ECR artifact registry".to_string(),
766                        })?,
767                );
768                Ok(registry)
769            }
770            #[cfg(not(feature = "aws"))]
771            ArtifactRegistryBinding::Ecr { .. } => {
772                Err(AlienError::new(ErrorData::FeatureNotEnabled {
773                    feature: "aws".to_string(),
774                }))
775            }
776
777            #[cfg(feature = "azure")]
778            ArtifactRegistryBinding::Acr { .. } => {
779                use crate::providers::artifact_registry::acr::AcrArtifactRegistry;
780
781                let azure_config = self.client_config.azure_config().ok_or_else(|| {
782                    AlienError::new(ErrorData::ClientConfigInvalid {
783                        platform: Platform::Azure,
784                        message: "Azure config not available".to_string(),
785                    })
786                })?;
787
788                let registry: Arc<dyn ArtifactRegistry> = Arc::new(
789                    AcrArtifactRegistry::new(binding_name.to_string(), binding, azure_config)
790                        .await
791                        .context(ErrorData::BindingConfigInvalid {
792                            binding_name: binding_name.to_string(),
793                            reason: "Failed to initialize Azure ACR artifact registry".to_string(),
794                        })?,
795                );
796                Ok(registry)
797            }
798            #[cfg(not(feature = "azure"))]
799            ArtifactRegistryBinding::Acr { .. } => {
800                Err(AlienError::new(ErrorData::FeatureNotEnabled {
801                    feature: "azure".to_string(),
802                }))
803            }
804
805            #[cfg(feature = "gcp")]
806            ArtifactRegistryBinding::Gar { .. } => {
807                use crate::providers::artifact_registry::gar::GarArtifactRegistry;
808
809                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
810                    AlienError::new(ErrorData::ClientConfigInvalid {
811                        platform: Platform::Gcp,
812                        message: "GCP config not available".to_string(),
813                    })
814                })?;
815
816                let registry: Arc<dyn ArtifactRegistry> = Arc::new(
817                    GarArtifactRegistry::new(binding_name.to_string(), binding, gcp_config)
818                        .await
819                        .context(ErrorData::BindingConfigInvalid {
820                            binding_name: binding_name.to_string(),
821                            reason: "Failed to initialize GCP GAR artifact registry".to_string(),
822                        })?,
823                );
824                Ok(registry)
825            }
826            #[cfg(not(feature = "gcp"))]
827            ArtifactRegistryBinding::Gar { .. } => {
828                Err(AlienError::new(ErrorData::FeatureNotEnabled {
829                    feature: "gcp".to_string(),
830                }))
831            }
832
833            #[cfg(feature = "local")]
834            ArtifactRegistryBinding::Local { .. } => {
835                use crate::providers::artifact_registry::local::LocalArtifactRegistry;
836
837                let registry: Arc<dyn ArtifactRegistry> = Arc::new(
838                    LocalArtifactRegistry::new(binding_name.to_string(), binding.clone()).await?,
839                );
840                Ok(registry)
841            }
842            #[cfg(not(feature = "local"))]
843            ArtifactRegistryBinding::Local { .. } => {
844                Err(AlienError::new(ErrorData::FeatureNotEnabled {
845                    feature: "local".to_string(),
846                }))
847            }
848        }?;
849
850        self.put_cache("artifact_registry", binding_name, registry.clone())
851            .await;
852        Ok(registry)
853    }
854
855    async fn load_vault(&self, binding_name: &str) -> Result<Arc<dyn Vault>> {
856        if let Some(cached) = self
857            .get_cached::<Arc<dyn Vault>>("vault", binding_name)
858            .await
859        {
860            return Ok(cached);
861        }
862
863        use alien_core::bindings::VaultBinding;
864
865        let binding_json = self.bindings.get(binding_name).ok_or_else(|| {
866            AlienError::new(ErrorData::BindingConfigInvalid {
867                binding_name: binding_name.to_string(),
868                reason: "Binding not found".to_string(),
869            })
870        })?;
871
872        let binding: VaultBinding = serde_json::from_value(binding_json.clone())
873            .into_alien_error()
874            .context(ErrorData::BindingConfigInvalid {
875                binding_name: binding_name.to_string(),
876                reason: "Failed to parse vault binding".to_string(),
877            })?;
878
879        let result: Arc<dyn Vault> = match binding {
880            #[cfg(feature = "aws")]
881            VaultBinding::ParameterStore(config) => {
882                use crate::providers::vault::aws_parameter_store::AwsParameterStoreVault;
883                use alien_aws_clients::ssm::SsmClient;
884
885                let aws_config = self.client_config.aws_config().ok_or_else(|| {
886                    AlienError::new(ErrorData::ClientConfigInvalid {
887                        platform: Platform::Aws,
888                        message: "AWS config not available".to_string(),
889                    })
890                })?;
891                let credentials =
892                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
893                        .await
894                        .context(ErrorData::ClientConfigInvalid {
895                            platform: Platform::Aws,
896                            message: "Failed to create AWS credential provider".to_string(),
897                        })?;
898
899                let client = Arc::new(SsmClient::new(
900                    crate::http_client::create_http_client(),
901                    credentials,
902                ));
903
904                // Extract the vault prefix from the binding configuration
905                let vault_prefix = config
906                    .vault_prefix
907                    .into_value(&binding_name, "vault_prefix")
908                    .context(ErrorData::BindingConfigInvalid {
909                        binding_name: binding_name.to_string(),
910                        reason: "Failed to extract vault_prefix from ParameterStore binding"
911                            .to_string(),
912                    })?;
913
914                let vault: Arc<dyn Vault> =
915                    Arc::new(AwsParameterStoreVault::new(client, vault_prefix));
916                Ok(vault)
917            }
918            #[cfg(not(feature = "aws"))]
919            VaultBinding::ParameterStore(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
920                feature: "aws".to_string(),
921            })),
922
923            #[cfg(feature = "azure")]
924            VaultBinding::KeyVault(config) => {
925                use crate::providers::vault::azure_key_vault::AzureKeyVault;
926                use alien_azure_clients::keyvault::AzureKeyVaultSecretsClient;
927                use alien_azure_clients::AzureTokenCache;
928
929                let azure_config = self.client_config.azure_config().ok_or_else(|| {
930                    AlienError::new(ErrorData::ClientConfigInvalid {
931                        platform: Platform::Azure,
932                        message: "Azure config not available".to_string(),
933                    })
934                })?;
935
936                let client = Arc::new(AzureKeyVaultSecretsClient::new(
937                    crate::http_client::create_http_client(),
938                    AzureTokenCache::new(azure_config.clone()),
939                ));
940
941                // Extract the vault name from the binding configuration
942                let vault_name = config
943                    .vault_name
944                    .into_value(&binding_name, "vault_name")
945                    .context(ErrorData::BindingConfigInvalid {
946                        binding_name: binding_name.to_string(),
947                        reason: "Failed to extract vault_name from KeyVault binding".to_string(),
948                    })?;
949
950                // Construct the vault base URL
951                // Azure Key Vault URLs typically follow: https://{vault-name}.vault.azure.net/
952                let vault_base_url = format!("https://{}.vault.azure.net", vault_name);
953
954                let vault: Arc<dyn Vault> = Arc::new(AzureKeyVault::new(client, vault_base_url));
955                Ok(vault)
956            }
957            #[cfg(not(feature = "azure"))]
958            VaultBinding::KeyVault(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
959                feature: "azure".to_string(),
960            })),
961
962            #[cfg(feature = "gcp")]
963            VaultBinding::SecretManager(config) => {
964                use crate::providers::vault::gcp_secret_manager::GcpSecretManagerVault;
965                use alien_gcp_clients::secret_manager::SecretManagerClient;
966
967                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
968                    AlienError::new(ErrorData::ClientConfigInvalid {
969                        platform: Platform::Gcp,
970                        message: "GCP config not available".to_string(),
971                    })
972                })?;
973
974                let client = Arc::new(SecretManagerClient::new(
975                    crate::http_client::create_http_client(),
976                    gcp_config.clone(),
977                ));
978
979                // Extract the vault prefix from the binding configuration
980                let vault_prefix = config
981                    .vault_prefix
982                    .into_value(&binding_name, "vault_prefix")
983                    .context(ErrorData::BindingConfigInvalid {
984                        binding_name: binding_name.to_string(),
985                        reason: "Failed to extract vault_prefix from SecretManager binding"
986                            .to_string(),
987                    })?;
988
989                let vault: Arc<dyn Vault> = Arc::new(GcpSecretManagerVault::new(
990                    client,
991                    vault_prefix,
992                    gcp_config.project_id.clone(),
993                ));
994                Ok(vault)
995            }
996            #[cfg(not(feature = "gcp"))]
997            VaultBinding::SecretManager(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
998                feature: "gcp".to_string(),
999            })),
1000
1001            #[cfg(feature = "local")]
1002            VaultBinding::Local(config) => {
1003                use crate::providers::vault::local::LocalVault;
1004
1005                let vault_dir = config
1006                    .data_dir
1007                    .into_value(binding_name, "data_dir")
1008                    .context(ErrorData::BindingConfigInvalid {
1009                        binding_name: binding_name.to_string(),
1010                        reason: "Failed to extract data_dir from vault binding".to_string(),
1011                    })?;
1012
1013                let vault: Arc<dyn Vault> = Arc::new(LocalVault::new(
1014                    binding_name.to_string(),
1015                    std::path::PathBuf::from(vault_dir),
1016                ));
1017                Ok(vault)
1018            }
1019            #[cfg(not(feature = "local"))]
1020            VaultBinding::Local { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1021                feature: "local".to_string(),
1022            })),
1023
1024            #[cfg(feature = "kubernetes")]
1025            VaultBinding::KubernetesSecret(config) => {
1026                use crate::providers::vault::kubernetes_secret::KubernetesSecretVault;
1027                use alien_k8s_clients::{secrets::SecretsApi, KubernetesClient};
1028
1029                let kubernetes_config =
1030                    self.client_config.kubernetes_config().ok_or_else(|| {
1031                        AlienError::new(ErrorData::ClientConfigInvalid {
1032                            platform: Platform::Kubernetes,
1033                            message: "Kubernetes config not available".to_string(),
1034                        })
1035                    })?;
1036
1037                let kubernetes_client = KubernetesClient::new(kubernetes_config.clone())
1038                    .await
1039                    .context(ErrorData::CloudPlatformError {
1040                        message: "Failed to create Kubernetes client for vault".to_string(),
1041                        resource_id: None,
1042                    })?;
1043
1044                let client: Arc<dyn SecretsApi> = Arc::new(kubernetes_client);
1045
1046                // Extract namespace and vault prefix from binding
1047                let namespace = config
1048                    .namespace
1049                    .into_value(binding_name, "namespace")
1050                    .context(ErrorData::BindingConfigInvalid {
1051                        binding_name: binding_name.to_string(),
1052                        reason: "Failed to extract namespace from KubernetesSecret binding"
1053                            .to_string(),
1054                    })?;
1055
1056                let vault_prefix = config
1057                    .vault_prefix
1058                    .into_value(binding_name, "vault_prefix")
1059                    .context(ErrorData::BindingConfigInvalid {
1060                        binding_name: binding_name.to_string(),
1061                        reason: "Failed to extract vault_prefix from KubernetesSecret binding"
1062                            .to_string(),
1063                    })?;
1064
1065                let vault: Arc<dyn Vault> =
1066                    Arc::new(KubernetesSecretVault::new(client, namespace, vault_prefix));
1067                Ok(vault)
1068            }
1069            #[cfg(not(feature = "kubernetes"))]
1070            VaultBinding::KubernetesSecret(_) => {
1071                Err(AlienError::new(ErrorData::FeatureNotEnabled {
1072                    feature: "kubernetes".to_string(),
1073                }))
1074            }
1075        }?;
1076
1077        self.put_cache("vault", binding_name, result.clone()).await;
1078        Ok(result)
1079    }
1080
1081    async fn load_kv(&self, binding_name: &str) -> Result<Arc<dyn Kv>> {
1082        if let Some(cached) = self.get_cached::<Arc<dyn Kv>>("kv", binding_name).await {
1083            return Ok(cached);
1084        }
1085
1086        use alien_core::bindings::KvBinding;
1087
1088        let binding_json = self.bindings.get(binding_name).ok_or_else(|| {
1089            AlienError::new(ErrorData::BindingConfigInvalid {
1090                binding_name: binding_name.to_string(),
1091                reason: "Binding not found".to_string(),
1092            })
1093        })?;
1094
1095        let binding: KvBinding = serde_json::from_value(binding_json.clone())
1096            .into_alien_error()
1097            .context(ErrorData::BindingConfigInvalid {
1098                binding_name: binding_name.to_string(),
1099                reason: "Failed to parse KV binding".to_string(),
1100            })?;
1101
1102        let result: Arc<dyn Kv> = match binding {
1103            #[cfg(feature = "aws")]
1104            KvBinding::Dynamodb(config) => {
1105                use crate::providers::kv::aws_dynamodb::AwsDynamodbKv;
1106
1107                let table_name = config
1108                    .table_name
1109                    .into_value(binding_name, "table_name")
1110                    .context(ErrorData::BindingConfigInvalid {
1111                        binding_name: binding_name.to_string(),
1112                        reason: "Failed to extract table_name from DynamoDB binding".to_string(),
1113                    })?;
1114
1115                let aws_config = self.client_config.aws_config().ok_or_else(|| {
1116                    AlienError::new(ErrorData::ClientConfigInvalid {
1117                        platform: Platform::Aws,
1118                        message: "AWS config not available".to_string(),
1119                    })
1120                })?;
1121
1122                let credentials =
1123                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
1124                        .await
1125                        .context(ErrorData::ClientConfigInvalid {
1126                            platform: Platform::Aws,
1127                            message: "Failed to create AWS credential provider".to_string(),
1128                        })?;
1129                let dynamodb_client = alien_aws_clients::dynamodb::DynamoDbClient::new(
1130                    crate::http_client::create_http_client(),
1131                    credentials,
1132                );
1133                let kv_impl = AwsDynamodbKv::new(table_name, dynamodb_client);
1134                let kv: Arc<dyn Kv> = Arc::new(kv_impl);
1135                Ok(kv)
1136            }
1137            #[cfg(not(feature = "aws"))]
1138            KvBinding::Dynamodb(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1139                feature: "aws".to_string(),
1140            })),
1141
1142            #[cfg(feature = "gcp")]
1143            KvBinding::Firestore(config) => {
1144                use crate::providers::kv::gcp_firestore::GcpFirestoreKv;
1145                use alien_gcp_clients::firestore::FirestoreClient;
1146
1147                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1148                    AlienError::new(ErrorData::ClientConfigInvalid {
1149                        platform: Platform::Gcp,
1150                        message: "GCP config not available".to_string(),
1151                    })
1152                })?;
1153
1154                let client = FirestoreClient::new(
1155                    crate::http_client::create_http_client(),
1156                    gcp_config.clone(),
1157                );
1158
1159                let project_id = config
1160                    .project_id
1161                    .into_value(binding_name, "project_id")
1162                    .context(ErrorData::BindingConfigInvalid {
1163                        binding_name: binding_name.to_string(),
1164                        reason: "Failed to extract project_id from Firestore binding".to_string(),
1165                    })?;
1166
1167                let database_id = config
1168                    .database_id
1169                    .into_value(binding_name, "database_id")
1170                    .context(ErrorData::BindingConfigInvalid {
1171                        binding_name: binding_name.to_string(),
1172                        reason: "Failed to extract database_id from Firestore binding".to_string(),
1173                    })?;
1174
1175                let collection_name = config
1176                    .collection_name
1177                    .into_value(binding_name, "collection_name")
1178                    .context(ErrorData::BindingConfigInvalid {
1179                        binding_name: binding_name.to_string(),
1180                        reason: "Failed to extract collection_name from Firestore binding"
1181                            .to_string(),
1182                    })?;
1183
1184                let kv: Arc<dyn Kv> = Arc::new(GcpFirestoreKv::new(
1185                    client,
1186                    project_id,
1187                    database_id,
1188                    collection_name,
1189                )?);
1190                Ok(kv)
1191            }
1192            #[cfg(not(feature = "gcp"))]
1193            KvBinding::Firestore(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1194                feature: "gcp".to_string(),
1195            })),
1196
1197            #[cfg(feature = "azure")]
1198            KvBinding::TableStorage(config) => {
1199                use crate::providers::kv::azure_table_storage::AzureTableStorageKv;
1200                use alien_azure_clients::tables::AzureTableStorageClient;
1201                use alien_azure_clients::AzureTokenCache;
1202
1203                let azure_config = self.client_config.azure_config().ok_or_else(|| {
1204                    AlienError::new(ErrorData::ClientConfigInvalid {
1205                        platform: Platform::Azure,
1206                        message: "Azure config not available".to_string(),
1207                    })
1208                })?;
1209
1210                let resource_group_name = config
1211                    .resource_group_name
1212                    .into_value(binding_name, "resource_group_name")
1213                    .context(ErrorData::BindingConfigInvalid {
1214                        binding_name: binding_name.to_string(),
1215                        reason: "Failed to extract resource_group_name from TableStorage binding"
1216                            .to_string(),
1217                    })?;
1218
1219                let account_name = config
1220                    .account_name
1221                    .into_value(binding_name, "account_name")
1222                    .context(ErrorData::BindingConfigInvalid {
1223                        binding_name: binding_name.to_string(),
1224                        reason: "Failed to extract account_name from TableStorage binding"
1225                            .to_string(),
1226                    })?;
1227
1228                let table_name = config
1229                    .table_name
1230                    .into_value(binding_name, "table_name")
1231                    .context(ErrorData::BindingConfigInvalid {
1232                        binding_name: binding_name.to_string(),
1233                        reason: "Failed to extract table_name from TableStorage binding"
1234                            .to_string(),
1235                    })?;
1236
1237                let client = AzureTableStorageClient::new(
1238                    crate::http_client::create_http_client(),
1239                    AzureTokenCache::new(azure_config.clone()),
1240                );
1241
1242                let kv_impl =
1243                    AzureTableStorageKv::new(client, resource_group_name, account_name, table_name);
1244                let kv: Arc<dyn Kv> = Arc::new(kv_impl);
1245                Ok(kv)
1246            }
1247            #[cfg(not(feature = "azure"))]
1248            KvBinding::TableStorage(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1249                feature: "azure".to_string(),
1250            })),
1251
1252            #[cfg(feature = "local")]
1253            KvBinding::Local(local_binding) => {
1254                use crate::providers::kv::local::LocalKv;
1255                use std::path::PathBuf;
1256
1257                // Get data directory from binding
1258                let data_dir = PathBuf::from(
1259                    local_binding
1260                        .data_dir
1261                        .into_value(binding_name, "data_dir")
1262                        .context(ErrorData::BindingConfigInvalid {
1263                            binding_name: binding_name.to_string(),
1264                            reason: "Failed to extract data_dir from Local binding".to_string(),
1265                        })?,
1266                );
1267
1268                // Create local disk-persisted KV implementation
1269                let kv_impl = LocalKv::new(data_dir).await?;
1270
1271                let kv: Arc<dyn Kv> = Arc::new(kv_impl);
1272                Ok(kv)
1273            }
1274            #[cfg(not(feature = "local"))]
1275            KvBinding::Local { .. } => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1276                feature: "local".to_string(),
1277            })),
1278
1279            KvBinding::Redis(_) => Err(AlienError::new(ErrorData::NotImplemented {
1280                operation: "Redis KV binding".to_string(),
1281                reason: "Redis KV provider is not yet implemented".to_string(),
1282            })),
1283        }?;
1284
1285        self.put_cache("kv", binding_name, result.clone()).await;
1286        Ok(result)
1287    }
1288
1289    async fn load_postgres(&self, binding_name: &str) -> Result<Arc<dyn Postgres>> {
1290        if let Some(cached) = self
1291            .get_cached::<Arc<dyn Postgres>>("postgres", binding_name)
1292            .await
1293        {
1294            return Ok(cached);
1295        }
1296
1297        let binding_json = self.bindings.get(binding_name).ok_or_else(|| {
1298            AlienError::new(ErrorData::BindingConfigInvalid {
1299                binding_name: binding_name.to_string(),
1300                reason: "Binding not found".to_string(),
1301            })
1302        })?;
1303
1304        let binding: PostgresBinding = serde_json::from_value(binding_json.clone())
1305            .into_alien_error()
1306            .context(ErrorData::BindingConfigInvalid {
1307                binding_name: binding_name.to_string(),
1308                reason: "Failed to parse Postgres binding".to_string(),
1309            })?;
1310
1311        let result: Arc<dyn Postgres> = Arc::new(
1312            crate::providers::postgres::local::LocalPostgres::from_binding(binding_name, &binding)?,
1313        );
1314
1315        self.put_cache("postgres", binding_name, result.clone())
1316            .await;
1317        Ok(result)
1318    }
1319
1320    async fn load_queue(&self, binding_name: &str) -> Result<Arc<dyn Queue>> {
1321        if let Some(cached) = self
1322            .get_cached::<Arc<dyn Queue>>("queue", binding_name)
1323            .await
1324        {
1325            return Ok(cached);
1326        }
1327
1328        use alien_core::bindings::QueueBinding;
1329
1330        let binding_json = self.bindings.get(binding_name).ok_or_else(|| {
1331            AlienError::new(ErrorData::BindingConfigInvalid {
1332                binding_name: binding_name.to_string(),
1333                reason: "Binding not found".to_string(),
1334            })
1335        })?;
1336
1337        let binding: QueueBinding = serde_json::from_value(binding_json.clone())
1338            .into_alien_error()
1339            .context(ErrorData::BindingConfigInvalid {
1340                binding_name: binding_name.to_string(),
1341                reason: "Failed to parse Queue binding".to_string(),
1342            })?;
1343
1344        let result: Arc<dyn Queue> = match binding {
1345            #[cfg(feature = "aws")]
1346            QueueBinding::Sqs(config) => {
1347                use crate::providers::queue::aws_sqs::AwsSqsQueue;
1348
1349                let queue_url = config
1350                    .queue_url
1351                    .into_value(binding_name, "queue_url")
1352                    .context(ErrorData::BindingConfigInvalid {
1353                        binding_name: binding_name.to_string(),
1354                        reason: "Failed to extract queue_url from SQS binding".to_string(),
1355                    })?;
1356
1357                let aws_config = self.client_config.aws_config().ok_or_else(|| {
1358                    AlienError::new(ErrorData::ClientConfigInvalid {
1359                        platform: Platform::Aws,
1360                        message: "AWS config not available".to_string(),
1361                    })
1362                })?;
1363                let credentials =
1364                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
1365                        .await
1366                        .context(ErrorData::ClientConfigInvalid {
1367                            platform: Platform::Aws,
1368                            message: "Failed to create AWS credential provider".to_string(),
1369                        })?;
1370                let client = alien_aws_clients::sqs::SqsClient::new(
1371                    crate::http_client::create_http_client(),
1372                    credentials,
1373                );
1374                let q: Arc<dyn Queue> = Arc::new(AwsSqsQueue::new(queue_url, client));
1375                Ok(q)
1376            }
1377            #[cfg(not(feature = "aws"))]
1378            QueueBinding::Sqs(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1379                feature: "aws".to_string(),
1380            })),
1381
1382            #[cfg(feature = "gcp")]
1383            QueueBinding::Pubsub(config) => {
1384                use crate::providers::queue::gcp_pubsub::GcpPubSubQueue;
1385                let topic_name = config.topic.into_value(binding_name, "topic").context(
1386                    ErrorData::BindingConfigInvalid {
1387                        binding_name: binding_name.to_string(),
1388                        reason: "Failed to extract topic".to_string(),
1389                    },
1390                )?;
1391                let subscription_name = config
1392                    .subscription
1393                    .into_value(binding_name, "subscription")
1394                    .context(ErrorData::BindingConfigInvalid {
1395                        binding_name: binding_name.to_string(),
1396                        reason: "Failed to extract subscription".to_string(),
1397                    })?;
1398                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1399                    AlienError::new(ErrorData::ClientConfigInvalid {
1400                        platform: Platform::Gcp,
1401                        message: "GCP config not available".to_string(),
1402                    })
1403                })?;
1404
1405                // Pass short names — PubSubClient methods prepend the project prefix
1406                let topic = if let Some(short) =
1407                    topic_name.strip_prefix(&format!("projects/{}/topics/", gcp_config.project_id))
1408                {
1409                    short.to_string()
1410                } else {
1411                    topic_name
1412                };
1413                let subscription = if let Some(short) = subscription_name.strip_prefix(&format!(
1414                    "projects/{}/subscriptions/",
1415                    gcp_config.project_id
1416                )) {
1417                    short.to_string()
1418                } else {
1419                    subscription_name
1420                };
1421
1422                let q: Arc<dyn Queue> =
1423                    Arc::new(GcpPubSubQueue::new(topic, subscription, gcp_config.clone()).await?);
1424                Ok(q)
1425            }
1426            #[cfg(not(feature = "gcp"))]
1427            QueueBinding::Pubsub(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1428                feature: "gcp".to_string(),
1429            })),
1430
1431            #[cfg(feature = "azure")]
1432            QueueBinding::Servicebus(config) => {
1433                use crate::providers::queue::azure_service_bus::AzureServiceBusQueue;
1434                let namespace = config
1435                    .namespace
1436                    .into_value(binding_name, "namespace")
1437                    .context(ErrorData::BindingConfigInvalid {
1438                        binding_name: binding_name.to_string(),
1439                        reason: "Failed to extract namespace".to_string(),
1440                    })?;
1441                let queue_name = config
1442                    .queue_name
1443                    .into_value(binding_name, "queue_name")
1444                    .context(ErrorData::BindingConfigInvalid {
1445                        binding_name: binding_name.to_string(),
1446                        reason: "Failed to extract queue_name".to_string(),
1447                    })?;
1448                let azure_config = self.client_config.azure_config().ok_or_else(|| {
1449                    AlienError::new(ErrorData::ClientConfigInvalid {
1450                        platform: Platform::Azure,
1451                        message: "Azure config not available".to_string(),
1452                    })
1453                })?;
1454                let q: Arc<dyn Queue> = Arc::new(
1455                    AzureServiceBusQueue::new(namespace, queue_name, azure_config.clone()).await?,
1456                );
1457                Ok(q)
1458            }
1459            #[cfg(not(feature = "azure"))]
1460            QueueBinding::Servicebus(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1461                feature: "azure".to_string(),
1462            })),
1463
1464            #[cfg(feature = "local")]
1465            QueueBinding::Local(config) => {
1466                use crate::providers::queue::local::LocalQueue;
1467
1468                let queue = LocalQueue::from_binding(config).await?;
1469                let q: Arc<dyn Queue> = Arc::new(queue);
1470                Ok(q)
1471            }
1472            #[cfg(not(feature = "local"))]
1473            QueueBinding::Local(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1474                feature: "local".to_string(),
1475            })),
1476        }?;
1477
1478        self.put_cache("queue", binding_name, result.clone()).await;
1479        Ok(result)
1480    }
1481
1482    async fn load_worker(&self, binding_name: &str) -> Result<Arc<dyn Worker>> {
1483        use alien_core::bindings::WorkerBinding;
1484
1485        let binding_json = self.bindings.get(binding_name).ok_or_else(|| {
1486            AlienError::new(ErrorData::BindingConfigInvalid {
1487                binding_name: binding_name.to_string(),
1488                reason: "Binding not found".to_string(),
1489            })
1490        })?;
1491
1492        let binding: WorkerBinding = serde_json::from_value(binding_json.clone())
1493            .into_alien_error()
1494            .context(ErrorData::BindingConfigInvalid {
1495                binding_name: binding_name.to_string(),
1496                reason: "Failed to parse worker binding".to_string(),
1497            })?;
1498
1499        match binding {
1500            #[cfg(feature = "aws")]
1501            WorkerBinding::Lambda(lambda_binding) => {
1502                use crate::providers::worker::LambdaWorker;
1503
1504                let aws_config = self.client_config.aws_config().ok_or_else(|| {
1505                    AlienError::new(ErrorData::ClientConfigInvalid {
1506                        platform: Platform::Aws,
1507                        message: "AWS config not available".to_string(),
1508                    })
1509                })?;
1510                let credentials =
1511                    alien_aws_clients::AwsCredentialProvider::from_config(aws_config.clone())
1512                        .await
1513                        .context(ErrorData::ClientConfigInvalid {
1514                            platform: Platform::Aws,
1515                            message: "Failed to create AWS credential provider".to_string(),
1516                        })?;
1517                let client = crate::http_client::create_http_client();
1518
1519                let function_impl = LambdaWorker::new(client, credentials, lambda_binding);
1520                let function: Arc<dyn Worker> = Arc::new(function_impl);
1521                Ok(function)
1522            }
1523            #[cfg(not(feature = "aws"))]
1524            WorkerBinding::Lambda(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1525                feature: "aws".to_string(),
1526            })),
1527
1528            #[cfg(feature = "gcp")]
1529            WorkerBinding::CloudRun(cloudrun_binding) => {
1530                use crate::providers::worker::CloudRunWorker;
1531
1532                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1533                    AlienError::new(ErrorData::ClientConfigInvalid {
1534                        platform: Platform::Gcp,
1535                        message: "GCP config not available".to_string(),
1536                    })
1537                })?;
1538                let client = crate::http_client::create_http_client();
1539
1540                let function_impl =
1541                    CloudRunWorker::new(client, gcp_config.clone(), cloudrun_binding);
1542                let function: Arc<dyn Worker> = Arc::new(function_impl);
1543                Ok(function)
1544            }
1545            #[cfg(not(feature = "gcp"))]
1546            WorkerBinding::CloudRun(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1547                feature: "gcp".to_string(),
1548            })),
1549
1550            #[cfg(feature = "azure")]
1551            WorkerBinding::ContainerApp(container_app_binding) => {
1552                use crate::providers::worker::ContainerAppWorker;
1553
1554                let azure_config = self.client_config.azure_config().ok_or_else(|| {
1555                    AlienError::new(ErrorData::ClientConfigInvalid {
1556                        platform: Platform::Azure,
1557                        message: "Azure config not available".to_string(),
1558                    })
1559                })?;
1560                let client = crate::http_client::create_http_client();
1561
1562                let function_impl =
1563                    ContainerAppWorker::new(client, azure_config.clone(), container_app_binding);
1564                let function: Arc<dyn Worker> = Arc::new(function_impl);
1565                Ok(function)
1566            }
1567            #[cfg(not(feature = "azure"))]
1568            WorkerBinding::ContainerApp(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1569                feature: "azure".to_string(),
1570            })),
1571
1572            #[cfg(feature = "local")]
1573            WorkerBinding::Local(local_binding) => {
1574                use crate::providers::worker::LocalWorker;
1575
1576                let function_impl = LocalWorker::new(local_binding);
1577                let function: Arc<dyn Worker> = Arc::new(function_impl);
1578                Ok(function)
1579            }
1580            #[cfg(not(feature = "local"))]
1581            WorkerBinding::Local(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1582                feature: "local".to_string(),
1583            })),
1584
1585            #[cfg(feature = "kubernetes")]
1586            WorkerBinding::Kubernetes(kubernetes_binding) => {
1587                use crate::providers::worker::KubernetesWorker;
1588
1589                let function_impl =
1590                    KubernetesWorker::new(binding_name.to_string(), kubernetes_binding)?;
1591                let function: Arc<dyn Worker> = Arc::new(function_impl);
1592                Ok(function)
1593            }
1594            #[cfg(not(feature = "kubernetes"))]
1595            WorkerBinding::Kubernetes(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1596                feature: "kubernetes".to_string(),
1597            })),
1598        }
1599    }
1600
1601    async fn load_container(
1602        &self,
1603        binding_name: &str,
1604    ) -> Result<Arc<dyn crate::traits::Container>> {
1605        use alien_core::bindings::ContainerBinding;
1606
1607        let binding_json = self.bindings.get(binding_name).ok_or_else(|| {
1608            AlienError::new(ErrorData::BindingConfigInvalid {
1609                binding_name: binding_name.to_string(),
1610                reason: "Binding not found".to_string(),
1611            })
1612        })?;
1613
1614        let binding: ContainerBinding = serde_json::from_value(binding_json.clone())
1615            .into_alien_error()
1616            .context(ErrorData::BindingConfigInvalid {
1617                binding_name: binding_name.to_string(),
1618                reason: "Failed to parse container binding".to_string(),
1619            })?;
1620
1621        match binding {
1622            ContainerBinding::Horizon(horizon_binding) => {
1623                use crate::providers::container::HorizonContainer;
1624
1625                let container_impl = HorizonContainer::new(horizon_binding)?;
1626                let container: Arc<dyn crate::traits::Container> = Arc::new(container_impl);
1627                Ok(container)
1628            }
1629
1630            #[cfg(feature = "local")]
1631            ContainerBinding::Local(local_binding) => {
1632                use crate::providers::container::LocalContainer;
1633
1634                let container_impl = LocalContainer::new(local_binding)?;
1635                let container: Arc<dyn crate::traits::Container> = Arc::new(container_impl);
1636                Ok(container)
1637            }
1638            #[cfg(not(feature = "local"))]
1639            ContainerBinding::Local(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1640                feature: "local".to_string(),
1641            })),
1642
1643            #[cfg(feature = "kubernetes")]
1644            ContainerBinding::Kubernetes(kubernetes_binding) => {
1645                use crate::providers::container::KubernetesContainer;
1646
1647                let container_impl =
1648                    KubernetesContainer::new(binding_name.to_string(), kubernetes_binding)?;
1649                let container: Arc<dyn crate::traits::Container> = Arc::new(container_impl);
1650                Ok(container)
1651            }
1652            #[cfg(not(feature = "kubernetes"))]
1653            ContainerBinding::Kubernetes(_) => Err(AlienError::new(ErrorData::FeatureNotEnabled {
1654                feature: "kubernetes".to_string(),
1655            })),
1656        }
1657    }
1658
1659    async fn load_service_account(
1660        &self,
1661        binding_name: &str,
1662    ) -> Result<Arc<dyn crate::traits::ServiceAccount>> {
1663        use alien_core::bindings::ServiceAccountBinding;
1664
1665        let binding_json = self.bindings.get(binding_name).ok_or_else(|| {
1666            AlienError::new(ErrorData::BindingConfigInvalid {
1667                binding_name: binding_name.to_string(),
1668                reason: "Binding not found".to_string(),
1669            })
1670        })?;
1671
1672        let binding: ServiceAccountBinding = serde_json::from_value(binding_json.clone())
1673            .into_alien_error()
1674            .context(ErrorData::BindingConfigInvalid {
1675                binding_name: binding_name.to_string(),
1676                reason: "Failed to parse service account binding".to_string(),
1677            })?;
1678
1679        match binding {
1680            #[cfg(feature = "aws")]
1681            ServiceAccountBinding::AwsIam(aws_binding) => {
1682                use crate::providers::service_account::aws_iam::AwsIamServiceAccount;
1683
1684                let aws_config = self.client_config.aws_config().ok_or_else(|| {
1685                    AlienError::new(ErrorData::ClientConfigInvalid {
1686                        platform: Platform::Aws,
1687                        message: "AWS config not available".to_string(),
1688                    })
1689                })?;
1690                let client = crate::http_client::create_http_client();
1691
1692                let service_account_impl =
1693                    AwsIamServiceAccount::new(client, aws_config.clone(), aws_binding);
1694                let service_account: Arc<dyn crate::traits::ServiceAccount> =
1695                    Arc::new(service_account_impl);
1696                Ok(service_account)
1697            }
1698            #[cfg(not(feature = "aws"))]
1699            ServiceAccountBinding::AwsIam(_) => {
1700                Err(AlienError::new(ErrorData::FeatureNotEnabled {
1701                    feature: "aws".to_string(),
1702                }))
1703            }
1704
1705            #[cfg(feature = "gcp")]
1706            ServiceAccountBinding::GcpServiceAccount(gcp_binding) => {
1707                use crate::providers::service_account::gcp_service_account::GcpServiceAccount;
1708
1709                let gcp_config = self.client_config.gcp_config().ok_or_else(|| {
1710                    AlienError::new(ErrorData::ClientConfigInvalid {
1711                        platform: Platform::Gcp,
1712                        message: "GCP config not available".to_string(),
1713                    })
1714                })?;
1715                let client = crate::http_client::create_http_client();
1716
1717                let service_account_impl =
1718                    GcpServiceAccount::new(client, gcp_config.clone(), gcp_binding);
1719                let service_account: Arc<dyn crate::traits::ServiceAccount> =
1720                    Arc::new(service_account_impl);
1721                Ok(service_account)
1722            }
1723            #[cfg(not(feature = "gcp"))]
1724            ServiceAccountBinding::GcpServiceAccount(_) => {
1725                Err(AlienError::new(ErrorData::FeatureNotEnabled {
1726                    feature: "gcp".to_string(),
1727                }))
1728            }
1729
1730            #[cfg(feature = "azure")]
1731            ServiceAccountBinding::AzureManagedIdentity(azure_binding) => {
1732                use crate::providers::service_account::azure_managed_identity::AzureManagedIdentityServiceAccount;
1733
1734                let azure_config = self.client_config.azure_config().ok_or_else(|| {
1735                    AlienError::new(ErrorData::ClientConfigInvalid {
1736                        platform: Platform::Azure,
1737                        message: "Azure config not available".to_string(),
1738                    })
1739                })?;
1740
1741                let service_account_impl =
1742                    AzureManagedIdentityServiceAccount::new(azure_config.clone(), azure_binding);
1743                let service_account: Arc<dyn crate::traits::ServiceAccount> =
1744                    Arc::new(service_account_impl);
1745                Ok(service_account)
1746            }
1747            #[cfg(not(feature = "azure"))]
1748            ServiceAccountBinding::AzureManagedIdentity(_) => {
1749                Err(AlienError::new(ErrorData::FeatureNotEnabled {
1750                    feature: "azure".to_string(),
1751                }))
1752            }
1753        }
1754    }
1755}
1756
1757#[cfg(test)]
1758mod tests {
1759    use super::*;
1760    use alien_core::ENV_ALIEN_DEPLOYMENT_TYPE;
1761
1762    fn kubernetes_aws_env() -> HashMap<String, String> {
1763        HashMap::from([
1764            (
1765                ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
1766                Platform::Kubernetes.as_str().to_string(),
1767            ),
1768            (
1769                ENV_OPERATOR_BASE_PLATFORM.to_string(),
1770                Platform::Aws.as_str().to_string(),
1771            ),
1772            (
1773                "KUBERNETES_SERVICE_HOST".to_string(),
1774                "10.0.0.1".to_string(),
1775            ),
1776            ("KUBERNETES_SERVICE_PORT".to_string(), "443".to_string()),
1777            ("AWS_REGION".to_string(), "us-east-1".to_string()),
1778            ("AWS_ACCOUNT_ID".to_string(), "123456789012".to_string()),
1779            ("AWS_ACCESS_KEY_ID".to_string(), "test".to_string()),
1780            ("AWS_SECRET_ACCESS_KEY".to_string(), "test".to_string()),
1781        ])
1782    }
1783
1784    fn kubernetes_azure_env() -> HashMap<String, String> {
1785        HashMap::from([
1786            (
1787                ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
1788                Platform::Kubernetes.as_str().to_string(),
1789            ),
1790            (
1791                ENV_OPERATOR_BASE_PLATFORM.to_string(),
1792                Platform::Azure.as_str().to_string(),
1793            ),
1794            (
1795                "KUBERNETES_SERVICE_HOST".to_string(),
1796                "10.0.0.1".to_string(),
1797            ),
1798            ("KUBERNETES_SERVICE_PORT".to_string(), "443".to_string()),
1799            (
1800                "AZURE_SUBSCRIPTION_ID".to_string(),
1801                "00000000-0000-0000-0000-000000000000".to_string(),
1802            ),
1803            (
1804                "AZURE_TENANT_ID".to_string(),
1805                "11111111-1111-1111-1111-111111111111".to_string(),
1806            ),
1807            ("AZURE_REGION".to_string(), "eastus".to_string()),
1808            (
1809                "AZURE_CLIENT_ID".to_string(),
1810                "22222222-2222-2222-2222-222222222222".to_string(),
1811            ),
1812            (
1813                "AZURE_FEDERATED_TOKEN_FILE".to_string(),
1814                "/var/run/secrets/azure/tokens/azure-identity-token".to_string(),
1815            ),
1816            (
1817                "AZURE_AUTHORITY_HOST".to_string(),
1818                "https://login.microsoftonline.com/".to_string(),
1819            ),
1820        ])
1821    }
1822
1823    #[tokio::test]
1824    async fn lazy_env_provider_defers_cloud_client_config_until_binding_use() {
1825        let env = HashMap::from([
1826            (
1827                ENV_ALIEN_DEPLOYMENT_TYPE.to_string(),
1828                Platform::Aws.as_str().to_string(),
1829            ),
1830            ("AWS_EC2_METADATA_DISABLED".to_string(), "true".to_string()),
1831            (
1832                "AWS_PROFILE".to_string(),
1833                "__alien_missing_test_profile__".to_string(),
1834            ),
1835            (
1836                "ALIEN_SECRETS_BINDING".to_string(),
1837                r#"{"service":"parameter-store","vaultPrefix":"test-secrets"}"#.to_string(),
1838            ),
1839        ]);
1840
1841        let provider = BindingsProvider::from_env_lazy(env)
1842            .expect("lazy provider construction should validate binding JSON without AWS config");
1843
1844        let error = provider
1845            .load_vault("secrets")
1846            .await
1847            .expect_err("binding use should still require AWS client config");
1848
1849        assert_eq!(error.code, "CLIENT_CONFIG_INVALID");
1850    }
1851
1852    #[tokio::test]
1853    async fn from_env_builds_kubernetes_cloud_config_when_base_platform_is_set() {
1854        let provider = BindingsProvider::from_env(kubernetes_aws_env())
1855            .await
1856            .unwrap();
1857
1858        assert!(provider.client_config.kubernetes_config().is_some());
1859        assert!(provider.client_config.aws_config().is_some());
1860        assert!(matches!(
1861            provider.client_config,
1862            ClientConfig::KubernetesCloud { .. }
1863        ));
1864    }
1865
1866    #[tokio::test]
1867    async fn from_env_builds_kubernetes_cloud_config_for_azure_workload_identity() {
1868        let provider = BindingsProvider::from_env(kubernetes_azure_env())
1869            .await
1870            .unwrap();
1871
1872        assert!(provider.client_config.kubernetes_config().is_some());
1873        assert!(provider.client_config.azure_config().is_some());
1874        assert!(matches!(
1875            provider.client_config,
1876            ClientConfig::KubernetesCloud { .. }
1877        ));
1878    }
1879
1880    #[tokio::test]
1881    async fn from_env_rejects_non_cloud_kubernetes_base_platform() {
1882        let mut env = kubernetes_aws_env();
1883        env.insert(
1884            ENV_OPERATOR_BASE_PLATFORM.to_string(),
1885            Platform::Kubernetes.as_str().to_string(),
1886        );
1887
1888        let error = BindingsProvider::from_env(env).await.unwrap_err();
1889
1890        assert!(error.to_string().contains(ENV_OPERATOR_BASE_PLATFORM));
1891    }
1892}
1893
1894/// Conversion functions between SDK types and alien-core types
1895#[cfg(feature = "platform-sdk")]
1896mod conversions {
1897    use super::*;
1898    use serde::Serialize;
1899
1900    /// Convert SDK AgentStackState to alien-core StackState
1901    /// Generic over any serializable type since we convert via JSON
1902    pub fn convert_stack_state<T: Serialize>(sdk_stack_state: &T) -> Result<StackState> {
1903        // Convert via JSON serialization/deserialization (same pattern as deploy.rs)
1904        let stack_state: StackState = serde_json::from_value(
1905            serde_json::to_value(sdk_stack_state)
1906                .into_alien_error()
1907                .context(ErrorData::BindingConfigInvalid {
1908                    binding_name: "stack_state".to_string(),
1909                    reason: "Failed to serialize SDK stack state".to_string(),
1910                })?,
1911        )
1912        .into_alien_error()
1913        .context(ErrorData::BindingConfigInvalid {
1914            binding_name: "stack_state".to_string(),
1915            reason: "Failed to parse stack state".to_string(),
1916        })?;
1917
1918        Ok(stack_state)
1919    }
1920}