Skip to main content

alien_core/
runtime_environment.rs

1use crate::{bindings::binding_env_var_name, ErrorData, Platform, ResourceRef, Result};
2use alien_error::AlienError;
3use std::collections::HashMap;
4
5pub const ENV_ALIEN_CURRENT_WORKER_BINDING_NAME: &str = "ALIEN_CURRENT_WORKER_BINDING_NAME";
6pub const ENV_ALIEN_CURRENT_CONTAINER_BINDING_NAME: &str = "ALIEN_CURRENT_CONTAINER_BINDING_NAME";
7pub const ENV_OPERATOR_BASE_PLATFORM: &str = "OPERATOR_BASE_PLATFORM";
8pub const ENV_ALIEN_DEPLOYMENT_TYPE: &str = "ALIEN_DEPLOYMENT_TYPE";
9pub const ENV_ALIEN_LAMBDA_MODE: &str = "ALIEN_LAMBDA_MODE";
10pub const ENV_ALIEN_RUNTIME_SEND_OTLP: &str = "ALIEN_RUNTIME_SEND_OTLP";
11pub const ENV_ALIEN_RUNTIME_SECRETS: &str = "ALIEN_RUNTIME_SECRETS";
12pub const ENV_ALIEN_SECRETS: &str = "ALIEN_SECRETS";
13/// Opaque deployment-managed revision that rolls workloads when referenced
14/// secret values change without exposing secret-derived data in the spec.
15pub const ENV_ALIEN_SECRET_ENV_REVISION: &str = "ALIEN_SECRET_ENV_REVISION";
16pub const ENV_ALIEN_TRANSPORT: &str = "ALIEN_TRANSPORT";
17pub const ENV_ALIEN_DEPLOYMENT_ID: &str = "ALIEN_DEPLOYMENT_ID";
18pub const ENV_ALIEN_DEPLOYMENT_NAME: &str = "ALIEN_DEPLOYMENT_NAME";
19/// Identifies the current app resource within its deployment stack. Unlike
20/// command-specific target variables, this is the universal resource identity
21/// name. External/bootstrap mint clients include it when requesting
22/// resource-scoped credentials.
23pub const ENV_ALIEN_RESOURCE_ID: &str = "ALIEN_RESOURCE_ID";
24pub const ENV_ALIEN_PUBLIC_ENDPOINTS_JSON: &str = "ALIEN_PUBLIC_ENDPOINTS_JSON";
25pub const ENV_ALIEN_COMMANDS_TOKEN: &str = "ALIEN_COMMANDS_TOKEN";
26/// File containing the command receiver bearer token. Receivers re-read this
27/// file after an unauthorized response so controllers can rotate credentials
28/// without restarting the workload.
29pub const ENV_ALIEN_COMMANDS_TOKEN_FILE: &str = "ALIEN_COMMANDS_TOKEN_FILE";
30/// Lease duration requested by app-owned command receivers, in seconds.
31pub const ENV_ALIEN_COMMANDS_LEASE_SECONDS: &str = "ALIEN_COMMANDS_LEASE_SECONDS";
32/// Maximum command leases requested by an app-owned receiver per poll.
33pub const ENV_ALIEN_COMMANDS_MAX_LEASES: &str = "ALIEN_COMMANDS_MAX_LEASES";
34/// Base command receiver poll interval, in milliseconds.
35pub const ENV_ALIEN_COMMANDS_POLL_INTERVAL_MS: &str = "ALIEN_COMMANDS_POLL_INTERVAL_MS";
36/// Maximum command receiver poll interval after adaptive backoff, in milliseconds.
37pub const ENV_ALIEN_COMMANDS_POLL_MAX_INTERVAL_MS: &str = "ALIEN_COMMANDS_POLL_MAX_INTERVAL_MS";
38/// Fractional jitter applied to command receiver poll intervals.
39pub const ENV_ALIEN_COMMANDS_POLL_JITTER: &str = "ALIEN_COMMANDS_POLL_JITTER";
40/// Graceful command receiver drain timeout, in milliseconds.
41pub const ENV_ALIEN_COMMANDS_DRAIN_TIMEOUT_MS: &str = "ALIEN_COMMANDS_DRAIN_TIMEOUT_MS";
42/// Identifies which stack resource an app-owned command receiver leases for.
43pub const ENV_ALIEN_COMMANDS_TARGET_RESOURCE_ID: &str = "ALIEN_COMMANDS_TARGET_RESOURCE_ID";
44/// Base URL of the command server API an app-owned pull `Receiver`
45/// (Container/Daemon) leases commands from. Pinned by the receiver contract;
46/// the TypeScript receiver reads the same variable. Missing or
47/// invalid values fail fast with `COMMAND_RECEIVER_CONFIG_INVALID`. Injected
48/// by the manager and operator controllers, scoped per command-enabled
49/// resource.
50pub const ENV_ALIEN_COMMANDS_URL: &str = "ALIEN_COMMANDS_URL";
51/// Type of the command target a pull `Receiver` leases for (`container` |
52/// `daemon`). Lease requests require a typed target and a receiver must not
53/// guess it (the worker runtime hardcodes `worker`; a Container/Daemon
54/// receiver gets its type injected). Companion to
55/// [`ENV_ALIEN_COMMANDS_TARGET_RESOURCE_ID`].
56pub const ENV_ALIEN_COMMANDS_TARGET_RESOURCE_TYPE: &str = "ALIEN_COMMANDS_TARGET_RESOURCE_TYPE";
57/// Base URL of the deployment's manager. The client-side minting-backed
58/// credential resolver ([`alien-bindings`]) posts to `{ALIEN_MANAGER_URL}/v1/credentials/mint`
59/// when an external/bootstrap integration explicitly configures the mint
60/// environment contract. Managed cloud workloads use projected identities.
61pub const ENV_ALIEN_MANAGER_URL: &str = "ALIEN_MANAGER_URL";
62/// Deployment bearer token an external/bootstrap mint client presents to the
63/// manager. Kept distinct from [`ENV_ALIEN_COMMANDS_TOKEN`], which authenticates
64/// command delivery. Managed workload controllers do not inject this token.
65pub const ENV_ALIEN_DEPLOYMENT_TOKEN: &str = "ALIEN_DEPLOYMENT_TOKEN";
66/// Service-account binding name the minting resolver asks the manager to mint
67/// credentials for. Required by the external/bootstrap mint request contract.
68///
69/// Deliberately does **not** end in `_BINDING`: names matching `ALIEN_*_BINDING`
70/// are parsed as resource-binding JSON by the provider, which this is not.
71pub const ENV_ALIEN_DEPLOYMENT_SERVICE_ACCOUNT: &str = "ALIEN_DEPLOYMENT_SERVICE_ACCOUNT";
72/// Address of the worker app protocol gRPC server. The runtime binds its
73/// Control + WaitUntil services here and injects the same value for the
74/// application it spawns; the app connects as the gRPC client. Presence of this
75/// variable is what selects the worker-protocol gRPC channel in the app SDK.
76pub const ENV_ALIEN_WORKER_GRPC_ADDRESS: &str = "ALIEN_WORKER_GRPC_ADDRESS";
77/// Configured maximum Worker command execution time, in seconds. Controllers
78/// inject this from the trusted Worker resource rather than accepting a
79/// user-provided override.
80pub const ENV_ALIEN_WORKER_TIMEOUT_SECONDS: &str = "ALIEN_WORKER_TIMEOUT_SECONDS";
81pub const ENV_AWS_ACCOUNT_ID: &str = "AWS_ACCOUNT_ID";
82pub const ENV_AWS_REGION: &str = "AWS_REGION";
83pub const ENV_AZURE_CLIENT_ID: &str = "AZURE_CLIENT_ID";
84pub const ENV_AZURE_REGION: &str = "AZURE_REGION";
85pub const ENV_AZURE_SUBSCRIPTION_ID: &str = "AZURE_SUBSCRIPTION_ID";
86pub const ENV_AZURE_TENANT_ID: &str = "AZURE_TENANT_ID";
87pub const ENV_GCP_PROJECT_ID: &str = "GCP_PROJECT_ID";
88pub const ENV_GCP_REGION: &str = "GCP_REGION";
89pub const ENV_GOOGLE_CLOUD_PROJECT: &str = "GOOGLE_CLOUD_PROJECT";
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum RuntimeEnvironmentValue {
93    Literal(&'static str),
94    AwsAccountId,
95    AwsRegion,
96    AzureClientId,
97    AzureRegion,
98    AzureSubscriptionId,
99    AzureTenantId,
100    BasePlatform,
101    CurrentContainerBindingName,
102    CurrentWorkerBindingName,
103    GcpProjectId,
104    GcpRegion,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub struct RuntimeEnvironmentEntry {
109    pub name: &'static str,
110    pub value: RuntimeEnvironmentValue,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum RuntimeEnvironmentBindingSource {
115    LinkedResource(ResourceRef),
116    CurrentContainer,
117    CurrentWorker,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct RuntimeEnvironmentBindingEntry {
122    pub env_name: String,
123    pub binding_name: String,
124    pub source: RuntimeEnvironmentBindingSource,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub enum RuntimeEnvironmentPlanEntry {
129    Scalar(RuntimeEnvironmentEntry),
130    Binding(RuntimeEnvironmentBindingEntry),
131}
132
133#[derive(Debug, Clone, Default, PartialEq, Eq)]
134pub struct RuntimeEnvironmentPlan {
135    entries: Vec<RuntimeEnvironmentPlanEntry>,
136}
137
138impl RuntimeEnvironmentPlan {
139    pub fn new() -> Self {
140        Self::default()
141    }
142
143    pub fn add_scalar_entries(
144        mut self,
145        entries: impl IntoIterator<Item = RuntimeEnvironmentEntry>,
146    ) -> Self {
147        self.entries
148            .extend(entries.into_iter().map(RuntimeEnvironmentPlanEntry::Scalar));
149        self
150    }
151
152    pub fn add_linked_bindings(mut self, links: &[ResourceRef]) -> Self {
153        self.entries.extend(links.iter().cloned().map(|link| {
154            let binding_name = link.id().to_string();
155            RuntimeEnvironmentPlanEntry::Binding(RuntimeEnvironmentBindingEntry {
156                env_name: binding_env_var_name(&binding_name),
157                binding_name,
158                source: RuntimeEnvironmentBindingSource::LinkedResource(link),
159            })
160        }));
161        self
162    }
163
164    pub fn add_current_worker_binding(mut self, worker_id: &str) -> Self {
165        self.entries.push(RuntimeEnvironmentPlanEntry::Binding(
166            RuntimeEnvironmentBindingEntry {
167                env_name: binding_env_var_name(worker_id),
168                binding_name: worker_id.to_string(),
169                source: RuntimeEnvironmentBindingSource::CurrentWorker,
170            },
171        ));
172        self
173    }
174
175    pub fn add_current_container_binding(mut self, container_id: &str) -> Self {
176        self.entries.push(RuntimeEnvironmentPlanEntry::Binding(
177            RuntimeEnvironmentBindingEntry {
178                env_name: binding_env_var_name(container_id),
179                binding_name: container_id.to_string(),
180                source: RuntimeEnvironmentBindingSource::CurrentContainer,
181            },
182        ));
183        self
184    }
185
186    pub fn entries(&self) -> &[RuntimeEnvironmentPlanEntry] {
187        &self.entries
188    }
189}
190
191pub trait RuntimeEnvironmentRenderer {
192    type Value;
193
194    fn render_runtime_environment_value(
195        &self,
196        value: RuntimeEnvironmentValue,
197    ) -> Result<Option<Self::Value>>;
198
199    fn render_runtime_environment_binding(
200        &self,
201        entry: &RuntimeEnvironmentBindingEntry,
202    ) -> Result<Option<Self::Value>>;
203}
204
205pub fn standard_runtime_environment_plan(platform: Platform) -> Vec<RuntimeEnvironmentEntry> {
206    let mut entries = vec![RuntimeEnvironmentEntry {
207        name: ENV_ALIEN_DEPLOYMENT_TYPE,
208        value: RuntimeEnvironmentValue::Literal(platform.as_str()),
209    }];
210
211    match platform {
212        Platform::Aws => entries.push(RuntimeEnvironmentEntry {
213            name: ENV_AWS_ACCOUNT_ID,
214            value: RuntimeEnvironmentValue::AwsAccountId,
215        }),
216        Platform::Gcp => entries.extend([
217            RuntimeEnvironmentEntry {
218                name: ENV_GOOGLE_CLOUD_PROJECT,
219                value: RuntimeEnvironmentValue::GcpProjectId,
220            },
221            RuntimeEnvironmentEntry {
222                name: ENV_GCP_PROJECT_ID,
223                value: RuntimeEnvironmentValue::GcpProjectId,
224            },
225            RuntimeEnvironmentEntry {
226                name: ENV_GCP_REGION,
227                value: RuntimeEnvironmentValue::GcpRegion,
228            },
229        ]),
230        Platform::Azure => entries.extend([
231            RuntimeEnvironmentEntry {
232                name: ENV_AZURE_SUBSCRIPTION_ID,
233                value: RuntimeEnvironmentValue::AzureSubscriptionId,
234            },
235            RuntimeEnvironmentEntry {
236                name: ENV_AZURE_TENANT_ID,
237                value: RuntimeEnvironmentValue::AzureTenantId,
238            },
239            RuntimeEnvironmentEntry {
240                name: ENV_AZURE_REGION,
241                value: RuntimeEnvironmentValue::AzureRegion,
242            },
243        ]),
244        Platform::Kubernetes => entries.push(RuntimeEnvironmentEntry {
245            name: ENV_OPERATOR_BASE_PLATFORM,
246            value: RuntimeEnvironmentValue::BasePlatform,
247        }),
248        Platform::Machines | Platform::Local | Platform::Test => {}
249    }
250
251    entries
252}
253
254pub fn kubernetes_base_platform_runtime_environment_plan(
255    base_platform: Option<Platform>,
256) -> Vec<RuntimeEnvironmentEntry> {
257    match base_platform {
258        Some(Platform::Aws) => vec![
259            RuntimeEnvironmentEntry {
260                name: ENV_AWS_ACCOUNT_ID,
261                value: RuntimeEnvironmentValue::AwsAccountId,
262            },
263            RuntimeEnvironmentEntry {
264                name: ENV_AWS_REGION,
265                value: RuntimeEnvironmentValue::AwsRegion,
266            },
267        ],
268        Some(Platform::Gcp) => vec![
269            RuntimeEnvironmentEntry {
270                name: ENV_GOOGLE_CLOUD_PROJECT,
271                value: RuntimeEnvironmentValue::GcpProjectId,
272            },
273            RuntimeEnvironmentEntry {
274                name: ENV_GCP_PROJECT_ID,
275                value: RuntimeEnvironmentValue::GcpProjectId,
276            },
277            RuntimeEnvironmentEntry {
278                name: ENV_GCP_REGION,
279                value: RuntimeEnvironmentValue::GcpRegion,
280            },
281        ],
282        Some(Platform::Azure) => vec![
283            RuntimeEnvironmentEntry {
284                name: ENV_AZURE_SUBSCRIPTION_ID,
285                value: RuntimeEnvironmentValue::AzureSubscriptionId,
286            },
287            RuntimeEnvironmentEntry {
288                name: ENV_AZURE_TENANT_ID,
289                value: RuntimeEnvironmentValue::AzureTenantId,
290            },
291            RuntimeEnvironmentEntry {
292                name: ENV_AZURE_REGION,
293                value: RuntimeEnvironmentValue::AzureRegion,
294            },
295            RuntimeEnvironmentEntry {
296                name: ENV_AZURE_CLIENT_ID,
297                value: RuntimeEnvironmentValue::AzureClientId,
298            },
299        ],
300        _ => Vec::new(),
301    }
302}
303
304pub fn worker_transport_runtime_environment_plan(
305    platform: Platform,
306) -> Vec<RuntimeEnvironmentEntry> {
307    match platform {
308        Platform::Aws => vec![
309            RuntimeEnvironmentEntry {
310                name: ENV_ALIEN_TRANSPORT,
311                value: RuntimeEnvironmentValue::Literal("lambda"),
312            },
313            RuntimeEnvironmentEntry {
314                name: ENV_ALIEN_LAMBDA_MODE,
315                value: RuntimeEnvironmentValue::Literal("buffered"),
316            },
317        ],
318        Platform::Gcp => vec![RuntimeEnvironmentEntry {
319            name: ENV_ALIEN_TRANSPORT,
320            value: RuntimeEnvironmentValue::Literal("cloud-run"),
321        }],
322        Platform::Azure => vec![RuntimeEnvironmentEntry {
323            name: ENV_ALIEN_TRANSPORT,
324            value: RuntimeEnvironmentValue::Literal("container-app"),
325        }],
326        Platform::Kubernetes | Platform::Machines => vec![RuntimeEnvironmentEntry {
327            name: ENV_ALIEN_TRANSPORT,
328            value: RuntimeEnvironmentValue::Literal("http"),
329        }],
330        Platform::Local | Platform::Test => vec![RuntimeEnvironmentEntry {
331            name: ENV_ALIEN_TRANSPORT,
332            // Local/Test Workers run under the runtime's `local` transport (the
333            // in-process HTTP invocation proxy the worker manager selects via
334            // `TransportType::Local`). The env-plan value now matches that reality,
335            // so `ALIEN_TRANSPORT` for Workers is exactly the transport set
336            // `lambda | cloud-run | container-app | http | local`.
337            value: RuntimeEnvironmentValue::Literal("local"),
338        }],
339    }
340}
341
342pub fn worker_runtime_environment_plan(platform: Platform) -> Vec<RuntimeEnvironmentEntry> {
343    let mut entries = standard_runtime_environment_plan(platform);
344    entries.extend(worker_transport_runtime_environment_plan(platform));
345    entries.push(RuntimeEnvironmentEntry {
346        name: ENV_ALIEN_RUNTIME_SEND_OTLP,
347        value: RuntimeEnvironmentValue::Literal("true"),
348    });
349    entries.push(RuntimeEnvironmentEntry {
350        name: ENV_ALIEN_CURRENT_WORKER_BINDING_NAME,
351        value: RuntimeEnvironmentValue::CurrentWorkerBindingName,
352    });
353    if platform == Platform::Azure {
354        entries.push(RuntimeEnvironmentEntry {
355            name: ENV_AZURE_CLIENT_ID,
356            value: RuntimeEnvironmentValue::AzureClientId,
357        });
358    }
359    entries
360}
361
362pub fn worker_runtime_environment_contract(
363    platform: Platform,
364    worker_id: &str,
365    links: &[ResourceRef],
366) -> RuntimeEnvironmentPlan {
367    RuntimeEnvironmentPlan::new()
368        .add_scalar_entries(worker_runtime_environment_plan(platform))
369        .add_linked_bindings(links)
370        .add_current_worker_binding(worker_id)
371}
372
373pub fn container_runtime_environment_plan(platform: Platform) -> Vec<RuntimeEnvironmentEntry> {
374    let mut entries = standard_runtime_environment_plan(platform);
375    // `ALIEN_TRANSPORT` is Worker-only. Command-enabled Containers run the pull
376    // receiver configured per resource through the `ALIEN_COMMANDS_*` contract.
377    entries.push(RuntimeEnvironmentEntry {
378        name: ENV_ALIEN_CURRENT_CONTAINER_BINDING_NAME,
379        value: RuntimeEnvironmentValue::CurrentContainerBindingName,
380    });
381    entries
382}
383
384pub fn container_runtime_environment_contract(
385    platform: Platform,
386    container_id: &str,
387    links: &[ResourceRef],
388) -> RuntimeEnvironmentPlan {
389    RuntimeEnvironmentPlan::new()
390        .add_scalar_entries(container_runtime_environment_plan(platform))
391        .add_linked_bindings(links)
392        .add_current_container_binding(container_id)
393}
394
395pub fn daemon_runtime_environment_plan(platform: Platform) -> Vec<RuntimeEnvironmentEntry> {
396    // Daemons run under direct supervision and receive only the standard
397    // platform-identity vars. `ALIEN_TRANSPORT` and the container self-binding
398    // var are not part of the Daemon contract. Command-enabled Daemons get their
399    // `ALIEN_COMMANDS_*` receiver config from the resource controller.
400    standard_runtime_environment_plan(platform)
401}
402
403pub fn daemon_runtime_environment_contract(
404    platform: Platform,
405    links: &[ResourceRef],
406) -> RuntimeEnvironmentPlan {
407    RuntimeEnvironmentPlan::new()
408        .add_scalar_entries(daemon_runtime_environment_plan(platform))
409        .add_linked_bindings(links)
410}
411
412pub fn render_runtime_environment_entries<R>(
413    entries: impl IntoIterator<Item = RuntimeEnvironmentEntry>,
414    renderer: &R,
415) -> Result<Vec<(&'static str, R::Value)>>
416where
417    R: RuntimeEnvironmentRenderer,
418{
419    let mut rendered = Vec::new();
420    for entry in entries {
421        if let Some(value) = renderer.render_runtime_environment_value(entry.value)? {
422            rendered.push((entry.name, value));
423        }
424    }
425    Ok(rendered)
426}
427
428pub fn render_runtime_environment_plan<R>(
429    plan: &RuntimeEnvironmentPlan,
430    renderer: &R,
431) -> Result<Vec<(String, R::Value)>>
432where
433    R: RuntimeEnvironmentRenderer,
434{
435    let mut rendered = Vec::new();
436    for entry in plan.entries() {
437        match entry {
438            RuntimeEnvironmentPlanEntry::Scalar(entry) => {
439                if let Some(value) = renderer.render_runtime_environment_value(entry.value)? {
440                    rendered.push((entry.name.to_string(), value));
441                }
442            }
443            RuntimeEnvironmentPlanEntry::Binding(entry) => {
444                if let Some(value) = renderer.render_runtime_environment_binding(entry)? {
445                    rendered.push((entry.env_name.clone(), value));
446                }
447            }
448        }
449    }
450    Ok(rendered)
451}
452
453pub fn is_runtime_environment_contract_name(name: &str) -> bool {
454    matches!(
455        name,
456        ENV_ALIEN_CURRENT_CONTAINER_BINDING_NAME
457            | ENV_ALIEN_CURRENT_WORKER_BINDING_NAME
458            | ENV_OPERATOR_BASE_PLATFORM
459            | ENV_ALIEN_DEPLOYMENT_TYPE
460            | ENV_ALIEN_LAMBDA_MODE
461            | ENV_ALIEN_RUNTIME_SEND_OTLP
462            | ENV_ALIEN_TRANSPORT
463            | ENV_AWS_ACCOUNT_ID
464            | ENV_AWS_REGION
465            | ENV_AZURE_CLIENT_ID
466            | ENV_AZURE_REGION
467            | ENV_AZURE_SUBSCRIPTION_ID
468            | ENV_AZURE_TENANT_ID
469            | ENV_GCP_PROJECT_ID
470            | ENV_GCP_REGION
471            | ENV_GOOGLE_CLOUD_PROJECT
472    ) || (name.starts_with("ALIEN_") && name.ends_with("_BINDING"))
473}
474
475pub fn is_reserved_runtime_environment_name(name: &str) -> bool {
476    is_runtime_environment_contract_name(name)
477        || matches!(
478            name,
479            ENV_ALIEN_WORKER_GRPC_ADDRESS
480                | ENV_ALIEN_WORKER_TIMEOUT_SECONDS
481                | ENV_ALIEN_COMMANDS_TOKEN
482                | ENV_ALIEN_COMMANDS_TOKEN_FILE
483                | ENV_ALIEN_COMMANDS_LEASE_SECONDS
484                | ENV_ALIEN_COMMANDS_MAX_LEASES
485                | ENV_ALIEN_COMMANDS_POLL_INTERVAL_MS
486                | ENV_ALIEN_COMMANDS_POLL_MAX_INTERVAL_MS
487                | ENV_ALIEN_COMMANDS_POLL_JITTER
488                | ENV_ALIEN_COMMANDS_DRAIN_TIMEOUT_MS
489                | ENV_ALIEN_COMMANDS_TARGET_RESOURCE_ID
490                | ENV_ALIEN_COMMANDS_TARGET_RESOURCE_TYPE
491                | ENV_ALIEN_COMMANDS_URL
492                | ENV_ALIEN_DEPLOYMENT_ID
493                | ENV_ALIEN_DEPLOYMENT_NAME
494                | ENV_ALIEN_DEPLOYMENT_SERVICE_ACCOUNT
495                | ENV_ALIEN_DEPLOYMENT_TOKEN
496                | ENV_ALIEN_MANAGER_URL
497                | ENV_ALIEN_RESOURCE_ID
498                | ENV_ALIEN_PUBLIC_ENDPOINTS_JSON
499                | ENV_ALIEN_RUNTIME_SECRETS
500                | ENV_ALIEN_SECRET_ENV_REVISION
501                | ENV_ALIEN_SECRETS
502        )
503        || name.starts_with("ALIEN_BINDING_")
504}
505
506pub fn validate_runtime_environment_user_vars<'a>(
507    names: impl IntoIterator<Item = &'a str>,
508) -> Result<()> {
509    let reserved: Vec<String> = names
510        .into_iter()
511        .filter(|name| is_reserved_runtime_environment_name(name))
512        .map(ToString::to_string)
513        .collect();
514    if reserved.is_empty() {
515        return Ok(());
516    }
517
518    Err(AlienError::new(ErrorData::GenericError {
519        message: format!(
520            "Environment variables use reserved Alien runtime names: {}",
521            reserved.join(", ")
522        ),
523    }))
524}
525
526pub fn validate_runtime_environment_user_map(env: &HashMap<String, String>) -> Result<()> {
527    validate_runtime_environment_user_vars(env.keys().map(String::as_str))
528}
529
530pub fn validate_prepared_runtime_environment_vars<'a>(
531    names: impl IntoIterator<Item = &'a str>,
532) -> Result<()> {
533    let reserved: Vec<String> = names
534        .into_iter()
535        .filter(|name| is_runtime_environment_contract_name(name))
536        .map(ToString::to_string)
537        .collect();
538    if reserved.is_empty() {
539        return Ok(());
540    }
541
542    Err(AlienError::new(ErrorData::GenericError {
543        message: format!(
544            "Environment variables collide with Alien runtime contract names: {}",
545            reserved.join(", ")
546        ),
547    }))
548}
549
550pub fn validate_prepared_runtime_environment_map(env: &HashMap<String, String>) -> Result<()> {
551    validate_prepared_runtime_environment_vars(env.keys().map(String::as_str))
552}
553
554#[cfg(test)]
555mod tests {
556    use super::*;
557
558    #[test]
559    fn reserves_builtin_and_binding_environment_names() {
560        assert!(is_reserved_runtime_environment_name(ENV_ALIEN_TRANSPORT));
561        assert!(is_reserved_runtime_environment_name(
562            ENV_ALIEN_CURRENT_CONTAINER_BINDING_NAME
563        ));
564        assert!(is_reserved_runtime_environment_name(
565            ENV_OPERATOR_BASE_PLATFORM
566        ));
567        assert!(is_reserved_runtime_environment_name(ENV_ALIEN_SECRETS));
568        assert!(is_reserved_runtime_environment_name(
569            ENV_ALIEN_SECRET_ENV_REVISION
570        ));
571        assert!(is_reserved_runtime_environment_name(
572            ENV_ALIEN_WORKER_GRPC_ADDRESS
573        ));
574        assert_eq!(ENV_ALIEN_WORKER_GRPC_ADDRESS, "ALIEN_WORKER_GRPC_ADDRESS");
575        assert!(is_reserved_runtime_environment_name(
576            ENV_ALIEN_WORKER_TIMEOUT_SECONDS
577        ));
578        assert_eq!(
579            ENV_ALIEN_WORKER_TIMEOUT_SECONDS,
580            "ALIEN_WORKER_TIMEOUT_SECONDS"
581        );
582        assert!(is_reserved_runtime_environment_name(
583            "ALIEN_STORAGE_BINDING"
584        ));
585        assert!(is_reserved_runtime_environment_name(
586            "ALIEN_BINDING_STORAGE_URL"
587        ));
588        assert!(!is_reserved_runtime_environment_name("USER_DEFINED"));
589    }
590
591    #[test]
592    fn reserves_minting_credential_resolver_names() {
593        assert!(is_reserved_runtime_environment_name(ENV_ALIEN_MANAGER_URL));
594        assert!(is_reserved_runtime_environment_name(
595            ENV_ALIEN_DEPLOYMENT_TOKEN
596        ));
597        assert!(is_reserved_runtime_environment_name(
598            ENV_ALIEN_DEPLOYMENT_SERVICE_ACCOUNT
599        ));
600        assert!(is_reserved_runtime_environment_name(ENV_ALIEN_RESOURCE_ID));
601        assert_eq!(ENV_ALIEN_MANAGER_URL, "ALIEN_MANAGER_URL");
602        assert_eq!(ENV_ALIEN_DEPLOYMENT_TOKEN, "ALIEN_DEPLOYMENT_TOKEN");
603        assert_eq!(ENV_ALIEN_RESOURCE_ID, "ALIEN_RESOURCE_ID");
604        assert_eq!(
605            ENV_ALIEN_DEPLOYMENT_SERVICE_ACCOUNT,
606            "ALIEN_DEPLOYMENT_SERVICE_ACCOUNT"
607        );
608        // Must not match the `ALIEN_*_BINDING` resource-binding pattern, or the
609        // provider would try to parse its value as binding JSON.
610        assert!(!ENV_ALIEN_DEPLOYMENT_SERVICE_ACCOUNT.ends_with("_BINDING"));
611    }
612
613    #[test]
614    fn reserves_commands_target_resource_id() {
615        assert!(is_reserved_runtime_environment_name(
616            ENV_ALIEN_COMMANDS_TARGET_RESOURCE_ID
617        ));
618        assert_eq!(
619            ENV_ALIEN_COMMANDS_TARGET_RESOURCE_ID,
620            "ALIEN_COMMANDS_TARGET_RESOURCE_ID"
621        );
622    }
623
624    #[test]
625    fn reserves_command_receiver_names() {
626        for (constant, expected) in [
627            (ENV_ALIEN_COMMANDS_URL, "ALIEN_COMMANDS_URL"),
628            (ENV_ALIEN_COMMANDS_TOKEN_FILE, "ALIEN_COMMANDS_TOKEN_FILE"),
629            (
630                ENV_ALIEN_COMMANDS_LEASE_SECONDS,
631                "ALIEN_COMMANDS_LEASE_SECONDS",
632            ),
633            (ENV_ALIEN_COMMANDS_MAX_LEASES, "ALIEN_COMMANDS_MAX_LEASES"),
634            (
635                ENV_ALIEN_COMMANDS_POLL_INTERVAL_MS,
636                "ALIEN_COMMANDS_POLL_INTERVAL_MS",
637            ),
638            (
639                ENV_ALIEN_COMMANDS_POLL_MAX_INTERVAL_MS,
640                "ALIEN_COMMANDS_POLL_MAX_INTERVAL_MS",
641            ),
642            (ENV_ALIEN_COMMANDS_POLL_JITTER, "ALIEN_COMMANDS_POLL_JITTER"),
643            (
644                ENV_ALIEN_COMMANDS_DRAIN_TIMEOUT_MS,
645                "ALIEN_COMMANDS_DRAIN_TIMEOUT_MS",
646            ),
647            (
648                ENV_ALIEN_COMMANDS_TARGET_RESOURCE_TYPE,
649                "ALIEN_COMMANDS_TARGET_RESOURCE_TYPE",
650            ),
651        ] {
652            assert_eq!(constant, expected);
653            assert!(is_reserved_runtime_environment_name(constant));
654        }
655    }
656
657    #[test]
658    fn rejects_reserved_user_environment_names() {
659        let error = validate_runtime_environment_user_vars(["USER_DEFINED", ENV_ALIEN_TRANSPORT])
660            .unwrap_err();
661
662        assert!(error.to_string().contains(ENV_ALIEN_TRANSPORT));
663    }
664
665    #[test]
666    fn prepared_environment_allows_deployment_managed_names() {
667        validate_prepared_runtime_environment_vars([
668            ENV_ALIEN_SECRETS,
669            ENV_ALIEN_DEPLOYMENT_ID,
670            ENV_ALIEN_DEPLOYMENT_NAME,
671            ENV_ALIEN_PUBLIC_ENDPOINTS_JSON,
672        ])
673        .unwrap();
674
675        let error =
676            validate_prepared_runtime_environment_vars([ENV_ALIEN_SECRETS, ENV_ALIEN_TRANSPORT])
677                .unwrap_err();
678
679        assert!(error.to_string().contains(ENV_ALIEN_TRANSPORT));
680        assert!(!error.to_string().contains(ENV_ALIEN_SECRETS));
681    }
682
683    #[test]
684    fn kubernetes_standard_environment_declares_base_platform() {
685        let entries = standard_runtime_environment_plan(Platform::Kubernetes);
686
687        assert!(entries.iter().any(|entry| {
688            entry.name == ENV_OPERATOR_BASE_PLATFORM
689                && entry.value == RuntimeEnvironmentValue::BasePlatform
690        }));
691    }
692
693    #[test]
694    fn kubernetes_gcp_base_environment_declares_gcp_identity() {
695        let entries = kubernetes_base_platform_runtime_environment_plan(Some(Platform::Gcp));
696
697        assert!(entries.iter().any(|entry| {
698            entry.name == ENV_GOOGLE_CLOUD_PROJECT
699                && entry.value == RuntimeEnvironmentValue::GcpProjectId
700        }));
701        assert!(entries.iter().any(|entry| {
702            entry.name == ENV_GCP_PROJECT_ID && entry.value == RuntimeEnvironmentValue::GcpProjectId
703        }));
704        assert!(entries.iter().any(|entry| {
705            entry.name == ENV_GCP_REGION && entry.value == RuntimeEnvironmentValue::GcpRegion
706        }));
707    }
708
709    #[test]
710    fn kubernetes_worker_environment_uses_http_proxy_transport() {
711        let entries = worker_transport_runtime_environment_plan(Platform::Kubernetes);
712
713        assert!(entries.iter().any(|entry| {
714            entry.name == ENV_ALIEN_TRANSPORT
715                && entry.value == RuntimeEnvironmentValue::Literal("http")
716        }));
717    }
718
719    #[test]
720    fn container_environment_does_not_inject_worker_transport() {
721        for platform in [
722            Platform::Local,
723            Platform::Kubernetes,
724            Platform::Aws,
725            Platform::Gcp,
726            Platform::Azure,
727            Platform::Test,
728        ] {
729            let entries = container_runtime_environment_plan(platform);
730            assert!(
731                !entries
732                    .iter()
733                    .any(|entry| entry.name == ENV_ALIEN_TRANSPORT),
734                "container plan for {platform:?} must not set ALIEN_TRANSPORT"
735            );
736            // The container binding-name var is still present.
737            assert!(
738                entries
739                    .iter()
740                    .any(|entry| entry.name == ENV_ALIEN_CURRENT_CONTAINER_BINDING_NAME),
741                "container plan for {platform:?} must still declare the binding-name var"
742            );
743        }
744    }
745
746    #[test]
747    fn daemon_environment_is_standard_identity_only() {
748        // Daemons use the standard platform-identity set. Receiver config is
749        // injected per resource, not by the static environment plan.
750        for platform in [
751            Platform::Local,
752            Platform::Kubernetes,
753            Platform::Aws,
754            Platform::Gcp,
755            Platform::Azure,
756            Platform::Test,
757        ] {
758            let entries = daemon_runtime_environment_plan(platform);
759            assert!(
760                !entries
761                    .iter()
762                    .any(|entry| entry.name == ENV_ALIEN_TRANSPORT),
763                "daemon plan for {platform:?} must not set ALIEN_TRANSPORT"
764            );
765            assert!(
766                !entries
767                    .iter()
768                    .any(|entry| entry.name == ENV_ALIEN_CURRENT_CONTAINER_BINDING_NAME),
769                "daemon plan for {platform:?} must not set the container self-binding var"
770            );
771            // The standard deployment-type identity var is always present.
772            assert!(
773                entries
774                    .iter()
775                    .any(|entry| entry.name == ENV_ALIEN_DEPLOYMENT_TYPE),
776                "daemon plan for {platform:?} must declare ALIEN_DEPLOYMENT_TYPE"
777            );
778        }
779    }
780
781    #[test]
782    fn worker_local_transport_uses_local_proxy() {
783        // Local/Test Workers run under `TransportType::Local`, selected by the
784        // Worker manager.
785        for platform in [Platform::Local, Platform::Test] {
786            let entries = worker_transport_runtime_environment_plan(platform);
787            assert!(entries.iter().any(|entry| {
788                entry.name == ENV_ALIEN_TRANSPORT
789                    && entry.value == RuntimeEnvironmentValue::Literal("local")
790            }));
791        }
792    }
793}