Download codex-rs/exec-server/src/environment_toml.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 29.9 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/exec-server/src/environment_toml.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/exec-server/src/environment_toml.rs
-
curl -L -o environment_toml.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/exec-server/src/environment_toml.rs
29.9 kB
| use std::collections::HashMap; | |
| use std::collections::HashSet; | |
| use std::path::Path; | |
| use std::path::PathBuf; | |
| use std::time::Duration; | |
| use http::HeaderMap; | |
| use serde::Deserialize; | |
| use tokio_tungstenite::tungstenite::client::IntoClientRequest; | |
| use crate::DefaultEnvironmentProvider; | |
| use crate::EnvironmentProvider; | |
| use crate::EnvironmentProviderFuture; | |
| use crate::ExecServerError; | |
| use crate::client_api::DEFAULT_REMOTE_EXEC_SERVER_CONNECT_TIMEOUT; | |
| use crate::client_api::DEFAULT_REMOTE_EXEC_SERVER_INITIALIZE_TIMEOUT; | |
| use crate::client_api::ExecServerTransportParams; | |
| use crate::client_api::StdioExecServerCommand; | |
| use crate::environment::LOCAL_ENVIRONMENT_ID; | |
| use crate::environment_provider::EnvironmentDefault; | |
| use crate::environment_provider::EnvironmentProviderSnapshot; | |
| const ENVIRONMENTS_TOML_FILE: &str = "environments.toml"; | |
| const MAX_ENVIRONMENT_ID_LEN: usize = 64; | |
| struct EnvironmentsToml { | |
| default: Option<String>, | |
| include_local: Option<bool>, | |
| environments: Vec<EnvironmentToml>, | |
| } | |
| struct EnvironmentToml { | |
| id: String, | |
| url: Option<String>, | |
| program: Option<String>, | |
| args: Option<Vec<String>>, | |
| env: Option<HashMap<String, String>>, | |
| cwd: Option<PathBuf>, | |
| connect_timeout_sec: Option<Duration>, | |
| initialize_timeout_sec: Option<Duration>, | |
| } | |
| struct TomlEnvironmentProvider { | |
| default: EnvironmentDefault, | |
| include_local: bool, | |
| environments: Vec<(String, ExecServerTransportParams)>, | |
| } | |
| impl TomlEnvironmentProvider { | |
| fn new(config: EnvironmentsToml) -> Result<Self, ExecServerError> { | |
| Self::new_with_config_dir(config, /*config_dir*/ None) | |
| } | |
| fn new_with_config_dir( | |
| config: EnvironmentsToml, | |
| config_dir: Option<&Path>, | |
| ) -> Result<Self, ExecServerError> { | |
| let EnvironmentsToml { | |
| default, | |
| include_local, | |
| environments, | |
| } = config; | |
| let include_local = include_local.unwrap_or(true); | |
| let mut ids = HashSet::new(); | |
| if include_local { | |
| ids.insert(LOCAL_ENVIRONMENT_ID.to_string()); | |
| } | |
| let mut parsed_environments = Vec::with_capacity(environments.len()); | |
| for item in environments { | |
| let (id, transport) = parse_environment_toml(item, config_dir)?; | |
| if !ids.insert(id.clone()) { | |
| return Err(ExecServerError::Protocol(format!( | |
| "environment id `{id}` is duplicated" | |
| ))); | |
| } | |
| parsed_environments.push((id, transport)); | |
| } | |
| let default = normalize_default_environment_id(default.as_deref(), include_local, &ids)?; | |
| Ok(Self { | |
| default, | |
| include_local, | |
| environments: parsed_environments, | |
| }) | |
| } | |
| async fn snapshot(&self) -> Result<EnvironmentProviderSnapshot, ExecServerError> { | |
| Ok(EnvironmentProviderSnapshot { | |
| environments: self.environments.clone(), | |
| default: self.default.clone(), | |
| include_local: self.include_local, | |
| }) | |
| } | |
| } | |
| impl EnvironmentProvider for TomlEnvironmentProvider { | |
| fn snapshot(&self) -> EnvironmentProviderFuture<'_> { | |
| Box::pin(TomlEnvironmentProvider::snapshot(self)) | |
| } | |
| } | |
| fn parse_environment_toml( | |
| item: EnvironmentToml, | |
| config_dir: Option<&Path>, | |
| ) -> Result<(String, ExecServerTransportParams), ExecServerError> { | |
| let EnvironmentToml { | |
| id, | |
| url, | |
| program, | |
| args, | |
| env, | |
| cwd, | |
| connect_timeout_sec, | |
| initialize_timeout_sec, | |
| } = item; | |
| validate_environment_id(&id)?; | |
| if program.is_none() && (args.is_some() || env.is_some() || cwd.is_some()) { | |
| return Err(ExecServerError::Protocol(format!( | |
| "environment `{id}` args, env, and cwd require program" | |
| ))); | |
| } | |
| if url.is_none() && connect_timeout_sec.is_some() { | |
| return Err(ExecServerError::Protocol(format!( | |
| "environment `{id}` connect_timeout_sec requires url" | |
| ))); | |
| } | |
| let connect_timeout = connect_timeout_sec.unwrap_or(DEFAULT_REMOTE_EXEC_SERVER_CONNECT_TIMEOUT); | |
| let initialize_timeout = | |
| initialize_timeout_sec.unwrap_or(DEFAULT_REMOTE_EXEC_SERVER_INITIALIZE_TIMEOUT); | |
| let transport_params = match (url, program) { | |
| (Some(url), None) => { | |
| let url = validate_websocket_url(url)?; | |
| ExecServerTransportParams::WebSocketUrl { | |
| websocket_url: url, | |
| connect_timeout, | |
| initialize_timeout, | |
| http_headers: HeaderMap::new(), | |
| } | |
| } | |
| (None, Some(program)) => { | |
| let program = program.trim().to_string(); | |
| if program.is_empty() { | |
| return Err(ExecServerError::Protocol(format!( | |
| "environment `{id}` program cannot be empty" | |
| ))); | |
| } | |
| let cwd = normalize_stdio_cwd(&id, cwd, config_dir)?; | |
| ExecServerTransportParams::StdioCommand { | |
| command: StdioExecServerCommand { | |
| program, | |
| args: args.unwrap_or_default(), | |
| env: env.unwrap_or_default(), | |
| cwd, | |
| }, | |
| initialize_timeout, | |
| } | |
| } | |
| (None, None) | (Some(_), Some(_)) => { | |
| return Err(ExecServerError::Protocol(format!( | |
| "environment `{id}` must set exactly one of url or program" | |
| ))); | |
| } | |
| }; | |
| Ok((id, transport_params)) | |
| } | |
| fn normalize_stdio_cwd( | |
| id: &str, | |
| cwd: Option<PathBuf>, | |
| config_dir: Option<&Path>, | |
| ) -> Result<Option<PathBuf>, ExecServerError> { | |
| let Some(cwd) = cwd else { | |
| return Ok(None); | |
| }; | |
| if cwd.is_absolute() { | |
| return Ok(Some(cwd)); | |
| } | |
| let Some(config_dir) = config_dir else { | |
| return Err(ExecServerError::Protocol(format!( | |
| "environment `{id}` cwd must be absolute" | |
| ))); | |
| }; | |
| Ok(Some(config_dir.join(cwd))) | |
| } | |
| pub(crate) fn environment_provider_from_codex_home( | |
| codex_home: &Path, | |
| ) -> Result<Box<dyn EnvironmentProvider>, ExecServerError> { | |
| let path = codex_home.join(ENVIRONMENTS_TOML_FILE); | |
| let Some(environments) = load_environments_toml(&path)? else { | |
| return Ok(Box::new(DefaultEnvironmentProvider::from_env())); | |
| }; | |
| Ok(Box::new(TomlEnvironmentProvider::new_with_config_dir( | |
| environments, | |
| Some(codex_home), | |
| )?)) | |
| } | |
| fn normalize_default_environment_id( | |
| default: Option<&str>, | |
| include_local: bool, | |
| ids: &HashSet<String>, | |
| ) -> Result<EnvironmentDefault, ExecServerError> { | |
| let Some(default) = default.map(str::trim) else { | |
| return if include_local { | |
| Ok(EnvironmentDefault::EnvironmentId( | |
| LOCAL_ENVIRONMENT_ID.to_string(), | |
| )) | |
| } else { | |
| Ok(EnvironmentDefault::Disabled) | |
| }; | |
| }; | |
| if default.is_empty() { | |
| return Err(ExecServerError::Protocol( | |
| "default environment id cannot be empty".to_string(), | |
| )); | |
| } | |
| if !default.eq_ignore_ascii_case("none") && !ids.contains(default) { | |
| return Err(ExecServerError::Protocol(format!( | |
| "default environment `{default}` is not configured" | |
| ))); | |
| } | |
| if default.eq_ignore_ascii_case("none") { | |
| Ok(EnvironmentDefault::Disabled) | |
| } else { | |
| Ok(EnvironmentDefault::EnvironmentId(default.to_string())) | |
| } | |
| } | |
| fn validate_environment_id(id: &str) -> Result<(), ExecServerError> { | |
| let trimmed_id = id.trim(); | |
| if trimmed_id.is_empty() { | |
| return Err(ExecServerError::Protocol( | |
| "environment id cannot be empty".to_string(), | |
| )); | |
| } | |
| if trimmed_id != id { | |
| return Err(ExecServerError::Protocol(format!( | |
| "environment id `{id}` must not contain surrounding whitespace" | |
| ))); | |
| } | |
| if id == LOCAL_ENVIRONMENT_ID || id.eq_ignore_ascii_case("none") { | |
| return Err(ExecServerError::Protocol(format!( | |
| "environment id `{id}` is reserved" | |
| ))); | |
| } | |
| if id.len() > MAX_ENVIRONMENT_ID_LEN { | |
| return Err(ExecServerError::Protocol(format!( | |
| "environment id `{id}` cannot be longer than {MAX_ENVIRONMENT_ID_LEN} characters" | |
| ))); | |
| } | |
| if !id | |
| .chars() | |
| .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_') | |
| { | |
| return Err(ExecServerError::Protocol(format!( | |
| "environment id `{id}` must contain only ASCII letters, numbers, '-' or '_'" | |
| ))); | |
| } | |
| Ok(()) | |
| } | |
| fn validate_websocket_url(url: String) -> Result<String, ExecServerError> { | |
| let url = url.trim(); | |
| if url.is_empty() { | |
| return Err(ExecServerError::Protocol( | |
| "environment url cannot be empty".to_string(), | |
| )); | |
| } | |
| if !url.starts_with("ws://") && !url.starts_with("wss://") { | |
| return Err(ExecServerError::Protocol(format!( | |
| "environment url `{url}` must use ws:// or wss://" | |
| ))); | |
| } | |
| url.into_client_request().map_err(|err| { | |
| ExecServerError::Protocol(format!("environment url `{url}` is invalid: {err}")) | |
| })?; | |
| Ok(url.to_string()) | |
| } | |
| /// Returns `None` when the config is missing; other I/O and parse failures remain errors. | |
| fn load_environments_toml(path: &Path) -> Result<Option<EnvironmentsToml>, ExecServerError> { | |
| let contents = match std::fs::read_to_string(path) { | |
| Ok(contents) => contents, | |
| Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), | |
| Err(err) => { | |
| return Err(ExecServerError::Protocol(format!( | |
| "failed to read environment config `{}`: {err}", | |
| path.display() | |
| ))); | |
| } | |
| }; | |
| toml::from_str(&contents) | |
| .map_err(|err| { | |
| ExecServerError::Protocol(format!( | |
| "failed to parse environment config `{}`: {err}", | |
| path.display() | |
| )) | |
| }) | |
| .map(Some) | |
| } | |
| mod option_duration_secs { | |
| use std::time::Duration; | |
| use serde::Deserialize; | |
| use serde::Deserializer; | |
| pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error> | |
| where | |
| D: Deserializer<'de>, | |
| { | |
| let secs = Option::<f64>::deserialize(deserializer)?; | |
| secs.map(|secs| Duration::try_from_secs_f64(secs).map_err(serde::de::Error::custom)) | |
| .transpose() | |
| } | |
| } | |
| mod tests { | |
| use pretty_assertions::assert_eq; | |
| use tempfile::tempdir; | |
| use super::*; | |
| async fn toml_provider_includes_local_and_adds_configured_environments() { | |
| let provider = TomlEnvironmentProvider::new(EnvironmentsToml { | |
| default: Some("ssh-dev".to_string()), | |
| include_local: None, | |
| environments: vec![ | |
| EnvironmentToml { | |
| id: "devbox".to_string(), | |
| url: Some(" ws://127.0.0.1:8765 ".to_string()), | |
| ..Default::default() | |
| }, | |
| EnvironmentToml { | |
| id: "ssh-dev".to_string(), | |
| program: Some(" ssh ".to_string()), | |
| args: Some(vec![ | |
| "dev".to_string(), | |
| "codex exec-server --listen stdio".to_string(), | |
| ]), | |
| env: Some(HashMap::from([( | |
| "CODEX_LOG".to_string(), | |
| "debug".to_string(), | |
| )])), | |
| ..Default::default() | |
| }, | |
| ], | |
| }) | |
| .expect("provider"); | |
| let snapshot = provider.snapshot().await.expect("environments"); | |
| let EnvironmentProviderSnapshot { | |
| environments, | |
| default, | |
| include_local, | |
| } = snapshot; | |
| let environment_ids: Vec<_> = environments | |
| .iter() | |
| .map(|(id, _environment)| id.as_str()) | |
| .collect(); | |
| assert_eq!(environment_ids, vec!["devbox", "ssh-dev"]); | |
| let environments: HashMap<_, _> = environments.into_iter().collect(); | |
| assert!(include_local); | |
| assert!(!environments.contains_key(LOCAL_ENVIRONMENT_ID)); | |
| assert!(matches!( | |
| &environments["devbox"], | |
| ExecServerTransportParams::WebSocketUrl { .. } | |
| )); | |
| assert!(matches!( | |
| &environments["ssh-dev"], | |
| ExecServerTransportParams::StdioCommand { .. } | |
| )); | |
| assert_eq!( | |
| default, | |
| EnvironmentDefault::EnvironmentId("ssh-dev".to_string()) | |
| ); | |
| } | |
| async fn toml_provider_default_omitted_selects_local() { | |
| let provider = TomlEnvironmentProvider::new(EnvironmentsToml::default()).expect("provider"); | |
| let snapshot = provider.snapshot().await.expect("environments"); | |
| assert!(snapshot.include_local); | |
| assert_eq!( | |
| snapshot.default, | |
| EnvironmentDefault::EnvironmentId(LOCAL_ENVIRONMENT_ID.to_string()) | |
| ); | |
| } | |
| async fn toml_provider_default_none_disables_default() { | |
| let provider = TomlEnvironmentProvider::new(EnvironmentsToml { | |
| default: Some("none".to_string()), | |
| include_local: None, | |
| environments: Vec::new(), | |
| }) | |
| .expect("provider"); | |
| let snapshot = provider.snapshot().await.expect("environments"); | |
| assert!(snapshot.include_local); | |
| assert_eq!(snapshot.default, EnvironmentDefault::Disabled); | |
| } | |
| async fn toml_provider_can_disable_local_environment() { | |
| let provider = TomlEnvironmentProvider::new(EnvironmentsToml { | |
| default: Some("ssh-dev".to_string()), | |
| include_local: Some(false), | |
| environments: vec![EnvironmentToml { | |
| id: "ssh-dev".to_string(), | |
| program: Some("ssh".to_string()), | |
| ..Default::default() | |
| }], | |
| }) | |
| .expect("provider"); | |
| let snapshot = provider.snapshot().await.expect("environments"); | |
| assert!(!snapshot.include_local); | |
| assert_eq!( | |
| snapshot.default, | |
| EnvironmentDefault::EnvironmentId("ssh-dev".to_string()) | |
| ); | |
| } | |
| async fn toml_provider_without_local_and_default_omitted_disables_default() { | |
| let provider = TomlEnvironmentProvider::new(EnvironmentsToml { | |
| include_local: Some(false), | |
| ..Default::default() | |
| }) | |
| .expect("provider"); | |
| let snapshot = provider.snapshot().await.expect("environments"); | |
| assert!(!snapshot.include_local); | |
| assert_eq!(snapshot.default, EnvironmentDefault::Disabled); | |
| } | |
| fn toml_provider_rejects_local_default_when_local_is_disabled() { | |
| let err = TomlEnvironmentProvider::new(EnvironmentsToml { | |
| default: Some(LOCAL_ENVIRONMENT_ID.to_string()), | |
| include_local: Some(false), | |
| environments: Vec::new(), | |
| }) | |
| .expect_err("local default without local environment should fail"); | |
| assert_eq!( | |
| err.to_string(), | |
| "exec-server protocol error: default environment `local` is not configured" | |
| ); | |
| } | |
| fn toml_provider_rejects_invalid_environments() { | |
| let cases = [ | |
| ( | |
| EnvironmentToml { | |
| id: "local".to_string(), | |
| url: Some("ws://127.0.0.1:8765".to_string()), | |
| ..Default::default() | |
| }, | |
| "environment id `local` is reserved", | |
| ), | |
| ( | |
| EnvironmentToml { | |
| id: " devbox ".to_string(), | |
| url: Some("ws://127.0.0.1:8765".to_string()), | |
| ..Default::default() | |
| }, | |
| "environment id ` devbox ` must not contain surrounding whitespace", | |
| ), | |
| ( | |
| EnvironmentToml { | |
| id: "dev box".to_string(), | |
| url: Some("ws://127.0.0.1:8765".to_string()), | |
| ..Default::default() | |
| }, | |
| "environment id `dev box` must contain only ASCII letters, numbers, '-' or '_'", | |
| ), | |
| ( | |
| EnvironmentToml { | |
| id: "devbox".to_string(), | |
| url: Some("http://127.0.0.1:8765".to_string()), | |
| ..Default::default() | |
| }, | |
| "environment url `http://127.0.0.1:8765` must use ws:// or wss://", | |
| ), | |
| ( | |
| EnvironmentToml { | |
| id: "devbox".to_string(), | |
| url: Some("ws://127.0.0.1:8765".to_string()), | |
| program: Some("codex".to_string()), | |
| ..Default::default() | |
| }, | |
| "environment `devbox` must set exactly one of url or program", | |
| ), | |
| ( | |
| EnvironmentToml { | |
| id: "devbox".to_string(), | |
| program: Some(" ".to_string()), | |
| ..Default::default() | |
| }, | |
| "environment `devbox` program cannot be empty", | |
| ), | |
| ( | |
| EnvironmentToml { | |
| id: "devbox".to_string(), | |
| args: Some(Vec::new()), | |
| ..Default::default() | |
| }, | |
| "environment `devbox` args, env, and cwd require program", | |
| ), | |
| ( | |
| EnvironmentToml { | |
| id: "ssh-dev".to_string(), | |
| program: Some("ssh".to_string()), | |
| connect_timeout_sec: Some(Duration::from_secs(1)), | |
| ..Default::default() | |
| }, | |
| "environment `ssh-dev` connect_timeout_sec requires url", | |
| ), | |
| ]; | |
| for (item, expected) in cases { | |
| let err = TomlEnvironmentProvider::new(EnvironmentsToml { | |
| default: None, | |
| include_local: None, | |
| environments: vec![item], | |
| }) | |
| .expect_err("invalid item should fail"); | |
| assert_eq!( | |
| err.to_string(), | |
| format!("exec-server protocol error: {expected}") | |
| ); | |
| } | |
| } | |
| fn toml_provider_resolves_relative_stdio_cwd_from_config_dir() { | |
| let config_dir = tempdir().expect("tempdir"); | |
| let provider = TomlEnvironmentProvider::new_with_config_dir( | |
| EnvironmentsToml { | |
| default: None, | |
| include_local: None, | |
| environments: vec![EnvironmentToml { | |
| id: "ssh-dev".to_string(), | |
| program: Some("ssh".to_string()), | |
| cwd: Some(PathBuf::from("workspace")), | |
| ..Default::default() | |
| }], | |
| }, | |
| Some(config_dir.path()), | |
| ) | |
| .expect("provider"); | |
| let ExecServerTransportParams::StdioCommand { | |
| command, | |
| initialize_timeout, | |
| } = &provider.environments[0].1 | |
| else { | |
| panic!("expected stdio transport"); | |
| }; | |
| assert_eq!( | |
| command, | |
| &StdioExecServerCommand { | |
| program: "ssh".to_string(), | |
| args: Vec::new(), | |
| env: HashMap::new(), | |
| cwd: Some(config_dir.path().join("workspace")), | |
| } | |
| ); | |
| assert_eq!( | |
| *initialize_timeout, | |
| DEFAULT_REMOTE_EXEC_SERVER_INITIALIZE_TIMEOUT | |
| ); | |
| } | |
| fn toml_provider_parses_configured_transport_timeouts() { | |
| let provider = TomlEnvironmentProvider::new(EnvironmentsToml { | |
| default: None, | |
| include_local: None, | |
| environments: vec![ | |
| EnvironmentToml { | |
| id: "devbox".to_string(), | |
| url: Some("ws://127.0.0.1:8765".to_string()), | |
| connect_timeout_sec: Some(Duration::from_secs(12)), | |
| initialize_timeout_sec: Some(Duration::from_secs(34)), | |
| ..Default::default() | |
| }, | |
| EnvironmentToml { | |
| id: "ssh-dev".to_string(), | |
| program: Some("ssh".to_string()), | |
| initialize_timeout_sec: Some(Duration::from_secs(56)), | |
| ..Default::default() | |
| }, | |
| ], | |
| }) | |
| .expect("provider"); | |
| let ExecServerTransportParams::WebSocketUrl { | |
| websocket_url, | |
| connect_timeout, | |
| initialize_timeout, | |
| .. | |
| } = &provider.environments[0].1 | |
| else { | |
| panic!("expected websocket transport"); | |
| }; | |
| assert_eq!(websocket_url, "ws://127.0.0.1:8765"); | |
| assert_eq!(*connect_timeout, Duration::from_secs(12)); | |
| assert_eq!(*initialize_timeout, Duration::from_secs(34)); | |
| let ExecServerTransportParams::StdioCommand { | |
| command, | |
| initialize_timeout, | |
| } = &provider.environments[1].1 | |
| else { | |
| panic!("expected stdio transport"); | |
| }; | |
| assert_eq!( | |
| command, | |
| &StdioExecServerCommand { | |
| program: "ssh".to_string(), | |
| args: Vec::new(), | |
| env: HashMap::new(), | |
| cwd: None, | |
| } | |
| ); | |
| assert_eq!(*initialize_timeout, Duration::from_secs(56)); | |
| } | |
| fn toml_provider_rejects_relative_stdio_cwd_without_config_dir() { | |
| let err = TomlEnvironmentProvider::new(EnvironmentsToml { | |
| default: None, | |
| include_local: None, | |
| environments: vec![EnvironmentToml { | |
| id: "ssh-dev".to_string(), | |
| program: Some("ssh".to_string()), | |
| cwd: Some(PathBuf::from("workspace")), | |
| ..Default::default() | |
| }], | |
| }) | |
| .expect_err("relative cwd without config dir should fail"); | |
| assert_eq!( | |
| err.to_string(), | |
| "exec-server protocol error: environment `ssh-dev` cwd must be absolute" | |
| ); | |
| } | |
| fn toml_provider_rejects_duplicate_ids() { | |
| let err = TomlEnvironmentProvider::new(EnvironmentsToml { | |
| default: None, | |
| include_local: None, | |
| environments: vec![ | |
| EnvironmentToml { | |
| id: "devbox".to_string(), | |
| url: Some("ws://127.0.0.1:8765".to_string()), | |
| ..Default::default() | |
| }, | |
| EnvironmentToml { | |
| id: "devbox".to_string(), | |
| program: Some("codex".to_string()), | |
| ..Default::default() | |
| }, | |
| ], | |
| }) | |
| .expect_err("duplicate id should fail"); | |
| assert_eq!( | |
| err.to_string(), | |
| "exec-server protocol error: environment id `devbox` is duplicated" | |
| ); | |
| } | |
| fn toml_provider_rejects_overlong_id() { | |
| let id = "a".repeat(MAX_ENVIRONMENT_ID_LEN + 1); | |
| let err = TomlEnvironmentProvider::new(EnvironmentsToml { | |
| default: None, | |
| include_local: None, | |
| environments: vec![EnvironmentToml { | |
| id: id.clone(), | |
| url: Some("ws://127.0.0.1:8765".to_string()), | |
| ..Default::default() | |
| }], | |
| }) | |
| .expect_err("overlong id should fail"); | |
| assert_eq!( | |
| err.to_string(), | |
| format!( | |
| "exec-server protocol error: environment id `{id}` cannot be longer than {MAX_ENVIRONMENT_ID_LEN} characters" | |
| ) | |
| ); | |
| } | |
| fn toml_provider_rejects_unknown_default() { | |
| let err = TomlEnvironmentProvider::new(EnvironmentsToml { | |
| default: Some("missing".to_string()), | |
| include_local: None, | |
| environments: Vec::new(), | |
| }) | |
| .expect_err("unknown default should fail"); | |
| assert_eq!( | |
| err.to_string(), | |
| "exec-server protocol error: default environment `missing` is not configured" | |
| ); | |
| } | |
| fn load_environments_toml_reads_root_environment_list() { | |
| let codex_home = tempdir().expect("tempdir"); | |
| let path = codex_home.path().join(ENVIRONMENTS_TOML_FILE); | |
| std::fs::write( | |
| &path, | |
| r#" | |
| default = "ssh-dev" | |
| include_local = false | |
| [[environments]] | |
| id = "devbox" | |
| url = "ws://127.0.0.1:4512" | |
| connect_timeout_sec = 12.0 | |
| initialize_timeout_sec = 34.0 | |
| [[environments]] | |
| id = "ssh-dev" | |
| program = "ssh" | |
| args = ["dev", "codex exec-server --listen stdio"] | |
| cwd = "/tmp" | |
| [environments.env] | |
| CODEX_LOG = "debug" | |
| "#, | |
| ) | |
| .expect("write environments.toml"); | |
| let environments = load_environments_toml(&path) | |
| .expect("environments.toml") | |
| .expect("environments.toml should exist"); | |
| assert_eq!(environments.default.as_deref(), Some("ssh-dev")); | |
| assert_eq!(environments.include_local, Some(false)); | |
| assert_eq!(environments.environments.len(), 2); | |
| assert_eq!( | |
| environments.environments[0], | |
| EnvironmentToml { | |
| id: "devbox".to_string(), | |
| url: Some("ws://127.0.0.1:4512".to_string()), | |
| connect_timeout_sec: Some(Duration::from_secs(12)), | |
| initialize_timeout_sec: Some(Duration::from_secs(34)), | |
| ..Default::default() | |
| } | |
| ); | |
| assert_eq!( | |
| environments.environments[1], | |
| EnvironmentToml { | |
| id: "ssh-dev".to_string(), | |
| program: Some("ssh".to_string()), | |
| args: Some(vec![ | |
| "dev".to_string(), | |
| "codex exec-server --listen stdio".to_string(), | |
| ]), | |
| env: Some(HashMap::from([( | |
| "CODEX_LOG".to_string(), | |
| "debug".to_string(), | |
| )])), | |
| cwd: Some(PathBuf::from("/tmp")), | |
| ..Default::default() | |
| } | |
| ); | |
| } | |
| fn load_environments_toml_rejects_unknown_fields() { | |
| let codex_home = tempdir().expect("tempdir"); | |
| let cases = [ | |
| ("unknown = true\n", "unknown field `unknown`"), | |
| ( | |
| r#" | |
| [[environments]] | |
| id = "devbox" | |
| url = "ws://127.0.0.1:4512" | |
| unknown = true | |
| "#, | |
| "unknown field `unknown`", | |
| ), | |
| ]; | |
| for (index, (contents, expected)) in cases.into_iter().enumerate() { | |
| let path = codex_home.path().join(format!("environments-{index}.toml")); | |
| std::fs::write(&path, contents).expect("write environments.toml"); | |
| let err = load_environments_toml(&path).expect_err("unknown field should fail"); | |
| assert!( | |
| err.to_string().contains(expected), | |
| "expected `{err}` to contain `{expected}`" | |
| ); | |
| } | |
| } | |
| fn toml_provider_rejects_malformed_websocket_url() { | |
| let err = TomlEnvironmentProvider::new(EnvironmentsToml { | |
| default: None, | |
| include_local: None, | |
| environments: vec![EnvironmentToml { | |
| id: "devbox".to_string(), | |
| url: Some("ws://".to_string()), | |
| ..Default::default() | |
| }], | |
| }) | |
| .expect_err("malformed websocket url should fail"); | |
| assert!( | |
| err.to_string() | |
| .contains("environment url `ws://` is invalid"), | |
| "expected malformed URL error, got `{err}`" | |
| ); | |
| } | |
| async fn environment_provider_from_codex_home_uses_present_environments_file() { | |
| let codex_home = tempdir().expect("tempdir"); | |
| std::fs::write( | |
| codex_home.path().join(ENVIRONMENTS_TOML_FILE), | |
| r#" | |
| default = "none" | |
| include_local = false | |
| "#, | |
| ) | |
| .expect("write environments.toml"); | |
| let provider = | |
| environment_provider_from_codex_home(codex_home.path()).expect("environment provider"); | |
| let snapshot = provider.snapshot().await.expect("environments"); | |
| let environment_ids: Vec<_> = snapshot | |
| .environments | |
| .into_iter() | |
| .map(|(id, _environment)| id) | |
| .collect(); | |
| assert!(!snapshot.include_local); | |
| assert!(!environment_ids.contains(&LOCAL_ENVIRONMENT_ID.to_string())); | |
| assert_eq!(snapshot.default, EnvironmentDefault::Disabled); | |
| } | |
| async fn environment_provider_from_codex_home_falls_back_when_file_is_missing() { | |
| let codex_home = tempdir().expect("tempdir"); | |
| let provider = | |
| environment_provider_from_codex_home(codex_home.path()).expect("environment provider"); | |
| let snapshot = provider.snapshot().await.expect("environments"); | |
| let environment_ids: Vec<_> = snapshot | |
| .environments | |
| .into_iter() | |
| .map(|(id, _environment)| id) | |
| .collect(); | |
| assert!(snapshot.include_local); | |
| assert!(!environment_ids.contains(&LOCAL_ENVIRONMENT_ID.to_string())); | |
| assert_eq!( | |
| snapshot.default, | |
| EnvironmentDefault::EnvironmentId(LOCAL_ENVIRONMENT_ID.to_string()) | |
| ); | |
| } | |
| } | |