codex_exec_server/
environment_provider.rs1use 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
10pub trait EnvironmentProvider: Send + Sync {
18 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#[derive(Clone, Debug)]
40pub struct DefaultEnvironmentProvider {
41 exec_server_url: Option<String>,
42}
43
44impl DefaultEnvironmentProvider {
45 pub fn new(exec_server_url: Option<String>) -> Self {
47 Self { exec_server_url }
48 }
49
50 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, 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(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}