mcp: accept foreign absolute cwd for remote stdio (#29493)

## Why

Remote stdio MCP servers can run in an environment whose path convention
differs from the Codex host. A Windows cwd such as
`C:\Users\openai\share` is absolute for the executor but was rejected by
a POSIX orchestrator.

Built on #29501, now merged, which only clarifies the host-native
`PathUri` constructor name.

## What changed

- Deserialize MCP cwd values as `LegacyAppPathString` so config does not
apply host path rules.
- Interpret that spelling as host-native for local launches and convert
it to `PathUri` at executor launch.
- Skip host filesystem and command resolution checks for remote stdio in
`codex doctor`.
- Add host-independent config and executor-boundary coverage using the
foreign path convention for each test platform.

## Validation

- `just test -p codex-utils-path-uri -p codex-config -p codex-mcp -p
codex-rmcp-client` (408 passed)
- `just test -p codex-cli -p codex-rmcp-client` (372 passed)
- `cargo check --workspace --tests`
- `just test` (11,311 passed; 43 unrelated environment/timing failures)
- `just fix -p codex-cli -p codex-config -p codex-core -p codex-mcp -p
codex-mcp-extension -p codex-rmcp-client -p codex-tui`
This commit is contained in:
Adam Perry @ OpenAI
2026-06-22 18:33:51 -07:00
committed by GitHub
Unverified
parent 3310fc8ae5
commit 67009bc53f
20 changed files with 250 additions and 162 deletions
+7 -4
View File
@@ -89,6 +89,7 @@ use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::NetworkAccess;
use codex_protocol::protocol::RealtimeVoice;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_path_uri::LegacyAppPathString;
use serde::Deserialize;
use tempfile::tempdir;
@@ -5554,6 +5555,7 @@ async fn load_global_mcp_servers_returns_empty_if_missing() -> anyhow::Result<()
#[tokio::test]
async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let expected_cwd = LegacyAppPathString::from_path(codex_home.path());
let mut servers = BTreeMap::new();
servers.insert(
@@ -5564,7 +5566,7 @@ async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> {
args: vec!["hello".to_string()],
env: None,
env_vars: Vec::new(),
cwd: Some(codex_home.path().to_path_buf()),
cwd: Some(expected_cwd.clone()),
},
environment_id: "remote".to_string(),
enabled: true,
@@ -5603,7 +5605,7 @@ async fn replace_mcp_servers_round_trips_entries() -> anyhow::Result<()> {
assert_eq!(args, &vec!["hello".to_string()]);
assert!(env.is_none());
assert!(env_vars.is_empty());
assert_eq!(cwd, &Some(codex_home.path().to_path_buf()));
assert_eq!(cwd, &Some(expected_cwd));
}
other => panic!("unexpected transport {other:?}"),
}
@@ -6104,6 +6106,7 @@ async fn replace_mcp_servers_serializes_cwd() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let cwd_path = PathBuf::from("/tmp/codex-mcp");
let cwd = LegacyAppPathString::from_path(&cwd_path);
let servers = BTreeMap::from([(
"docs".to_string(),
McpServerConfig {
@@ -6112,7 +6115,7 @@ async fn replace_mcp_servers_serializes_cwd() -> anyhow::Result<()> {
args: Vec::new(),
env: None,
env_vars: Vec::new(),
cwd: Some(cwd_path.clone()),
cwd: Some(cwd.clone()),
},
environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
enabled: true,
@@ -6147,7 +6150,7 @@ async fn replace_mcp_servers_serializes_cwd() -> anyhow::Result<()> {
let docs = loaded.get("docs").expect("docs entry");
match &docs.transport {
McpServerTransportConfig::Stdio { cwd, .. } => {
assert_eq!(cwd.as_deref(), Some(Path::new("/tmp/codex-mcp")));
assert_eq!(cwd, &Some(LegacyAppPathString::from_path(&cwd_path)));
}
other => panic!("unexpected transport {other:?}"),
}
@@ -69,7 +69,7 @@ fn serialize_mcp_server_table(config: &McpServerConfig) -> TomlTable {
entry["env_vars"] = array_from_env_vars(env_vars);
}
if let Some(cwd) = cwd {
entry["cwd"] = value(cwd.to_string_lossy().to_string());
entry["cwd"] = value(cwd.as_str());
}
}
McpServerTransportConfig::StreamableHttp {