app-server: preserve target-native environment cwd (#28146)

## Why

app-server may run on a different OS from the selected exec-server
environment. Parsing that environment’s cwd with the Codex host’s path
rules prevents thread startup.

## What

Carry environment cwd values as `LegacyAppPathString` at the app-server
boundary and `PathUri` internally. Existing tool-call schemas and
relative-path behavior stay host-native; remaining local-only consumers
convert explicitly and leave follow-up TODOs.

The Wine integration test verifies app-server can start a thread and
complete an ordinary turn with a Windows environment cwd from Linux.

## Validation

- `bazel test //codex-rs/core/tests/remote_env_windows:smoke-test
--test_output=errors`
- focused app-server environment-selection and protocol schema tests
- scoped Clippy for `codex-core` and `codex-app-server-protocol`
This commit is contained in:
Adam Perry @ OpenAI
2026-06-16 14:42:28 -07:00
committed by GitHub
Unverified
parent 33d50234a8
commit f8850cab1d
37 changed files with 346 additions and 208 deletions
@@ -3723,7 +3723,14 @@ fn thread_settings_update_params_preserve_field_level_experimental_gates() {
#[test]
fn turn_start_params_round_trip_environments() {
let cwd = test_absolute_path();
// Use a path foreign to the test host so this exercises syntax preservation instead of the
// host-native conversion performed by test_absolute_path().
#[cfg(windows)]
let raw_cwd = "/workspace";
#[cfg(not(windows))]
let raw_cwd = r"C:\workspace";
let cwd: LegacyAppPathString =
serde_json::from_value(json!(raw_cwd)).expect("API path should deserialize");
let params: TurnStartParams = serde_json::from_value(json!({
"threadId": "thread_123",
"input": [],
@@ -3805,27 +3812,6 @@ fn turn_start_params_treat_null_or_omitted_environments_as_default() {
);
}
#[test]
fn turn_start_params_reject_relative_environment_cwd() {
let err = serde_json::from_value::<TurnStartParams>(json!({
"threadId": "thread_123",
"input": [],
"environments": [
{
"environmentId": "local",
"cwd": "relative"
}
],
}))
.expect_err("relative environment cwd should fail");
assert!(
err.to_string()
.contains("AbsolutePathBuf deserialized without a base path"),
"unexpected error: {err}"
);
}
#[test]
fn realtime_append_text_defaults_role_to_user() {
let params = serde_json::from_value::<ThreadRealtimeAppendTextParams>(json!({
@@ -14,6 +14,7 @@ use codex_protocol::user_input::ByteRange as CoreByteRange;
use codex_protocol::user_input::TextElement as CoreTextElement;
use codex_protocol::user_input::UserInput as CoreUserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::LegacyAppPathString;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
@@ -38,7 +39,7 @@ pub enum TurnStatus {
#[ts(export_to = "v2/")]
pub struct TurnEnvironmentParams {
pub environment_id: String,
pub cwd: AbsolutePathBuf,
pub cwd: LegacyAppPathString,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]