Files
codex/codex-rs/core/src/environment_selection.rs
T
Adam Perry @ OpenAI efbd00f21f [codex] exec-server honors remote environment cwd and shell (#28122)
## Why

Next slice needed to make progress on the `remote_env_windows` test is
to support passing a Windows cwd for the remote environment and using
that environment's native shell. This lets the test run a real Windows
process instead of only recording an early path or shell mismatch.

## What

- change `TurnEnvironmentSelection.cwd` from `AbsolutePathBuf` to
`PathUri`
- convert local cwd values to URIs when constructing selections
- preserve a remote primary cwd instead of replacing it with the local
legacy fallback
- prefer the selected environment's discovered shell for unified exec,
falling back to the session shell when unavailable
- convert back to a host-native absolute path at current native-only
consumer boundaries
- reject or deny unsupported foreign cwd values at the existing
request-permissions boundary, with TODOs for its future migration
- extend the hermetic Wine test to execute Windows PowerShell in
`C:\windows` and verify successful process completion
- record the current app-server rejection against the same Wine-backed
remote Windows fixture when its cwd is supplied as a native Windows path
2026-06-14 06:07:46 +00:00

309 lines
11 KiB
Rust

use std::collections::HashSet;
use std::sync::Arc;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::ExecutorFileSystem;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use crate::session::turn_context::TurnEnvironment;
use crate::shell::Shell;
pub(crate) fn default_thread_environment_selections(
environment_manager: &EnvironmentManager,
cwd: &AbsolutePathBuf,
) -> Vec<TurnEnvironmentSelection> {
environment_manager
.default_environment_ids()
.into_iter()
.map(|environment_id| TurnEnvironmentSelection {
environment_id,
cwd: PathUri::from_abs_path(cwd),
})
.collect()
}
#[derive(Clone, Debug, Default)]
pub(crate) struct ResolvedTurnEnvironments {
pub(crate) turn_environments: Vec<TurnEnvironment>,
}
impl ResolvedTurnEnvironments {
pub(crate) fn to_selections(&self) -> Vec<TurnEnvironmentSelection> {
self.turn_environments
.iter()
.map(TurnEnvironment::selection)
.collect()
}
pub(crate) fn primary(&self) -> Option<&TurnEnvironment> {
self.turn_environments.first()
}
#[cfg(test)]
pub(crate) fn primary_environment(&self) -> Option<Arc<codex_exec_server::Environment>> {
self.primary()
.map(|environment| Arc::clone(&environment.environment))
}
pub(crate) fn primary_filesystem(&self) -> Option<Arc<dyn ExecutorFileSystem>> {
self.primary()
.map(|environment| environment.environment.get_filesystem())
}
pub(crate) fn single_local_environment_cwd(&self) -> Option<&AbsolutePathBuf> {
let [environment] = self.turn_environments.as_slice() else {
return None;
};
(!environment.environment.is_remote()).then_some(environment.cwd())
}
}
pub(crate) async fn resolve_environment_selections(
environment_manager: &EnvironmentManager,
environments: &[TurnEnvironmentSelection],
) -> CodexResult<ResolvedTurnEnvironments> {
let mut seen_environment_ids = HashSet::with_capacity(environments.len());
let mut turn_environments = Vec::with_capacity(environments.len());
for selected_environment in environments {
if !seen_environment_ids.insert(selected_environment.environment_id.as_str()) {
return Err(CodexErr::InvalidRequest(format!(
"duplicate turn environment id `{}`",
selected_environment.environment_id
)));
}
let environment_id = selected_environment.environment_id.clone();
let environment = environment_manager
.get_environment(&environment_id)
.ok_or_else(|| {
CodexErr::InvalidRequest(format!("unknown turn environment id `{environment_id}`"))
})?;
let shell = match environment.info().await {
Ok(info) => match Shell::from_environment_shell_info(info.shell) {
Ok(shell) => Some(shell),
Err(err) => {
tracing::warn!(
"failed to resolve shell for environment `{environment_id}`: {err}"
);
None
}
},
Err(err) => {
tracing::warn!("failed to get info for environment `{environment_id}`: {err}");
None
}
};
turn_environments.push(TurnEnvironment::new(
environment_id,
environment,
selected_environment.cwd.to_abs_path().map_err(|err| {
CodexErr::InvalidRequest(format!(
"turn environment cwd `{}` is not valid on this host: {err}",
selected_environment.cwd
))
})?,
shell,
));
}
Ok(ResolvedTurnEnvironments { turn_environments })
}
#[cfg(test)]
mod tests {
use codex_exec_server::Environment;
use codex_exec_server::ExecServerRuntimePaths;
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
use codex_exec_server::REMOTE_ENVIRONMENT_ID;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use super::*;
fn test_runtime_paths() -> ExecServerRuntimePaths {
ExecServerRuntimePaths::new(
std::env::current_exe().expect("current exe"),
/*codex_linux_sandbox_exe*/ None,
)
.expect("runtime paths")
}
#[tokio::test]
async fn default_thread_environment_selections_use_manager_default_id() {
let cwd = AbsolutePathBuf::current_dir().expect("cwd");
let cwd_uri = PathUri::from_abs_path(&cwd);
let manager = EnvironmentManager::create_for_tests(
Some("ws://127.0.0.1:8765".to_string()),
Some(test_runtime_paths()),
)
.await;
assert_eq!(
default_thread_environment_selections(&manager, &cwd),
vec![TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri,
}]
);
}
#[tokio::test]
async fn toml_default_thread_environment_selections_include_local_and_remote() {
let temp_dir = tempfile::tempdir().expect("tempdir");
std::fs::write(
temp_dir.path().join("environments.toml"),
r#"
[[environments]]
id = "remote"
url = "ws://127.0.0.1:8765"
"#,
)
.expect("write environments.toml");
let cwd = AbsolutePathBuf::current_dir().expect("cwd");
let cwd_uri = PathUri::from_abs_path(&cwd);
let manager =
EnvironmentManager::from_codex_home(temp_dir.path(), Some(test_runtime_paths()))
.await
.expect("environment manager");
assert_eq!(
default_thread_environment_selections(&manager, &cwd),
vec![
TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
},
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri,
},
]
);
}
#[tokio::test]
async fn default_thread_environment_selections_empty_when_default_disabled() {
let cwd = AbsolutePathBuf::current_dir().expect("cwd");
let manager = EnvironmentManager::without_environments();
assert_eq!(
default_thread_environment_selections(&manager, &cwd),
Vec::<TurnEnvironmentSelection>::new()
);
}
#[tokio::test]
async fn resolve_environment_selections_rejects_duplicate_ids() {
let cwd = AbsolutePathBuf::current_dir().expect("cwd");
let cwd_uri = PathUri::from_abs_path(&cwd);
let manager = EnvironmentManager::default_for_tests();
let err = resolve_environment_selections(
&manager,
&[
TurnEnvironmentSelection {
environment_id: "local".to_string(),
cwd: cwd_uri.clone(),
},
TurnEnvironmentSelection {
environment_id: "local".to_string(),
cwd: cwd_uri.join("other").expect("other cwd URI"),
},
],
)
.await
.expect_err("duplicate environment id should fail");
assert!(err.to_string().contains("duplicate"));
}
#[tokio::test]
async fn resolved_environment_selections_use_first_selection_as_primary() {
let cwd = AbsolutePathBuf::current_dir().expect("cwd");
let selected_cwd = cwd.join("selected");
let selected_cwd_uri = PathUri::from_abs_path(&selected_cwd);
let manager = EnvironmentManager::default_for_tests();
let resolved = resolve_environment_selections(
&manager,
&[TurnEnvironmentSelection {
environment_id: "local".to_string(),
cwd: selected_cwd_uri,
}],
)
.await
.expect("environment selections should resolve");
assert_eq!(
resolved
.primary()
.expect("primary environment")
.environment_id,
"local"
);
assert_eq!(
resolved.primary().expect("primary environment").shell,
Some(
Shell::from_environment_shell_info(
manager
.get_environment("local")
.expect("local environment")
.info()
.await
.expect("local environment info")
.shell
)
.expect("resolved shell")
)
);
}
#[tokio::test]
async fn single_local_environment_cwd_requires_exactly_one_local_environment() {
let cwd = AbsolutePathBuf::current_dir().expect("cwd");
let cwd_uri = PathUri::from_abs_path(&cwd);
let local_manager = EnvironmentManager::default_for_tests();
let local = resolve_environment_selections(
&local_manager,
&[TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri,
}],
)
.await
.expect("local environment should resolve");
let remote_environment = Arc::new(
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
.expect("remote environment"),
);
let remote = ResolvedTurnEnvironments {
turn_environments: vec![TurnEnvironment::new(
REMOTE_ENVIRONMENT_ID.to_string(),
remote_environment.clone(),
cwd.clone(),
/*shell*/ None,
)],
};
let multiple = ResolvedTurnEnvironments {
turn_environments: vec![
local.primary().expect("local environment").clone(),
TurnEnvironment::new(
REMOTE_ENVIRONMENT_ID.to_string(),
remote_environment,
cwd.clone(),
/*shell*/ None,
),
],
};
assert_eq!(local.single_local_environment_cwd(), Some(&cwd));
assert_eq!(remote.single_local_environment_cwd(), None);
assert_eq!(multiple.single_local_environment_cwd(), None);
}
}