diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 84500b5e55..38bce2ae32 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -682,7 +682,7 @@ Turns attach user input (text or images) to a thread and trigger Codex generatio - `{"type":"image","url":"https://…png"}` - `{"type":"localImage","path":"/tmp/screenshot.png"}` -You can optionally specify config overrides on the new turn. If specified, these settings become the default for subsequent turns on the same thread. `outputSchema` applies only to the current turn. Experimental `environments` is turn-scoped: omit it to inherit the thread's sticky environments, pass `[]` to run the turn with no environments, or pass explicit environment ids to override the sticky selection for this turn only. Each environment `cwd` must be absolute and use that environment's native path convention, so a Windows environment uses a value such as `C:\\workspace` even when app-server runs on Linux. The path must also be portable to the selected executor; app-server rejects malformed paths and Windows device names before starting the thread or turn. +You can optionally specify config overrides on the new turn. If specified, these settings become the default for subsequent turns on the same thread. `outputSchema` applies only to the current turn. Experimental `environments` is turn-scoped: omit it to inherit the thread's sticky environments, pass `[]` to run the turn with no environments, or pass explicit environment ids to override the sticky selection for this turn only. Each environment `cwd` must be absolute and use that environment's native path convention, so a Windows environment uses a value such as `C:\\workspace` even when app-server runs on Linux. The path must also be portable to the selected executor; app-server rejects malformed paths and Windows device names before starting the thread or turn. If a default environment uses a different path convention from app-server, clients must provide an explicit environment `cwd`. `approvalsReviewer` accepts: diff --git a/codex-rs/core/src/environment_selection.rs b/codex-rs/core/src/environment_selection.rs index f290aa6561..bb17cd9c98 100644 --- a/codex-rs/core/src/environment_selection.rs +++ b/codex-rs/core/src/environment_selection.rs @@ -212,6 +212,28 @@ url = "ws://127.0.0.1:8765" assert!(err.to_string().contains("duplicate")); } + #[cfg(unix)] + #[test] + fn foreign_remote_environment_requires_explicit_native_cwd() { + let environment = Arc::new( + Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string())) + .expect("remote environment"), + ); + let error = TurnEnvironment::new_with_uri( + REMOTE_ENVIRONMENT_ID.to_string(), + environment, + PathUri::parse("file:///workspace").expect("POSIX cwd URI"), + codex_utils_path_uri::PathConvention::Windows, + crate::shell::default_user_shell(), + ) + .expect_err("host cwd must not be projected into a foreign environment"); + + assert_eq!( + error.to_string(), + "explicit environment cwd required for foreign environment `remote` using Windows path syntax" + ); + } + #[tokio::test] async fn resolved_environment_selections_use_first_selection_as_primary() { let cwd = AbsolutePathBuf::current_dir().expect("cwd"); diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 916a69930e..a724d3ea7f 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -73,11 +73,19 @@ impl TurnEnvironment { path_convention: PathConvention, shell: shell::Shell, ) -> CodexResult { - ApiPathString::from_path_uri(&cwd, path_convention).map_err(|err| { - CodexErr::InvalidRequest(format!( - "turn environment cwd `{cwd}` cannot be rendered for {path_convention}: {err}" - )) - })?; + match ApiPathString::from_path_uri(&cwd, path_convention) { + Ok(_) => {} + Err(_) if environment.is_remote() && path_convention != PathConvention::native() => { + return Err(CodexErr::InvalidRequest(format!( + "explicit environment cwd required for foreign environment `{environment_id}` using {path_convention} path syntax" + ))); + } + Err(err) => { + return Err(CodexErr::InvalidRequest(format!( + "turn environment cwd `{cwd}` cannot be rendered for {path_convention}: {err}" + ))); + } + } Ok(Self { environment_id, environment, diff --git a/codex-rs/core/tests/remote_env_windows/wine_app_server_windows_exec_server_test.rs b/codex-rs/core/tests/remote_env_windows/wine_app_server_windows_exec_server_test.rs index e2be06fb9d..1f1ade54c2 100644 --- a/codex-rs/core/tests/remote_env_windows/wine_app_server_windows_exec_server_test.rs +++ b/codex-rs/core/tests/remote_env_windows/wine_app_server_windows_exec_server_test.rs @@ -163,6 +163,30 @@ async fn exercise_app_server(websocket_url: String) -> Result<()> { "invalid environment cwd must fail before model inference" ); + let default_request_id = app_server + .send_thread_start_request(ThreadStartParams { + model: Some("mock-model".to_string()), + ..Default::default() + }) + .await?; + let default_response: JSONRPCError = timeout( + APP_SERVER_TIMEOUT, + app_server.read_stream_until_error_message(RequestId::Integer(default_request_id)), + ) + .await??; + assert_eq!(default_response.id, RequestId::Integer(default_request_id)); + assert_eq!(default_response.error.code, -32600); + assert_eq!( + default_response.error.message, + format!( + "explicit environment cwd required for foreign environment `{REMOTE_ENVIRONMENT_ID}` using Windows path syntax" + ) + ); + assert!( + response_mock.requests().is_empty(), + "foreign default cwd must fail before model inference" + ); + let environment = remote_windows_environment(); let thread_request_id = app_server .send_thread_start_request(ThreadStartParams {