mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
efbd00f21f
## 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
309 lines
11 KiB
Rust
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);
|
|
}
|
|
}
|