Support default cwd for remote environments

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
starr-openai
2026-05-11 11:28:05 -07:00
parent cf6342b75b
commit 5af175990d
5 changed files with 152 additions and 18 deletions

View File

@@ -17,9 +17,17 @@ pub(crate) fn default_thread_environment_selections(
environment_manager
.default_environment_ids()
.into_iter()
.map(|environment_id| TurnEnvironmentSelection {
environment_id,
cwd: cwd.clone(),
.map(|environment_id| {
let environment = environment_manager
.get_environment(&environment_id)
.expect("default environment id should resolve");
TurnEnvironmentSelection {
environment_id,
cwd: environment
.default_cwd()
.cloned()
.unwrap_or_else(|| cwd.clone()),
}
})
.collect()
}
@@ -122,13 +130,17 @@ mod tests {
#[tokio::test]
async fn toml_default_thread_environment_selections_include_local_and_remote() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let default_cwd = AbsolutePathBuf::from_absolute_path("/workspace").expect("cwd");
std::fs::write(
temp_dir.path().join("environments.toml"),
r#"
format!(
r#"
[[environments]]
id = "remote"
url = "ws://127.0.0.1:8765"
"#,
default_cwd = "{default_cwd}"
"#
),
)
.expect("write environments.toml");
let cwd = AbsolutePathBuf::current_dir().expect("cwd");
@@ -145,7 +157,7 @@ url = "ws://127.0.0.1:8765"
},
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd,
cwd: default_cwd,
},
]
);

View File

@@ -351,17 +351,22 @@ async fn start_thread_uses_all_default_environments_from_codex_home() {
let mut config = test_config().await;
config.codex_home = temp_dir.path().join("codex-home").abs();
config.cwd = config.codex_home.abs();
let local_cwd = config.cwd.display().to_string();
let dev_cwd = "/home/dev-user/code/codex";
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
std::fs::write(
config.codex_home.join("environments.toml"),
r#"
format!(
r#"
default = "dev"
[[environments]]
id = "dev"
program = "ssh"
args = ["dev", "cd /tmp && true"]
"#,
default_cwd = "{dev_cwd}"
"#
),
)
.expect("write environments.toml");
@@ -416,15 +421,14 @@ args = ["dev", "cd /tmp && true"]
})
.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>
<cwd>{dev_cwd}</cwd>
<shell>"#
);
let local_entry = format!(
r#"<environment id="local">
<cwd>{cwd}</cwd>
<cwd>{local_cwd}</cwd>
<shell>"#
);
let dev_position = environment_context

View File

@@ -2,8 +2,10 @@ use anyhow::Context;
use anyhow::Result;
use codex_exec_server::CopyOptions;
use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::ExecParams;
use codex_exec_server::FileSystemSandboxContext;
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
use codex_exec_server::ProcessId;
use codex_exec_server::REMOTE_ENVIRONMENT_ID;
use codex_exec_server::RemoveOptions;
use codex_features::Feature;
@@ -84,6 +86,74 @@ async fn remote_test_env_can_connect_and_use_filesystem() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn environments_toml_default_cwd_runs_in_docker_remote_environment() -> Result<()> {
let Some(_remote_env) = get_remote_test_env() else {
return Ok(());
};
let codex_home = TempDir::new()?;
let default_cwd = remote_test_file_path().join("default-cwd").abs();
let default_cwd_display = default_cwd.as_path().display();
remote_exec(&format!("mkdir -p {}", default_cwd.as_path().display()))?;
std::fs::write(
codex_home.path().join("environments.toml"),
format!(
r#"
default = "docker"
[[environments]]
id = "docker"
url = "{}"
default_cwd = "{default_cwd_display}"
"#,
std::env::var("CODEX_TEST_REMOTE_EXEC_SERVER_URL")?
),
)?;
let environment_manager = codex_exec_server::EnvironmentManager::from_codex_home(
codex_home.path(),
codex_exec_server::ExecServerRuntimePaths::new(
std::env::current_exe()?,
/*codex_linux_sandbox_exe*/ None,
)?,
)
.await?;
let environment = environment_manager
.default_environment()
.context("default environment should resolve")?;
assert_eq!(environment.default_cwd(), Some(&default_cwd));
let started = environment
.get_exec_backend()
.start(ExecParams {
process_id: ProcessId::from("default-cwd-pwd"),
argv: vec!["pwd".to_string()],
cwd: default_cwd.to_path_buf(),
env_policy: None,
env: Default::default(),
tty: false,
pipe_stdin: false,
arg0: None,
})
.await?;
let response = started
.process
.read(/*after_seq*/ None, None, Some(1_000))
.await?;
let stdout = response
.chunks
.into_iter()
.flat_map(|chunk| chunk.chunk.0)
.collect::<Vec<_>>();
assert_eq!(
String::from_utf8(stdout)?,
format!("{default_cwd_display}\n")
);
Ok(())
}
fn absolute_path(path: PathBuf) -> AbsolutePathBuf {
match AbsolutePathBuf::try_from(path) {
Ok(path) => path,

View File

@@ -2,6 +2,8 @@ use std::collections::HashMap;
use std::sync::Arc;
use std::sync::RwLock;
use codex_utils_absolute_path::AbsolutePathBuf;
use crate::ExecServerError;
use crate::ExecServerRuntimePaths;
use crate::ExecutorFileSystem;
@@ -289,6 +291,7 @@ impl EnvironmentManager {
/// paths used by filesystem helpers.
#[derive(Clone)]
pub struct Environment {
default_cwd: Option<AbsolutePathBuf>,
exec_server_url: Option<String>,
remote_transport: Option<ExecServerTransportParams>,
exec_backend: Arc<dyn ExecBackend>,
@@ -301,6 +304,7 @@ impl Environment {
/// Builds a test-only local environment without configured sandbox helper paths.
pub fn default_for_tests() -> Self {
Self {
default_cwd: None,
exec_server_url: None,
remote_transport: None,
exec_backend: Arc::new(LocalProcess::default()),
@@ -357,6 +361,7 @@ impl Environment {
pub(crate) fn local(local_runtime_paths: ExecServerRuntimePaths) -> Self {
Self {
default_cwd: None,
exec_server_url: None,
remote_transport: None,
exec_backend: Arc::new(LocalProcess::default()),
@@ -381,6 +386,18 @@ impl Environment {
pub(crate) fn remote_with_transport(
remote_transport: ExecServerTransportParams,
local_runtime_paths: Option<ExecServerRuntimePaths>,
) -> Self {
Self::remote_with_transport_and_default_cwd(
remote_transport,
local_runtime_paths,
/*default_cwd*/ None,
)
}
pub(crate) fn remote_with_transport_and_default_cwd(
remote_transport: ExecServerTransportParams,
local_runtime_paths: Option<ExecServerRuntimePaths>,
default_cwd: Option<AbsolutePathBuf>,
) -> Self {
let exec_server_url = match &remote_transport {
ExecServerTransportParams::WebSocketUrl {
@@ -395,6 +412,7 @@ impl Environment {
Arc::new(RemoteFileSystem::new(client.clone()));
Self {
default_cwd,
exec_server_url,
remote_transport: Some(remote_transport),
exec_backend,
@@ -408,6 +426,10 @@ impl Environment {
self.remote_transport.is_some()
}
pub fn default_cwd(&self) -> Option<&AbsolutePathBuf> {
self.default_cwd.as_ref()
}
/// Returns the remote exec-server URL when this environment is remote.
pub fn exec_server_url(&self) -> Option<&str> {
self.exec_server_url.as_deref()

View File

@@ -5,6 +5,7 @@ use std::path::PathBuf;
use std::time::Duration;
use async_trait::async_trait;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
@@ -41,6 +42,7 @@ struct EnvironmentToml {
args: Option<Vec<String>>,
env: Option<HashMap<String, String>>,
cwd: Option<PathBuf>,
default_cwd: Option<AbsolutePathBuf>,
#[serde(default, with = "option_duration_secs")]
connect_timeout_sec: Option<Duration>,
#[serde(default, with = "option_duration_secs")]
@@ -50,7 +52,7 @@ struct EnvironmentToml {
#[derive(Clone, Debug, PartialEq, Eq)]
struct TomlEnvironmentProvider {
default: EnvironmentDefault,
environments: Vec<(String, ExecServerTransportParams)>,
environments: Vec<(String, ExecServerTransportParams, Option<AbsolutePathBuf>)>,
}
impl TomlEnvironmentProvider {
@@ -66,13 +68,13 @@ 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)?;
let (id, transport, default_cwd) = parse_environment_toml(item, config_dir)?;
if !ids.insert(id.clone()) {
return Err(ExecServerError::Protocol(format!(
"environment id `{id}` is duplicated"
)));
}
environments.push((id, transport));
environments.push((id, transport, default_cwd));
}
let default = normalize_default_environment_id(config.default.as_deref(), &ids)?;
Ok(Self {
@@ -86,12 +88,13 @@ 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 (id, transport_params, default_cwd) in &self.environments {
environments.push((
id.clone(),
Environment::remote_with_transport(
Environment::remote_with_transport_and_default_cwd(
transport_params.clone(),
/*local_runtime_paths*/ None,
default_cwd.clone(),
),
));
}
@@ -107,7 +110,7 @@ impl EnvironmentProvider for TomlEnvironmentProvider {
fn parse_environment_toml(
item: EnvironmentToml,
config_dir: Option<&Path>,
) -> Result<(String, ExecServerTransportParams), ExecServerError> {
) -> Result<(String, ExecServerTransportParams, Option<AbsolutePathBuf>), ExecServerError> {
let EnvironmentToml {
id,
url,
@@ -115,6 +118,7 @@ fn parse_environment_toml(
args,
env,
cwd,
default_cwd,
connect_timeout_sec,
initialize_timeout_sec,
} = item;
@@ -168,7 +172,7 @@ fn parse_environment_toml(
}
};
Ok((id, transport_params))
Ok((id, transport_params, default_cwd))
}
fn normalize_stdio_cwd(
@@ -565,6 +569,26 @@ mod tests {
);
}
#[tokio::test]
async fn toml_provider_adds_configured_default_cwd_to_environment() {
let default_cwd = AbsolutePathBuf::from_absolute_path("/workspace").expect("cwd");
let provider = TomlEnvironmentProvider::new(EnvironmentsToml {
default: None,
environments: vec![EnvironmentToml {
id: "ssh-dev".to_string(),
program: Some("ssh".to_string()),
default_cwd: Some(default_cwd.clone()),
..Default::default()
}],
})
.expect("provider");
let snapshot = provider.snapshot().await.expect("environments");
let environment = &snapshot.environments[0].1;
assert_eq!(environment.default_cwd(), Some(&default_cwd));
}
#[test]
fn toml_provider_rejects_relative_stdio_cwd_without_config_dir() {
let err = TomlEnvironmentProvider::new(EnvironmentsToml {
@@ -664,6 +688,7 @@ id = "ssh-dev"
program = "ssh"
args = ["dev", "codex exec-server --listen stdio"]
cwd = "/tmp"
default_cwd = "/workspace"
[environments.env]
CODEX_LOG = "debug"
"#,
@@ -698,6 +723,7 @@ CODEX_LOG = "debug"
"debug".to_string(),
)])),
cwd: Some(PathBuf::from("/tmp")),
default_cwd: Some(AbsolutePathBuf::from_absolute_path("/workspace").expect("cwd")),
..Default::default()
}
);