Skip to main content

codex_exec_server/
environment_provider.rs

1use std::future::Future;
2use std::pin::Pin;
3
4use crate::Environment;
5use crate::ExecServerError;
6use crate::environment::CODEX_EXEC_SERVER_URL_ENV_VAR;
7use crate::environment::LOCAL_ENVIRONMENT_ID;
8use crate::environment::REMOTE_ENVIRONMENT_ID;
9
10/// Lists the concrete environments available to Codex.
11///
12/// Implementations own a startup snapshot containing both the available
13/// environment list in configured order and the default environment
14/// selection. Providers should only return provider-owned remote environments;
15/// `include_local` controls whether `EnvironmentManager` should add the local
16/// environment to the snapshot.
17pub trait EnvironmentProvider: Send + Sync {
18    /// Returns the provider-owned environment startup snapshot.
19    fn snapshot(&self) -> EnvironmentProviderFuture<'_>;
20}
21
22pub type EnvironmentProviderFuture<'a> =
23    Pin<Box<dyn Future<Output = Result<EnvironmentProviderSnapshot, ExecServerError>> + Send + 'a>>;
24
25#[derive(Clone, Debug)]
26pub struct EnvironmentProviderSnapshot {
27    pub environments: Vec<(String, Environment)>,
28    pub default: EnvironmentDefault,
29    pub include_local: bool,
30}
31
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub enum EnvironmentDefault {
34    Disabled,
35    EnvironmentId(String),
36}
37
38/// Default provider backed by `CODEX_EXEC_SERVER_URL`.
39#[derive(Clone, Debug)]
40pub struct DefaultEnvironmentProvider {
41    exec_server_url: Option<String>,
42}
43
44impl DefaultEnvironmentProvider {
45    /// Builds a provider from an already-read raw `CODEX_EXEC_SERVER_URL` value.
46    pub fn new(exec_server_url: Option<String>) -> Self {
47        Self { exec_server_url }
48    }
49
50    /// Builds a provider by reading `CODEX_EXEC_SERVER_URL`.
51    pub fn from_env() -> Self {
52        Self::new(std::env::var(CODEX_EXEC_SERVER_URL_ENV_VAR).ok())
53    }
54
55    pub(crate) fn snapshot_inner(&self) -> EnvironmentProviderSnapshot {
56        let mut environments = Vec::new();
57        let (exec_server_url, disabled) = normalize_exec_server_url(self.exec_server_url.clone());
58
59        if let Some(exec_server_url) = exec_server_url {
60            environments.push((
61                REMOTE_ENVIRONMENT_ID.to_string(),
62                Environment::remote_inner(exec_server_url, /*local_runtime_paths*/ None),
63            ));
64        }
65
66        let has_remote = environments
67            .iter()
68            .any(|(id, _environment)| id == REMOTE_ENVIRONMENT_ID);
69        let include_local = !disabled && !has_remote;
70        let default = if disabled {
71            EnvironmentDefault::Disabled
72        } else if has_remote {
73            EnvironmentDefault::EnvironmentId(REMOTE_ENVIRONMENT_ID.to_string())
74        } else {
75            EnvironmentDefault::EnvironmentId(LOCAL_ENVIRONMENT_ID.to_string())
76        };
77
78        EnvironmentProviderSnapshot {
79            environments,
80            default,
81            include_local,
82        }
83    }
84}
85
86impl EnvironmentProvider for DefaultEnvironmentProvider {
87    fn snapshot(&self) -> EnvironmentProviderFuture<'_> {
88        Box::pin(async { Ok(self.snapshot_inner()) })
89    }
90}
91
92pub(crate) fn normalize_exec_server_url(exec_server_url: Option<String>) -> (Option<String>, bool) {
93    match exec_server_url.as_deref().map(str::trim) {
94        None | Some("") => (None, false),
95        Some(url) if url.eq_ignore_ascii_case("none") => (None, true),
96        Some(url) => (Some(url.to_string()), false),
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use std::collections::HashMap;
103
104    use pretty_assertions::assert_eq;
105
106    use super::*;
107
108    #[tokio::test]
109    async fn default_provider_requests_local_environment_when_url_is_missing() {
110        let provider = DefaultEnvironmentProvider::new(/*exec_server_url*/ None);
111        let snapshot = provider.snapshot().await.expect("environments");
112        let EnvironmentProviderSnapshot {
113            environments,
114            default,
115            include_local,
116        } = snapshot;
117        let environments: HashMap<_, _> = environments.into_iter().collect();
118
119        assert!(include_local);
120        assert!(!environments.contains_key(LOCAL_ENVIRONMENT_ID));
121        assert!(!environments.contains_key(REMOTE_ENVIRONMENT_ID));
122        assert_eq!(
123            default,
124            EnvironmentDefault::EnvironmentId(LOCAL_ENVIRONMENT_ID.to_string())
125        );
126    }
127
128    #[tokio::test]
129    async fn default_provider_requests_local_environment_when_url_is_empty() {
130        let provider = DefaultEnvironmentProvider::new(Some(String::new()));
131        let snapshot = provider.snapshot().await.expect("environments");
132        let EnvironmentProviderSnapshot {
133            environments,
134            default,
135            include_local,
136        } = snapshot;
137        let environments: HashMap<_, _> = environments.into_iter().collect();
138
139        assert!(include_local);
140        assert!(!environments.contains_key(LOCAL_ENVIRONMENT_ID));
141        assert!(!environments.contains_key(REMOTE_ENVIRONMENT_ID));
142        assert_eq!(
143            default,
144            EnvironmentDefault::EnvironmentId(LOCAL_ENVIRONMENT_ID.to_string())
145        );
146    }
147
148    #[tokio::test]
149    async fn default_provider_omits_local_environment_for_none_value() {
150        let provider = DefaultEnvironmentProvider::new(Some("none".to_string()));
151        let snapshot = provider.snapshot().await.expect("environments");
152        let EnvironmentProviderSnapshot {
153            environments,
154            default,
155            include_local,
156        } = snapshot;
157        let environments: HashMap<_, _> = environments.into_iter().collect();
158
159        assert!(!include_local);
160        assert!(!environments.contains_key(LOCAL_ENVIRONMENT_ID));
161        assert!(!environments.contains_key(REMOTE_ENVIRONMENT_ID));
162        assert_eq!(default, EnvironmentDefault::Disabled);
163    }
164
165    #[tokio::test]
166    async fn default_provider_adds_remote_environment_for_websocket_url() {
167        let provider = DefaultEnvironmentProvider::new(Some("ws://127.0.0.1:8765".to_string()));
168        let snapshot = provider.snapshot().await.expect("environments");
169        let EnvironmentProviderSnapshot {
170            environments,
171            default,
172            include_local,
173        } = snapshot;
174        let environments: HashMap<_, _> = environments.into_iter().collect();
175
176        assert!(!include_local);
177        assert!(!environments.contains_key(LOCAL_ENVIRONMENT_ID));
178        let remote_environment = &environments[REMOTE_ENVIRONMENT_ID];
179        assert!(remote_environment.is_remote());
180        assert_eq!(
181            default,
182            EnvironmentDefault::EnvironmentId(REMOTE_ENVIRONMENT_ID.to_string())
183        );
184    }
185
186    #[test]
187    fn normalizes_exec_server_url() {
188        assert_eq!(
189            normalize_exec_server_url(Some(" ws://127.0.0.1:8765 ".to_string())),
190            (Some("ws://127.0.0.1:8765".to_string()), false)
191        );
192    }
193}