mirror of
https://github.com/openai/codex.git
synced 2026-05-18 02:02:30 +00:00
Support default cwd for remote environments
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -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,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user