Compare commits

...

1 Commits

Author SHA1 Message Date
starr-openai
3657f23fe3 Add default cwd for configured environments
Co-authored-by: Codex <noreply@openai.com>
2026-05-11 15:28:29 -07:00
4 changed files with 134 additions and 32 deletions

View File

@@ -1033,9 +1033,10 @@ impl ThreadRequestProcessor {
let instruction_sources = Self::instruction_sources_from_config(&config).await;
let environments = environments.unwrap_or_else(|| {
listener_task_context
.thread_manager
.default_environment_selections(&config.cwd)
Self::default_environment_selections_with_provider_cwds(
listener_task_context.thread_manager.as_ref(),
&config.cwd,
)
});
let dynamic_tools = dynamic_tools.unwrap_or_default();
let core_dynamic_tools = if dynamic_tools.is_empty() {
@@ -1249,6 +1250,23 @@ impl ThreadRequestProcessor {
Ok(environment_selections)
}
fn default_environment_selections_with_provider_cwds(
thread_manager: &ThreadManager,
cwd: &AbsolutePathBuf,
) -> Vec<TurnEnvironmentSelection> {
let environment_manager = thread_manager.environment_manager();
environment_manager
.default_environment_ids()
.into_iter()
.map(|environment_id| TurnEnvironmentSelection {
cwd: environment_manager
.default_cwd(&environment_id)
.unwrap_or_else(|| cwd.clone()),
environment_id,
})
.collect()
}
async fn thread_archive_inner(
&self,
params: ThreadArchiveParams,

View File

@@ -20,6 +20,7 @@ use crate::local_process::LocalProcess;
use crate::process::ExecBackend;
use crate::remote_file_system::RemoteFileSystem;
use crate::remote_process::RemoteProcess;
use codex_utils_absolute_path::AbsolutePathBuf;
pub const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_EXEC_SERVER_URL";
@@ -43,6 +44,7 @@ pub struct EnvironmentManager {
default_environment: Option<String>,
environments: RwLock<HashMap<String, Arc<Environment>>>,
local_environment: Arc<Environment>,
environment_provider: Box<dyn EnvironmentProvider>,
}
pub const LOCAL_ENVIRONMENT_ID: &str = "local";
@@ -64,6 +66,9 @@ impl EnvironmentManagerArgs {
impl EnvironmentManager {
/// Builds a test-only manager without configured sandbox helper paths.
pub fn default_for_tests() -> Self {
let environment_provider: Box<dyn EnvironmentProvider> = Box::new(
DefaultEnvironmentProvider::new(/*exec_server_url*/ None),
);
Self {
default_environment: Some(LOCAL_ENVIRONMENT_ID.to_string()),
environments: RwLock::new(HashMap::from([(
@@ -71,15 +76,19 @@ impl EnvironmentManager {
Arc::new(Environment::default_for_tests()),
)])),
local_environment: Arc::new(Environment::default_for_tests()),
environment_provider,
}
}
/// Builds a test-only manager with environment access disabled.
pub fn disabled_for_tests(local_runtime_paths: ExecServerRuntimePaths) -> Self {
let environment_provider: Box<dyn EnvironmentProvider> =
Box::new(DefaultEnvironmentProvider::new(Some("none".to_string())));
Self {
default_environment: None,
environments: RwLock::new(HashMap::new()),
local_environment: Arc::new(Environment::local(local_runtime_paths)),
environment_provider,
}
}
@@ -112,7 +121,7 @@ impl EnvironmentManager {
local_runtime_paths: ExecServerRuntimePaths,
) -> Result<Self, ExecServerError> {
let provider = environment_provider_from_codex_home(codex_home.as_ref())?;
Self::from_provider(provider.as_ref(), local_runtime_paths).await
Self::from_provider(provider, local_runtime_paths).await
}
/// Builds a manager from the legacy environment-variable provider without
@@ -120,35 +129,35 @@ impl EnvironmentManager {
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
let provider: Box<dyn EnvironmentProvider> =
Box::new(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,
) -> Self {
let provider = DefaultEnvironmentProvider::new(exec_server_url);
match Self::from_provider(&provider, local_runtime_paths).await {
let provider: Box<dyn EnvironmentProvider> =
Box::new(DefaultEnvironmentProvider::new(exec_server_url));
match Self::from_provider(provider, local_runtime_paths).await {
Ok(manager) => manager,
Err(err) => panic!("default provider should create valid environments: {err}"),
}
}
/// Builds a manager from a provider-supplied startup snapshot.
pub async fn from_provider<P>(
provider: &P,
pub async fn from_provider(
provider: Box<dyn EnvironmentProvider>,
local_runtime_paths: ExecServerRuntimePaths,
) -> Result<Self, ExecServerError>
where
P: EnvironmentProvider + ?Sized,
{
Self::from_provider_snapshot(provider.snapshot().await?, local_runtime_paths)
) -> Result<Self, ExecServerError> {
Self::from_provider_snapshot(provider.snapshot().await?, local_runtime_paths, provider)
}
fn from_provider_snapshot(
snapshot: EnvironmentProviderSnapshot,
local_runtime_paths: ExecServerRuntimePaths,
environment_provider: Box<dyn EnvironmentProvider>,
) -> Result<Self, ExecServerError> {
let EnvironmentProviderSnapshot {
environments,
@@ -199,6 +208,7 @@ impl EnvironmentManager {
default_environment,
environments: RwLock::new(environment_map),
local_environment,
environment_provider,
})
}
@@ -214,6 +224,11 @@ impl EnvironmentManager {
self.default_environment.as_deref()
}
/// Returns the configured default cwd for one provider-owned environment.
pub fn default_cwd(&self, environment_id: &str) -> Option<AbsolutePathBuf> {
self.environment_provider.default_cwd(environment_id)
}
/// 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 {
@@ -446,6 +461,7 @@ mod tests {
use crate::environment_provider::EnvironmentProviderSnapshot;
use pretty_assertions::assert_eq;
#[derive(Debug)]
struct TestEnvironmentProvider {
snapshot: EnvironmentProviderSnapshot,
}
@@ -555,7 +571,7 @@ mod tests {
include_local: false,
},
};
let manager = EnvironmentManager::from_provider(&provider, test_runtime_paths())
let manager = EnvironmentManager::from_provider(Box::new(provider), test_runtime_paths())
.await
.expect("environment manager");
@@ -582,7 +598,7 @@ mod tests {
include_local: false,
},
};
let err = EnvironmentManager::from_provider(&provider, test_runtime_paths())
let err = EnvironmentManager::from_provider(Box::new(provider), test_runtime_paths())
.await
.expect_err("empty id should fail");
@@ -604,7 +620,7 @@ mod tests {
include_local: false,
},
};
let err = EnvironmentManager::from_provider(&provider, test_runtime_paths())
let err = EnvironmentManager::from_provider(Box::new(provider), test_runtime_paths())
.await
.expect_err("local id should fail");
@@ -627,7 +643,7 @@ mod tests {
include_local: true,
},
};
let manager = EnvironmentManager::from_provider(&provider, test_runtime_paths())
let manager = EnvironmentManager::from_provider(Box::new(provider), test_runtime_paths())
.await
.expect("manager");
@@ -652,7 +668,7 @@ mod tests {
include_local: true,
},
};
let manager = EnvironmentManager::from_provider(&provider, test_runtime_paths())
let manager = EnvironmentManager::from_provider(Box::new(provider), test_runtime_paths())
.await
.expect("manager");
@@ -679,7 +695,7 @@ mod tests {
include_local: true,
},
};
let err = EnvironmentManager::from_provider(&provider, test_runtime_paths())
let err = EnvironmentManager::from_provider(Box::new(provider), test_runtime_paths())
.await
.expect_err("unknown default should fail");

View File

@@ -5,6 +5,7 @@ use crate::ExecServerError;
use crate::environment::CODEX_EXEC_SERVER_URL_ENV_VAR;
use crate::environment::LOCAL_ENVIRONMENT_ID;
use crate::environment::REMOTE_ENVIRONMENT_ID;
use codex_utils_absolute_path::AbsolutePathBuf;
/// Lists the concrete environments available to Codex.
///
@@ -14,9 +15,14 @@ use crate::environment::REMOTE_ENVIRONMENT_ID;
/// `include_local` controls whether `EnvironmentManager` should add the local
/// environment to the snapshot.
#[async_trait]
pub trait EnvironmentProvider: Send + Sync {
pub trait EnvironmentProvider: Send + Sync + std::fmt::Debug {
/// Returns the provider-owned environment startup snapshot.
async fn snapshot(&self) -> Result<EnvironmentProviderSnapshot, ExecServerError>;
/// Returns the configured default cwd for one provider-owned environment.
fn default_cwd(&self, _environment_id: &str) -> Option<AbsolutePathBuf> {
None
}
}
#[derive(Clone, Debug)]

View File

@@ -19,6 +19,7 @@ use crate::client_api::StdioExecServerCommand;
use crate::environment::LOCAL_ENVIRONMENT_ID;
use crate::environment_provider::EnvironmentDefault;
use crate::environment_provider::EnvironmentProviderSnapshot;
use codex_utils_absolute_path::AbsolutePathBuf;
const ENVIRONMENTS_TOML_FILE: &str = "environments.toml";
const MAX_ENVIRONMENT_ID_LEN: usize = 64;
@@ -41,16 +42,24 @@ struct EnvironmentToml {
args: Option<Vec<String>>,
env: Option<HashMap<String, String>>,
cwd: Option<PathBuf>,
default_cwd: Option<PathBuf>,
#[serde(default, with = "option_duration_secs")]
connect_timeout_sec: Option<Duration>,
#[serde(default, with = "option_duration_secs")]
initialize_timeout_sec: Option<Duration>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct ConfiguredEnvironment {
id: String,
transport: ExecServerTransportParams,
default_cwd: Option<AbsolutePathBuf>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct TomlEnvironmentProvider {
default: EnvironmentDefault,
environments: Vec<(String, ExecServerTransportParams)>,
environments: Vec<ConfiguredEnvironment>,
}
impl TomlEnvironmentProvider {
@@ -66,13 +75,14 @@ impl TomlEnvironmentProvider {
let mut ids = HashSet::from([LOCAL_ENVIRONMENT_ID.to_string()]);
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()) {
let environment = parse_environment_toml(item, config_dir)?;
if !ids.insert(environment.id.clone()) {
return Err(ExecServerError::Protocol(format!(
"environment id `{id}` is duplicated"
"environment id `{}` is duplicated",
environment.id
)));
}
environments.push((id, transport));
environments.push(environment);
}
let default = normalize_default_environment_id(config.default.as_deref(), &ids)?;
Ok(Self {
@@ -86,11 +96,11 @@ impl TomlEnvironmentProvider {
impl EnvironmentProvider for TomlEnvironmentProvider {
async fn snapshot(&self) -> Result<EnvironmentProviderSnapshot, ExecServerError> {
let mut environments = Vec::with_capacity(self.environments.len());
for (id, transport_params) in &self.environments {
for environment in &self.environments {
environments.push((
id.clone(),
environment.id.clone(),
Environment::remote_with_transport(
transport_params.clone(),
environment.transport.clone(),
/*local_runtime_paths*/ None,
),
));
@@ -102,12 +112,19 @@ impl EnvironmentProvider for TomlEnvironmentProvider {
include_local: true,
})
}
fn default_cwd(&self, environment_id: &str) -> Option<AbsolutePathBuf> {
self.environments
.iter()
.find(|environment| environment.id == environment_id)
.and_then(|environment| environment.default_cwd.clone())
}
}
fn parse_environment_toml(
item: EnvironmentToml,
config_dir: Option<&Path>,
) -> Result<(String, ExecServerTransportParams), ExecServerError> {
) -> Result<ConfiguredEnvironment, ExecServerError> {
let EnvironmentToml {
id,
url,
@@ -115,6 +132,7 @@ fn parse_environment_toml(
args,
env,
cwd,
default_cwd,
connect_timeout_sec,
initialize_timeout_sec,
} = item;
@@ -133,6 +151,7 @@ fn parse_environment_toml(
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 default_cwd = normalize_default_cwd(&id, default_cwd)?;
let transport_params = match (url, program) {
(Some(url), None) => {
@@ -168,7 +187,11 @@ fn parse_environment_toml(
}
};
Ok((id, transport_params))
Ok(ConfiguredEnvironment {
id,
transport: transport_params,
default_cwd,
})
}
fn normalize_stdio_cwd(
@@ -190,6 +213,21 @@ fn normalize_stdio_cwd(
Ok(Some(config_dir.join(cwd)))
}
fn normalize_default_cwd(
id: &str,
default_cwd: Option<PathBuf>,
) -> Result<Option<AbsolutePathBuf>, ExecServerError> {
default_cwd
.map(|default_cwd| {
AbsolutePathBuf::from_absolute_path_checked(default_cwd).map_err(|err| {
ExecServerError::Protocol(format!(
"environment `{id}` default_cwd must be absolute: {err}"
))
})
})
.transpose()
}
pub(crate) fn environment_provider_from_codex_home(
codex_home: &Path,
) -> Result<Box<dyn EnvironmentProvider>, ExecServerError> {
@@ -347,6 +385,7 @@ mod tests {
"CODEX_LOG".to_string(),
"debug".to_string(),
)])),
default_cwd: Some(PathBuf::from("/home/dev-user/code/codex")),
..Default::default()
},
],
@@ -378,6 +417,10 @@ mod tests {
default,
EnvironmentDefault::EnvironmentId("ssh-dev".to_string())
);
assert_eq!(
provider.default_cwd("ssh-dev"),
Some(AbsolutePathBuf::try_from("/home/dev-user/code/codex").expect("absolute cwd"))
);
}
#[tokio::test]
@@ -405,6 +448,25 @@ mod tests {
assert_eq!(snapshot.default, EnvironmentDefault::Disabled);
}
#[test]
fn toml_provider_rejects_relative_default_cwd() {
let err = TomlEnvironmentProvider::new(EnvironmentsToml {
default: None,
environments: vec![EnvironmentToml {
id: "ssh-dev".to_string(),
program: Some("ssh".to_string()),
default_cwd: Some(PathBuf::from("repo")),
..Default::default()
}],
})
.expect_err("relative default cwd should fail");
assert!(
err.to_string().contains("default_cwd must be absolute"),
"unexpected error: {err}"
);
}
#[test]
fn toml_provider_rejects_invalid_environments() {
let cases = [