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 21:42:28 +00:00
committed by GitHub
parent 33d50234a8
commit f8850cab1d
37 changed files with 346 additions and 208 deletions
@@ -299,7 +299,7 @@ pub async fn handle(
Ok(FunctionToolOutput::from_text(content, Some(true)))
}
fn single_local_environment_cwd(turn: &TurnContext) -> Result<&AbsolutePathBuf, FunctionCallError> {
fn single_local_environment_cwd(turn: &TurnContext) -> Result<AbsolutePathBuf, FunctionCallError> {
let [turn_environment] = turn.environments.turn_environments.as_slice() else {
return Err(FunctionCallError::RespondToModel(
"spawn_agents_on_csv requires exactly one local environment".to_string(),
@@ -312,5 +312,12 @@ fn single_local_environment_cwd(turn: &TurnContext) -> Result<&AbsolutePathBuf,
));
}
Ok(turn_environment.cwd())
// TODO(anp): Migrate spawn_agents_on_csv filesystem access to PathUri before enabling it for
// remote environments.
turn_environment.cwd().to_abs_path().map_err(|err| {
FunctionCallError::RespondToModel(format!(
"spawn_agents_on_csv cwd `{}` is not native to the Codex host: {err}",
turn_environment.cwd()
))
})
}
@@ -359,11 +359,18 @@ impl ApplyPatchHandler {
"apply_patch is unavailable in this session".to_string(),
));
};
let cwd = turn_environment.cwd().clone();
// TODO(anp): Migrate apply-patch verification and permission accounting to PathUri so
// patches can target environment-native foreign paths without host projection.
let cwd = turn_environment.cwd().to_abs_path().map_err(|err| {
FunctionCallError::RespondToModel(format!(
"apply_patch cwd `{}` is not native to the Codex host: {err}",
turn_environment.cwd()
))
})?;
let fs = turn_environment.environment.get_filesystem();
let sandbox = turn.file_system_sandbox_context(
/*additional_permissions*/ None,
turn_environment.cwd_uri(),
turn_environment.cwd(),
);
match codex_apply_patch::verify_apply_patch_args(args, &cwd, fs.as_ref(), Some(&sandbox))
.await
@@ -114,10 +114,15 @@ async fn to_extension_call(invocation: &ToolInvocation) -> ExtensionToolCall {
ConversationHistory::new(invocation.session.clone_history().await.into_raw_items());
let mut environments = Vec::with_capacity(invocation.turn.environments.turn_environments.len());
for environment in &invocation.turn.environments.turn_environments {
// TODO(anp): Migrate extension ToolEnvironment and granted-permission lookup to PathUri
// so extensions can receive foreign environment cwd values.
let Ok(native_cwd) = environment.cwd().to_abs_path() else {
continue;
};
let additional_permissions = apply_granted_turn_permissions(
invocation.session.as_ref(),
&environment.environment_id,
environment.cwd().as_path(),
native_cwd.as_path(),
SandboxPermissions::UseDefault,
/*additional_permissions*/ None,
)
@@ -125,10 +130,10 @@ async fn to_extension_call(invocation: &ToolInvocation) -> ExtensionToolCall {
.additional_permissions;
let file_system_sandbox_context = invocation
.turn
.file_system_sandbox_context(additional_permissions, environment.cwd_uri());
.file_system_sandbox_context(additional_permissions, environment.cwd());
environments.push(ToolEnvironment {
environment_id: environment.environment_id.clone(),
cwd: environment.cwd().clone(),
cwd: native_cwd,
file_system: environment.environment.get_filesystem(),
file_system_sandbox_context,
});
@@ -315,7 +320,7 @@ mod tests {
.environments
.turn_environments
.iter()
.map(|environment| Some(environment.cwd_uri().clone()))
.map(|environment| Some(environment.cwd().clone()))
.collect::<Vec<_>>();
let history_item = ResponseItem::Message {
id: None,
@@ -70,8 +70,16 @@ impl RequestPermissionsHandler {
"request_permissions requires a primary environment".to_string(),
));
};
// TODO(anp): Migrate request_permissions parsing and permission profiles to PathUri so
// environment-native foreign paths do not require host conversion.
let native_cwd = turn_environment.cwd().to_abs_path().map_err(|err| {
FunctionCallError::RespondToModel(format!(
"request_permissions cwd `{}` is not native to the Codex host: {err}",
turn_environment.cwd()
))
})?;
let mut args: RequestPermissionsArgs =
parse_arguments_with_base_path(&arguments, turn_environment.cwd())?;
parse_arguments_with_base_path(&arguments, &native_cwd)?;
args.permissions = normalize_additional_permissions(args.permissions.into())
.map(codex_protocol::request_permissions::RequestPermissionProfile::from)
.map_err(FunctionCallError::RespondToModel)?;
@@ -129,13 +129,21 @@ impl ExecCommandHandler {
"unified exec is unavailable in this session".to_string(),
));
};
// TODO(anp): Resolve tool paths using the selected environment's native path convention
// so unified exec can support relative paths in foreign environments.
let native_environment_cwd = turn_environment.cwd().to_abs_path().map_err(|err| {
FunctionCallError::RespondToModel(format!(
"environment cwd `{}` is not native to the Codex host: {err}",
turn_environment.cwd()
))
})?;
let cwd = environment_args
.workdir
.as_deref()
.filter(|workdir| !workdir.is_empty())
.map_or_else(
|| turn_environment.cwd().clone(),
|workdir| turn_environment.cwd().join(workdir),
|| native_environment_cwd.clone(),
|workdir| native_environment_cwd.join(workdir),
);
let environment = Arc::clone(&turn_environment.environment);
let fs = environment.get_filesystem();
@@ -277,7 +285,7 @@ impl ExecCommandHandler {
yield_time_ms,
max_output_tokens,
cwd,
sandbox_cwd: turn_environment.cwd().clone(),
sandbox_cwd: native_environment_cwd,
turn_environment: turn_environment.clone(),
shell_mode,
network: context.turn.network.clone(),
+11 -3
View File
@@ -143,11 +143,18 @@ impl ViewImageHandler {
"view_image is unavailable in this session".to_string(),
));
};
let cwd = turn_environment.cwd().clone();
// TODO(anp): Resolve tool paths using the selected environment's native path convention
// so view_image can support relative paths in foreign environments.
let cwd = turn_environment.cwd().to_abs_path().map_err(|err| {
FunctionCallError::RespondToModel(format!(
"environment cwd `{}` is not native to the Codex host: {err}",
turn_environment.cwd()
))
})?;
let abs_path = cwd.join(path);
let sandbox = turn.file_system_sandbox_context(
/*additional_permissions*/ None,
turn_environment.cwd_uri(),
turn_environment.cwd(),
);
let fs = turn_environment.environment.get_filesystem();
let path_uri = PathUri::from_abs_path(&abs_path);
@@ -272,6 +279,7 @@ mod tests {
use crate::turn_diff_tracker::TurnDiffTracker;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use core_test_support::TempDirExt;
use pretty_assertions::assert_eq;
use serde_json::json;
@@ -288,7 +296,7 @@ mod tests {
turn.environments.turn_environments[0] = TurnEnvironment::new(
current.environment_id,
current.environment,
cwd,
PathUri::from_abs_path(&cwd),
current.shell,
);
}
@@ -19,7 +19,7 @@ fn test_turn_environment(environment_id: &str) -> crate::session::turn_context::
crate::session::turn_context::TurnEnvironment::new(
environment_id.to_string(),
std::sync::Arc::new(codex_exec_server::Environment::default_for_tests()),
std::env::temp_dir().abs(),
PathUri::from_abs_path(&std::env::temp_dir().abs()),
/*shell*/ None,
)
}
@@ -427,6 +427,7 @@ mod tests {
use codex_exec_server::Environment;
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
use codex_tools::ZshForkConfig;
use codex_utils_path_uri::PathUri;
use std::sync::Arc;
use std::time::Duration;
use tempfile::tempdir;
@@ -435,7 +436,7 @@ mod tests {
TurnEnvironment::new(
LOCAL_ENVIRONMENT_ID.to_string(),
Arc::new(Environment::default_for_tests()),
cwd,
PathUri::from_abs_path(&cwd),
/*shell*/ None,
)
}