Load configured environments from CODEX_HOME (#20667)

## Why

The earlier PRs add stdio transport support and the config-backed
environment provider, but the feature remains inert until normal Codex
entrypoints construct `EnvironmentManager` with enough context to
discover `CODEX_HOME/environments.toml`. This final stack PR activates
the provider while preserving the legacy `CODEX_EXEC_SERVER_URL`
fallback when no environments file exists.

**Stack position:** this is PR 5 of 5. It is the product wiring PR that
activates the configured environment provider added in PR 4.

## What Changed

- Thread `codex_home` into `EnvironmentManagerArgs`.
- Change `EnvironmentManager::new(...)` to load the provider from
`CODEX_HOME`.
- Preserve legacy behavior by falling back to
`DefaultEnvironmentProvider::from_env()` when `environments.toml` is
absent.
- Make `environments.toml`-backed managers start new threads with all
configured environments, default first, while keeping the legacy env-var
path single-default.
- Update the app-server, TUI, exec, MCP server, connector, prompt-debug,
and thread-manager-sample callsites to pass `codex_home` and handle
provider-loading errors.

## Self-Review Notes

- The multi-environment startup path is intentionally tied to the
`environments.toml` provider. Using `>1` configured environment as the
only signal would also expand the legacy `CODEX_EXEC_SERVER_URL`
provider because it keeps `local` addressable alongside `remote`.
- The startup environment list is still derived inside
`EnvironmentManager`; the provider only says whether its snapshot should
start new threads with all configured environments.
- The thread-manager sample was updated to pass the current
`ThreadManager::new(...)` installation id argument so the stack compiles
under Bazel.

## Stack

- 1. https://github.com/openai/codex/pull/20663 - Add stdio exec-server
listener
- 2. https://github.com/openai/codex/pull/20664 - Add stdio exec-server
client transport
- 3. https://github.com/openai/codex/pull/20665 - Make environment
providers own default selection
- 4. https://github.com/openai/codex/pull/20666 - Add CODEX_HOME
environments TOML provider
- **5. This PR:** https://github.com/openai/codex/pull/20667 - Load
configured environments from CODEX_HOME

Split from original draft: https://github.com/openai/codex/pull/20508

## Validation

- `just fmt`
- `git diff --check`
- `bazel build --config=remote --strategy=remote
--remote_download_toplevel
//codex-rs/thread-manager-sample:codex-thread-manager-sample`
- `bazel test --config=remote --strategy=remote
--remote_download_toplevel
//codex-rs/exec-server:exec-server-unit-tests`
- `bazel test --config=remote --strategy=remote
--remote_download_toplevel --test_sharding_strategy=disabled
--test_arg=default_thread_environment_selections_use_manager_default_id
//codex-rs/core:core-unit-tests`
- `bazel test --config=remote --strategy=remote
--remote_download_toplevel --test_sharding_strategy=disabled
--test_arg=start_thread_uses_all_default_environments_from_codex_home
//codex-rs/core:core-unit-tests`

## Documentation

This activates `CODEX_HOME/environments.toml`; user-facing documentation
should be added before this stack is treated as a documented public
workflow.

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
starr-openai
2026-05-08 11:17:56 -07:00
committed by GitHub
co-authored by Codex
parent 872b8b15b3
commit 5f2543b74e
22 changed files with 311 additions and 118 deletions
-1
View File
@@ -49,7 +49,6 @@ use codex_config::RemoteThreadConfigLoader;
use codex_config::ThreadConfigLoader;
use codex_core::config::Config;
pub use codex_exec_server::EnvironmentManager;
pub use codex_exec_server::EnvironmentManagerArgs;
pub use codex_exec_server::ExecServerRuntimePaths;
use codex_feedback::CodexFeedback;
use codex_protocol::protocol::SessionSource;
+11 -10
View File
@@ -8,7 +8,6 @@ use codex_config::RemoteThreadConfigLoader;
use codex_config::ThreadConfigLoader;
use codex_core::config::Config;
use codex_core::resolve_installation_id;
use codex_exec_server::EnvironmentManagerArgs;
use codex_features::Feature;
use codex_login::AuthManager;
use codex_utils_cli::CliConfigOverrides;
@@ -420,15 +419,6 @@ pub async fn run_main_with_transport_options(
auth: AppServerWebsocketAuthSettings,
runtime_options: AppServerRuntimeOptions,
) -> IoResult<()> {
let environment_manager = Arc::new(
EnvironmentManager::new(EnvironmentManagerArgs::new(
ExecServerRuntimePaths::from_optional_paths(
arg0_paths.codex_self_exe.clone(),
arg0_paths.codex_linux_sandbox_exe.clone(),
)?,
))
.await,
);
let (transport_event_tx, mut transport_event_rx) =
mpsc::channel::<TransportEvent>(CHANNEL_CAPACITY);
let (outgoing_tx, mut outgoing_rx) = mpsc::channel::<OutgoingEnvelope>(CHANNEL_CAPACITY);
@@ -444,6 +434,17 @@ pub async fn run_main_with_transport_options(
)
})?;
let codex_home = find_codex_home()?;
let local_runtime_paths = ExecServerRuntimePaths::from_optional_paths(
arg0_paths.codex_self_exe.clone(),
arg0_paths.codex_linux_sandbox_exe.clone(),
)?;
let environment_manager = if loader_overrides.ignore_user_config {
EnvironmentManager::from_env(local_runtime_paths).await
} else {
EnvironmentManager::from_codex_home(codex_home.clone(), local_runtime_paths).await
}
.map(Arc::new)
.map_err(std::io::Error::other)?;
let config_manager = ConfigManager::new(
codex_home.to_path_buf(),
cli_kv_overrides.clone(),
-1
View File
@@ -44,7 +44,6 @@ pub use codex_core::resolve_installation_id;
pub use codex_core::skills::SkillsManager;
pub use codex_core::thread_store_from_config;
pub use codex_exec_server::EnvironmentManager;
pub use codex_exec_server::EnvironmentManagerArgs;
pub use codex_exec_server::ExecServerRuntimePaths;
pub use codex_features::Feature;
pub use codex_features::Features;
+1 -2
View File
@@ -15,7 +15,6 @@ pub use codex_app_server_protocol::AppMetadata;
use codex_connectors::AllConnectorsCacheKey;
use codex_connectors::DirectoryListResponse;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::EnvironmentManagerArgs;
use codex_exec_server::ExecServerRuntimePaths;
use codex_protocol::models::PermissionProfile;
use codex_tools::DiscoverableTool;
@@ -202,7 +201,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
config.codex_linux_sandbox_exe.clone(),
)?;
let environment_manager =
EnvironmentManager::new(EnvironmentManagerArgs::new(local_runtime_paths)).await;
EnvironmentManager::from_codex_home(config.codex_home.clone(), local_runtime_paths).await?;
list_accessible_connectors_from_mcp_tools_with_environment_manager(
config,
force_refetch,
+13 -7
View File
@@ -15,12 +15,12 @@ pub(crate) fn default_thread_environment_selections(
cwd: &AbsolutePathBuf,
) -> Vec<TurnEnvironmentSelection> {
environment_manager
.default_environment_id()
.default_environment_ids()
.into_iter()
.map(|environment_id| TurnEnvironmentSelection {
environment_id: environment_id.to_string(),
environment_id,
cwd: cwd.clone(),
})
.into_iter()
.collect()
}
@@ -111,10 +111,16 @@ mod tests {
assert_eq!(
default_thread_environment_selections(&manager, &cwd),
vec![TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd,
}]
vec![
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd.clone(),
},
TurnEnvironmentSelection {
environment_id: "local".to_string(),
cwd,
},
]
);
}
+6 -2
View File
@@ -2,9 +2,9 @@ use std::collections::HashSet;
use std::sync::Arc;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::EnvironmentManagerArgs;
use codex_exec_server::ExecServerRuntimePaths;
use codex_login::AuthManager;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
@@ -44,7 +44,11 @@ pub async fn build_prompt_input(
&config,
Arc::clone(&auth_manager),
SessionSource::Exec,
Arc::new(EnvironmentManager::new(EnvironmentManagerArgs::new(local_runtime_paths)).await),
Arc::new(
EnvironmentManager::from_codex_home(config.codex_home.clone(), local_runtime_paths)
.await
.map_err(|err| CodexErr::Fatal(err.to_string()))?,
),
/*analytics_events_client*/ None,
thread_store,
state_db.clone(),
+94
View File
@@ -21,6 +21,7 @@ use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::ThreadSource;
use codex_protocol::protocol::TurnStartedEvent;
use codex_protocol::protocol::UserMessageEvent;
use codex_protocol::user_input::UserInput;
use core_test_support::PathBufExt;
use core_test_support::PathExt;
use core_test_support::responses::mount_models_once;
@@ -339,6 +340,99 @@ async fn start_thread_accepts_explicit_environment_when_default_environment_is_d
assert_eq!(manager.list_thread_ids().await, vec![thread.thread_id]);
}
#[tokio::test]
async fn start_thread_uses_all_default_environments_from_codex_home() {
let temp_dir = tempdir().expect("tempdir");
let mut config = test_config().await;
config.codex_home = temp_dir.path().join("codex-home").abs();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
std::fs::write(
config.codex_home.join("environments.toml"),
r#"
default = "dev"
[[environments]]
id = "dev"
program = "ssh"
args = ["dev", "cd /tmp && true"]
"#,
)
.expect("write environments.toml");
let runtime_paths = codex_exec_server::ExecServerRuntimePaths::new(
std::env::current_exe().expect("current exe path"),
/*codex_linux_sandbox_exe*/ None,
)
.expect("runtime paths");
let environment_manager = Arc::new(
codex_exec_server::EnvironmentManager::from_codex_home(
config.codex_home.clone(),
runtime_paths,
)
.await
.expect("environment manager"),
);
assert_eq!(
environment_manager.default_environment_ids(),
vec!["dev".to_string(), "local".to_string()]
);
let manager = ThreadManager::with_models_provider_and_home_for_tests(
CodexAuth::from_api_key("dummy"),
config.model_provider.clone(),
config.codex_home.to_path_buf(),
environment_manager,
);
let thread = manager
.start_thread(config)
.await
.expect("thread should start");
let prompt_items = crate::prompt_debug::build_prompt_input_from_session(
thread.thread.codex.session.as_ref(),
Vec::<UserInput>::new(),
)
.await
.expect("prompt input");
let environment_context = prompt_items
.iter()
.filter_map(|item| match item {
ResponseItem::Message { content, .. } => Some(content),
_ => None,
})
.flatten()
.find_map(|content| match content {
ContentItem::InputText { text } if text.contains("<environment_context>") => {
Some(text.as_str())
}
_ => None,
})
.expect("environment context prompt item");
assert!(environment_context.contains("<environments>"));
let cwd = thread.session_configured.cwd.display().to_string();
let dev_entry = format!(
r#"<environment id="dev">
<cwd>{cwd}</cwd>
<shell>"#
);
let local_entry = format!(
r#"<environment id="local">
<cwd>{cwd}</cwd>
<shell>"#
);
let dev_position = environment_context
.find(&dev_entry)
.expect("dev environment entry");
let local_position = environment_context
.find(&local_entry)
.expect("local environment entry");
assert!(dev_position < local_position);
assert!(!environment_context.contains("\n <cwd>"));
assert!(!environment_context.contains("\n <shell>"));
}
#[tokio::test]
async fn start_thread_keeps_internal_threads_hidden_from_normal_lookups() {
let temp_dir = tempdir().expect("tempdir");
+50 -18
View File
@@ -114,6 +114,15 @@ impl EnvironmentManager {
Self::from_provider(provider.as_ref(), local_runtime_paths).await
}
/// Builds a manager from the legacy environment-variable provider without
/// reading user config files from `CODEX_HOME`.
pub async fn from_env(
local_runtime_paths: ExecServerRuntimePaths,
) -> Result<Self, ExecServerError> {
let provider = DefaultEnvironmentProvider::from_env();
Self::from_provider(&provider, local_runtime_paths).await
}
async fn from_default_provider_url(
exec_server_url: Option<String>,
local_runtime_paths: ExecServerRuntimePaths,
@@ -147,18 +156,26 @@ impl EnvironmentManager {
environments,
default,
} = snapshot;
for id in environments.keys() {
let mut environment_map = HashMap::with_capacity(environments.len());
for (id, environment) in environments {
if id.is_empty() {
return Err(ExecServerError::Protocol(
"environment id cannot be empty".to_string(),
));
}
if environment_map
.insert(id.clone(), Arc::new(environment))
.is_some()
{
return Err(ExecServerError::Protocol(format!(
"environment id `{id}` is duplicated"
)));
}
}
let default_environment = match default {
EnvironmentDefault::Disabled => None,
EnvironmentDefault::EnvironmentId(environment_id) => {
if !environments.contains_key(&environment_id) {
if !environment_map.contains_key(&environment_id) {
return Err(ExecServerError::Protocol(format!(
"default environment `{environment_id}` is not configured"
)));
@@ -167,14 +184,10 @@ impl EnvironmentManager {
}
};
let local_environment = Arc::new(Environment::local(local_runtime_paths));
let environments = environments
.into_iter()
.map(|(id, environment)| (id, Arc::new(environment)))
.collect();
Ok(Self {
default_environment,
environments,
environments: environment_map,
local_environment,
})
}
@@ -191,6 +204,22 @@ impl EnvironmentManager {
self.default_environment.as_deref()
}
/// Returns the ordered environment ids used for new thread startup.
pub fn default_environment_ids(&self) -> Vec<String> {
let Some(default_environment_id) = self.default_environment.as_ref() else {
return Vec::new();
};
let mut environment_ids = Vec::with_capacity(self.environments.len());
environment_ids.push(default_environment_id.clone());
environment_ids.extend(
self.environments
.keys()
.filter(|environment_id| *environment_id != default_environment_id)
.cloned(),
);
environment_ids
}
/// Returns the local environment instance used for internal runtime work.
pub fn local_environment(&self) -> Arc<Environment> {
Arc::clone(&self.local_environment)
@@ -350,7 +379,6 @@ impl Environment {
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::sync::Arc;
use super::Environment;
@@ -472,11 +500,11 @@ mod tests {
async fn environment_manager_builds_from_provider() {
let provider = TestEnvironmentProvider {
snapshot: EnvironmentProviderSnapshot {
environments: HashMap::from([(
environments: vec![(
REMOTE_ENVIRONMENT_ID.to_string(),
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
.expect("remote environment"),
)]),
)],
default: EnvironmentDefault::EnvironmentId(REMOTE_ENVIRONMENT_ID.to_string()),
},
};
@@ -502,7 +530,7 @@ mod tests {
async fn environment_manager_rejects_empty_environment_id() {
let provider = TestEnvironmentProvider {
snapshot: EnvironmentProviderSnapshot {
environments: HashMap::from([("".to_string(), Environment::default_for_tests())]),
environments: vec![("".to_string(), Environment::default_for_tests())],
default: EnvironmentDefault::Disabled,
},
};
@@ -520,7 +548,7 @@ mod tests {
async fn environment_manager_uses_explicit_provider_default() {
let provider = TestEnvironmentProvider {
snapshot: EnvironmentProviderSnapshot {
environments: HashMap::from([
environments: vec![
(
LOCAL_ENVIRONMENT_ID.to_string(),
Environment::default_for_tests(),
@@ -530,7 +558,7 @@ mod tests {
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
.expect("remote environment"),
),
]),
],
default: EnvironmentDefault::EnvironmentId("devbox".to_string()),
},
};
@@ -539,6 +567,10 @@ mod tests {
.expect("manager");
assert_eq!(manager.default_environment_id(), Some("devbox"));
assert_eq!(
manager.default_environment_ids(),
vec!["devbox".to_string(), LOCAL_ENVIRONMENT_ID.to_string()]
);
assert!(manager.default_environment().expect("default").is_remote());
}
@@ -546,10 +578,10 @@ mod tests {
async fn environment_manager_disables_provider_default() {
let provider = TestEnvironmentProvider {
snapshot: EnvironmentProviderSnapshot {
environments: HashMap::from([(
environments: vec![(
LOCAL_ENVIRONMENT_ID.to_string(),
Environment::default_for_tests(),
)]),
)],
default: EnvironmentDefault::Disabled,
},
};
@@ -566,10 +598,10 @@ mod tests {
async fn environment_manager_rejects_unknown_provider_default() {
let provider = TestEnvironmentProvider {
snapshot: EnvironmentProviderSnapshot {
environments: HashMap::from([(
environments: vec![(
LOCAL_ENVIRONMENT_ID.to_string(),
Environment::default_for_tests(),
)]),
)],
default: EnvironmentDefault::EnvironmentId("missing".to_string()),
},
};
@@ -1,5 +1,3 @@
use std::collections::HashMap;
use async_trait::async_trait;
use crate::Environment;
@@ -12,9 +10,9 @@ use crate::environment::REMOTE_ENVIRONMENT_ID;
/// Lists the concrete environments available to Codex.
///
/// Implementations own a startup snapshot containing both the available
/// environment list and default environment selection. Providers that want the
/// local environment to be addressable by id should include it explicitly in
/// the returned map.
/// environment list in configured order and the default environment
/// selection. Providers that want the local environment to be addressable by
/// id should include it explicitly in the returned list.
#[async_trait]
pub trait EnvironmentProvider: Send + Sync {
/// Returns the provider-owned environment startup snapshot.
@@ -26,7 +24,7 @@ pub trait EnvironmentProvider: Send + Sync {
#[derive(Clone, Debug)]
pub struct EnvironmentProviderSnapshot {
pub environments: HashMap<String, Environment>,
pub environments: Vec<(String, Environment)>,
pub default: EnvironmentDefault,
}
@@ -57,22 +55,25 @@ impl DefaultEnvironmentProvider {
&self,
local_runtime_paths: &ExecServerRuntimePaths,
) -> EnvironmentProviderSnapshot {
let mut environments = HashMap::from([(
let mut environments = vec![(
LOCAL_ENVIRONMENT_ID.to_string(),
Environment::local(local_runtime_paths.clone()),
)]);
)];
let (exec_server_url, disabled) = normalize_exec_server_url(self.exec_server_url.clone());
if let Some(exec_server_url) = exec_server_url {
environments.insert(
environments.push((
REMOTE_ENVIRONMENT_ID.to_string(),
Environment::remote_inner(exec_server_url, Some(local_runtime_paths.clone())),
);
));
}
let has_remote = environments
.iter()
.any(|(id, _environment)| id == REMOTE_ENVIRONMENT_ID);
let default = if disabled {
EnvironmentDefault::Disabled
} else if environments.contains_key(REMOTE_ENVIRONMENT_ID) {
} else if has_remote {
EnvironmentDefault::EnvironmentId(REMOTE_ENVIRONMENT_ID.to_string())
} else {
EnvironmentDefault::EnvironmentId(LOCAL_ENVIRONMENT_ID.to_string())
@@ -105,6 +106,8 @@ pub(crate) fn normalize_exec_server_url(exec_server_url: Option<String>) -> (Opt
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use pretty_assertions::assert_eq;
use super::*;
@@ -126,7 +129,11 @@ mod tests {
.snapshot(&runtime_paths)
.await
.expect("environments");
let environments = snapshot.environments;
let EnvironmentProviderSnapshot {
environments,
default,
} = snapshot;
let environments: HashMap<_, _> = environments.into_iter().collect();
assert!(!environments[LOCAL_ENVIRONMENT_ID].is_remote());
assert_eq!(
@@ -135,7 +142,7 @@ mod tests {
);
assert!(!environments.contains_key(REMOTE_ENVIRONMENT_ID));
assert_eq!(
snapshot.default,
default,
EnvironmentDefault::EnvironmentId(LOCAL_ENVIRONMENT_ID.to_string())
);
}
@@ -148,12 +155,16 @@ mod tests {
.snapshot(&runtime_paths)
.await
.expect("environments");
let environments = snapshot.environments;
let EnvironmentProviderSnapshot {
environments,
default,
} = snapshot;
let environments: HashMap<_, _> = environments.into_iter().collect();
assert!(!environments[LOCAL_ENVIRONMENT_ID].is_remote());
assert!(!environments.contains_key(REMOTE_ENVIRONMENT_ID));
assert_eq!(
snapshot.default,
default,
EnvironmentDefault::EnvironmentId(LOCAL_ENVIRONMENT_ID.to_string())
);
}
@@ -166,11 +177,15 @@ mod tests {
.snapshot(&runtime_paths)
.await
.expect("environments");
let environments = snapshot.environments;
let EnvironmentProviderSnapshot {
environments,
default,
} = snapshot;
let environments: HashMap<_, _> = environments.into_iter().collect();
assert!(!environments[LOCAL_ENVIRONMENT_ID].is_remote());
assert!(!environments.contains_key(REMOTE_ENVIRONMENT_ID));
assert_eq!(snapshot.default, EnvironmentDefault::Disabled);
assert_eq!(default, EnvironmentDefault::Disabled);
}
#[tokio::test]
@@ -181,7 +196,11 @@ mod tests {
.snapshot(&runtime_paths)
.await
.expect("environments");
let environments = snapshot.environments;
let EnvironmentProviderSnapshot {
environments,
default,
} = snapshot;
let environments: HashMap<_, _> = environments.into_iter().collect();
assert!(!environments[LOCAL_ENVIRONMENT_ID].is_remote());
let remote_environment = &environments[REMOTE_ENVIRONMENT_ID];
@@ -191,7 +210,7 @@ mod tests {
Some("ws://127.0.0.1:8765")
);
assert_eq!(
snapshot.default,
default,
EnvironmentDefault::EnvironmentId(REMOTE_ENVIRONMENT_ID.to_string())
);
}
@@ -200,13 +219,14 @@ mod tests {
async fn default_provider_normalizes_exec_server_url() {
let provider = DefaultEnvironmentProvider::new(Some(" ws://127.0.0.1:8765 ".to_string()));
let runtime_paths = test_runtime_paths();
let environments = provider
let snapshot = provider
.snapshot(&runtime_paths)
.await
.expect("environments");
let environments: HashMap<_, _> = snapshot.environments.into_iter().collect();
assert_eq!(
environments.environments[REMOTE_ENVIRONMENT_ID].exec_server_url(),
environments[REMOTE_ENVIRONMENT_ID].exec_server_url(),
Some("ws://127.0.0.1:8765")
);
}
+30 -21
View File
@@ -44,7 +44,7 @@ struct EnvironmentToml {
#[derive(Clone, Debug, PartialEq, Eq)]
struct TomlEnvironmentProvider {
default: EnvironmentDefault,
environments: HashMap<String, ExecServerTransportParams>,
environments: Vec<(String, ExecServerTransportParams)>,
}
impl TomlEnvironmentProvider {
@@ -58,7 +58,7 @@ impl TomlEnvironmentProvider {
config_dir: Option<&Path>,
) -> Result<Self, ExecServerError> {
let mut ids = HashSet::from([LOCAL_ENVIRONMENT_ID.to_string()]);
let mut environments = HashMap::with_capacity(config.environments.len());
let mut environments = Vec::with_capacity(config.environments.len());
for item in config.environments {
let (id, transport) = parse_environment_toml(item, config_dir)?;
if !ids.insert(id.clone()) {
@@ -66,7 +66,7 @@ impl TomlEnvironmentProvider {
"environment id `{id}` is duplicated"
)));
}
environments.insert(id, transport);
environments.push((id, transport));
}
let default = normalize_default_environment_id(config.default.as_deref(), &ids)?;
Ok(Self {
@@ -82,19 +82,19 @@ impl EnvironmentProvider for TomlEnvironmentProvider {
&self,
local_runtime_paths: &ExecServerRuntimePaths,
) -> Result<EnvironmentProviderSnapshot, ExecServerError> {
let mut environments = HashMap::from([(
let mut environments = Vec::with_capacity(self.environments.len() + 1);
environments.push((
LOCAL_ENVIRONMENT_ID.to_string(),
Environment::local(local_runtime_paths.clone()),
)]);
));
for (id, transport_params) in &self.environments {
environments.insert(
environments.push((
id.clone(),
Environment::remote_with_transport(
transport_params.clone(),
Some(local_runtime_paths.clone()),
),
);
));
}
Ok(EnvironmentProviderSnapshot {
@@ -302,15 +302,6 @@ mod tests {
#[tokio::test]
async fn toml_provider_adds_implicit_local_and_configured_environments() {
let ssh_transport = ExecServerTransportParams::StdioCommand(StdioExecServerCommand {
program: "ssh".to_string(),
args: vec![
"dev".to_string(),
"codex exec-server --listen stdio".to_string(),
],
env: HashMap::from([("CODEX_LOG".to_string(), "debug".to_string())]),
cwd: None,
});
let provider = TomlEnvironmentProvider::new(EnvironmentsToml {
default: Some("ssh-dev".to_string()),
environments: vec![
@@ -345,13 +336,21 @@ mod tests {
environments,
default,
} = snapshot;
let environment_ids: Vec<_> = environments
.iter()
.map(|(id, _environment)| id.as_str())
.collect();
assert_eq!(
environment_ids,
vec![LOCAL_ENVIRONMENT_ID, "devbox", "ssh-dev"]
);
let environments: HashMap<_, _> = environments.into_iter().collect();
assert!(!environments[LOCAL_ENVIRONMENT_ID].is_remote());
assert_eq!(
environments["devbox"].exec_server_url(),
Some("ws://127.0.0.1:8765")
);
assert_eq!(provider.environments["ssh-dev"], ssh_transport);
assert!(environments["ssh-dev"].is_remote());
assert_eq!(environments["ssh-dev"].exec_server_url(), None);
assert_eq!(
@@ -483,7 +482,7 @@ mod tests {
.expect("provider");
assert_eq!(
provider.environments["ssh-dev"],
provider.environments[0].1,
ExecServerTransportParams::StdioCommand(StdioExecServerCommand {
program: "ssh".to_string(),
args: Vec::new(),
@@ -686,8 +685,13 @@ default = "none"
.snapshot(&test_runtime_paths())
.await
.expect("environments");
let environment_ids: Vec<_> = snapshot
.environments
.into_iter()
.map(|(id, _environment)| id)
.collect();
assert!(snapshot.environments.contains_key(LOCAL_ENVIRONMENT_ID));
assert!(environment_ids.contains(&LOCAL_ENVIRONMENT_ID.to_string()));
assert_eq!(snapshot.default, EnvironmentDefault::Disabled);
}
@@ -702,7 +706,12 @@ default = "none"
.snapshot(&test_runtime_paths())
.await
.expect("environments");
let environment_ids: Vec<_> = snapshot
.environments
.into_iter()
.map(|(id, _environment)| id)
.collect();
assert!(snapshot.environments.contains_key(LOCAL_ENVIRONMENT_ID));
assert!(environment_ids.contains(&LOCAL_ENVIRONMENT_ID.to_string()));
}
}
-1
View File
@@ -39,7 +39,6 @@ pub use codex_file_system::RemoveOptions;
pub use environment::CODEX_EXEC_SERVER_URL_ENV_VAR;
pub use environment::Environment;
pub use environment::EnvironmentManager;
pub use environment::EnvironmentManagerArgs;
pub use environment::LOCAL_ENVIRONMENT_ID;
pub use environment::REMOTE_ENVIRONMENT_ID;
pub use environment_provider::DefaultEnvironmentProvider;
+6 -4
View File
@@ -15,7 +15,6 @@ pub use cli::Command;
pub use cli::ReviewArgs;
use codex_app_server_client::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY;
use codex_app_server_client::EnvironmentManager;
use codex_app_server_client::EnvironmentManagerArgs;
use codex_app_server_client::ExecServerRuntimePaths;
use codex_app_server_client::InProcessAppServerClient;
use codex_app_server_client::InProcessClientStartArgs;
@@ -509,6 +508,11 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
arg0_paths.codex_linux_sandbox_exe.clone(),
)?;
let state_db = codex_core::init_state_db(&config).await;
let environment_manager = if run_loader_overrides.ignore_user_config {
EnvironmentManager::from_env(local_runtime_paths).await?
} else {
EnvironmentManager::from_codex_home(config.codex_home.clone(), local_runtime_paths).await?
};
let in_process_start_args = InProcessClientStartArgs {
arg0_paths,
config: std::sync::Arc::new(config.clone()),
@@ -518,9 +522,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
feedback: CodexFeedback::new(),
log_db: None,
state_db: state_db.clone(),
environment_manager: std::sync::Arc::new(
EnvironmentManager::new(EnvironmentManagerArgs::new(local_runtime_paths)).await,
),
environment_manager: std::sync::Arc::new(environment_manager),
config_warnings,
session_source: SessionSource::Exec,
enable_codex_api_key_env: true,
+11 -10
View File
@@ -9,7 +9,6 @@ use codex_arg0::Arg0DispatchPaths;
use codex_core::config::Config;
use codex_core::resolve_installation_id;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::EnvironmentManagerArgs;
use codex_exec_server::ExecServerRuntimePaths;
use codex_login::default_client::set_default_client_residency_requirement;
use codex_utils_cli::CliConfigOverrides;
@@ -61,15 +60,6 @@ pub async fn run_main(
arg0_paths: Arg0DispatchPaths,
cli_config_overrides: CliConfigOverrides,
) -> IoResult<()> {
let environment_manager = Arc::new(
EnvironmentManager::new(EnvironmentManagerArgs::new(
ExecServerRuntimePaths::from_optional_paths(
arg0_paths.codex_self_exe.clone(),
arg0_paths.codex_linux_sandbox_exe.clone(),
)?,
))
.await,
);
// Parse CLI overrides once and derive the base Config eagerly so later
// components do not need to work with raw TOML values.
let cli_kv_overrides = cli_config_overrides.parse_overrides().map_err(|e| {
@@ -85,6 +75,17 @@ pub async fn run_main(
})?;
set_default_client_residency_requirement(config.enforce_residency.value());
let state_db = codex_core::init_state_db(&config).await;
let environment_manager = Arc::new(
EnvironmentManager::from_codex_home(
config.codex_home.clone(),
ExecServerRuntimePaths::from_optional_paths(
arg0_paths.codex_self_exe.clone(),
arg0_paths.codex_linux_sandbox_exe.clone(),
)?,
)
.await
.map_err(std::io::Error::other)?,
);
let otel = codex_core::otel_init::build_provider(
&config,
+3 -3
View File
@@ -20,7 +20,6 @@ use codex_core_api::Config;
use codex_core_api::ConfigLayerStack;
use codex_core_api::Constrained;
use codex_core_api::EnvironmentManager;
use codex_core_api::EnvironmentManagerArgs;
use codex_core_api::EventMsg;
use codex_core_api::ExecServerRuntimePaths;
use codex_core_api::Features;
@@ -114,8 +113,9 @@ async fn run_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
config.codex_linux_sandbox_exe.clone(),
)?;
let thread_store = thread_store_from_config(&config, state_db.clone());
let environment_manager =
Arc::new(EnvironmentManager::new(EnvironmentManagerArgs::new(local_runtime_paths)).await);
let environment_manager = Arc::new(
EnvironmentManager::from_codex_home(config.codex_home.clone(), local_runtime_paths).await?,
);
let installation_id = resolve_installation_id(&config.codex_home).await?;
let thread_manager = ThreadManager::new(
&config,
+4
View File
@@ -575,6 +575,7 @@ impl App {
) -> crate::chatwidget::ChatWidgetInit {
crate::chatwidget::ChatWidgetInit {
config: cfg,
environment_manager: self.environment_manager.clone(),
frame_requester: tui.frame_requester(),
app_event_tx: self.app_event_tx.clone(),
workspace_command_runner: self.workspace_command_runner.clone(),
@@ -739,6 +740,7 @@ impl App {
.await;
let init = crate::chatwidget::ChatWidgetInit {
config: config.clone(),
environment_manager: environment_manager.clone(),
frame_requester: tui.frame_requester(),
app_event_tx: app_event_tx.clone(),
workspace_command_runner: Some(workspace_command_runner.clone()),
@@ -775,6 +777,7 @@ impl App {
})?;
let init = crate::chatwidget::ChatWidgetInit {
config: config.clone(),
environment_manager: environment_manager.clone(),
frame_requester: tui.frame_requester(),
app_event_tx: app_event_tx.clone(),
workspace_command_runner: Some(workspace_command_runner.clone()),
@@ -816,6 +819,7 @@ impl App {
})?;
let init = crate::chatwidget::ChatWidgetInit {
config: config.clone(),
environment_manager: environment_manager.clone(),
frame_requester: tui.frame_requester(),
app_event_tx: app_event_tx.clone(),
workspace_command_runner: Some(workspace_command_runner.clone()),
+2
View File
@@ -435,6 +435,7 @@ async fn enqueue_primary_thread_session_replays_turns_before_initial_prompt_subm
let model = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
app.chat_widget = ChatWidget::new_with_app_event(ChatWidgetInit {
config,
environment_manager: app.environment_manager.clone(),
frame_requester: crate::tui::FrameRequester::test_dummy(),
app_event_tx: app.app_event_tx.clone(),
workspace_command_runner: None,
@@ -4834,6 +4835,7 @@ async fn replace_chat_widget_reseeds_collab_agent_metadata_for_replay() {
let replacement = ChatWidget::new_with_app_event(ChatWidgetInit {
config: app.config.clone(),
environment_manager: app.environment_manager.clone(),
frame_requester: crate::tui::FrameRequester::test_dummy(),
app_event_tx: app.app_event_tx.clone(),
workspace_command_runner: None,
+8 -1
View File
@@ -129,6 +129,7 @@ use codex_config::types::ApprovalsReviewer;
use codex_config::types::Notifications;
use codex_config::types::WindowsSandboxModeToml;
use codex_core_skills::model::SkillMetadata;
use codex_exec_server::EnvironmentManager;
use codex_features::FEATURES;
use codex_features::Feature;
#[cfg(test)]
@@ -558,6 +559,7 @@ pub(crate) fn get_limits_duration(windows_minutes: i64) -> String {
/// Common initialization parameters shared by all `ChatWidget` constructors.
pub(crate) struct ChatWidgetInit {
pub(crate) config: Config,
pub(crate) environment_manager: Arc<EnvironmentManager>,
pub(crate) frame_requester: FrameRequester,
pub(crate) app_event_tx: AppEventSender,
/// App-server-backed runner used by status surfaces for workspace metadata probes.
@@ -759,6 +761,7 @@ pub(crate) struct ChatWidget {
/// where the overlay may briefly treat new tail content as already cached.
active_cell_revision: u64,
config: Config,
environment_manager: Arc<EnvironmentManager>,
raw_output_mode: bool,
/// Runtime value resolved by core. `config.service_tier` remains the explicit user choice.
effective_service_tier: Option<String>,
@@ -4839,6 +4842,7 @@ impl ChatWidget {
fn new_with_op_target(common: ChatWidgetInit, codex_op_target: CodexOpTarget) -> Self {
let ChatWidgetInit {
config,
environment_manager,
frame_requester,
app_event_tx,
workspace_command_runner,
@@ -4924,6 +4928,7 @@ impl ChatWidget {
active_cell_revision: 0,
raw_output_mode: config.tui_raw_output_mode,
config,
environment_manager,
effective_service_tier,
skills_all: Vec::new(),
skills_initial_state: None,
@@ -7148,12 +7153,14 @@ impl ChatWidget {
}
let config = self.config.clone();
let environment_manager = Arc::clone(&self.environment_manager);
let app_event_tx = self.app_event_tx.clone();
tokio::spawn(async move {
let accessible_result =
match connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status(
match connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager(
&config,
force_refetch,
&environment_manager,
)
.await
{
@@ -192,6 +192,7 @@ pub(super) async fn make_chatwidget_manual(
raw_output_mode: cfg.tui_raw_output_mode,
config: cfg,
effective_service_tier,
environment_manager: Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
current_collaboration_mode,
active_collaboration_mask,
has_chatgpt_account: false,
@@ -1536,6 +1536,7 @@ async fn make_startup_chat_with_cli_overrides(
let session_telemetry = test_session_telemetry(&cfg, resolved_model.as_str());
let init = ChatWidgetInit {
config: cfg.clone(),
environment_manager: Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
frame_requester: FrameRequester::test_dummy(),
app_event_tx: AppEventSender::new(unbounded_channel::<AppEvent>().0),
workspace_command_runner: None,
@@ -70,6 +70,7 @@ async fn experimental_mode_plan_is_ignored_on_startup() {
let session_telemetry = test_session_telemetry(&cfg, resolved_model.as_str());
let init = ChatWidgetInit {
config: cfg.clone(),
environment_manager: Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
frame_requester: FrameRequester::test_dummy(),
app_event_tx: AppEventSender::new(unbounded_channel::<AppEvent>().0),
workspace_command_runner: None,
@@ -246,6 +246,7 @@ async fn helpers_are_available_and_do_not_panic() {
let session_telemetry = test_session_telemetry(&cfg, resolved_model.as_str());
let init = ChatWidgetInit {
config: cfg.clone(),
environment_manager: Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
frame_requester: FrameRequester::test_dummy(),
app_event_tx: tx,
workspace_command_runner: None,
+27 -16
View File
@@ -8,7 +8,7 @@ use crate::legacy_core::config::Config;
use crate::legacy_core::config::ConfigBuilder;
use crate::legacy_core::config::ConfigOverrides;
use crate::legacy_core::config::find_codex_home;
use crate::legacy_core::config::load_config_as_toml_with_cli_overrides;
use crate::legacy_core::config::load_config_as_toml_with_cli_and_loader_overrides;
use crate::legacy_core::config::resolve_oss_provider;
use crate::legacy_core::format_exec_policy_error_with_source;
use crate::legacy_core::windows_sandbox::WindowsSandboxLevelExt;
@@ -40,7 +40,6 @@ use codex_config::ConfigLoadError;
use codex_config::LoaderOverrides;
use codex_config::format_config_error_with_source;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::EnvironmentManagerArgs;
use codex_exec_server::ExecServerRuntimePaths;
use codex_login::AuthConfig;
use codex_login::default_client::set_default_client_residency_requirement;
@@ -661,10 +660,10 @@ fn config_cwd_for_app_server_target(
app_server_target: &AppServerTarget,
environment_manager: &EnvironmentManager,
) -> std::io::Result<Option<AbsolutePathBuf>> {
if environment_manager
.default_environment()
.is_some_and(|environment| environment.is_remote())
|| matches!(app_server_target, AppServerTarget::Remote { .. })
if matches!(app_server_target, AppServerTarget::Remote { .. })
|| environment_manager
.default_environment()
.is_some_and(|environment| environment.is_remote())
{
return Ok(None);
}
@@ -678,6 +677,14 @@ fn config_cwd_for_app_server_target(
Ok(Some(cwd))
}
fn should_load_configured_environments(
loader_overrides: &LoaderOverrides,
app_server_target: &AppServerTarget,
) -> bool {
!loader_overrides.ignore_user_config
&& !matches!(app_server_target, AppServerTarget::Remote { .. })
}
fn latest_session_cwd_filter<'a>(
remote_mode: bool,
remote_cwd_override: Option<&'a Path>,
@@ -761,24 +768,28 @@ pub async fn run_main(
}
};
let environment_manager = Arc::new(
EnvironmentManager::new(EnvironmentManagerArgs::new(
ExecServerRuntimePaths::from_optional_paths(
arg0_paths.codex_self_exe.clone(),
arg0_paths.codex_linux_sandbox_exe.clone(),
)?,
))
.await,
);
let local_runtime_paths = ExecServerRuntimePaths::from_optional_paths(
arg0_paths.codex_self_exe.clone(),
arg0_paths.codex_linux_sandbox_exe.clone(),
)?;
let environment_manager =
if should_load_configured_environments(&loader_overrides, &app_server_target) {
EnvironmentManager::from_codex_home(codex_home.clone(), local_runtime_paths).await
} else {
EnvironmentManager::from_env(local_runtime_paths).await
}
.map(Arc::new)
.map_err(std::io::Error::other)?;
let cwd = cli.cwd.clone();
let config_cwd =
config_cwd_for_app_server_target(cwd.as_deref(), &app_server_target, &environment_manager)?;
#[allow(clippy::print_stderr)]
let config_toml = match load_config_as_toml_with_cli_overrides(
let config_toml = match load_config_as_toml_with_cli_and_loader_overrides(
&codex_home,
config_cwd.as_ref(),
cli_kv_overrides.clone(),
loader_overrides.clone(),
)
.await
{