mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
3310fc8ae5
commit
67009bc53f
+98
-16
@@ -1520,19 +1520,35 @@ async fn mcp_check_from_servers(servers: &HashMap<String, McpServerConfig>) -> D
|
||||
if disabled_server {
|
||||
continue;
|
||||
}
|
||||
if let Some(cwd) = cwd
|
||||
&& !cwd.exists()
|
||||
{
|
||||
missing_env.push(format!("{name}: cwd does not exist ({})", cwd.display()));
|
||||
}
|
||||
if command.trim().is_empty() {
|
||||
let command_is_empty = command.trim().is_empty();
|
||||
if command_is_empty {
|
||||
missing_env.push(format!("{name}: stdio command is empty"));
|
||||
} else if let Err(err) =
|
||||
stdio_command_resolves(command, cwd.as_deref(), env.as_ref())
|
||||
{
|
||||
missing_env.push(format!(
|
||||
"{name}: stdio command {command:?} is not resolvable ({err})"
|
||||
));
|
||||
}
|
||||
if server.is_local_environment() {
|
||||
let host_native_cwd = cwd.as_ref().map(|cwd| Path::new(cwd.as_str()));
|
||||
if let Some(cwd) = host_native_cwd
|
||||
&& !cwd.exists()
|
||||
{
|
||||
missing_env.push(format!("{name}: cwd does not exist ({})", cwd.display()));
|
||||
}
|
||||
if !command_is_empty
|
||||
&& let Err(err) =
|
||||
stdio_command_resolves(command, host_native_cwd, env.as_ref())
|
||||
{
|
||||
missing_env.push(format!(
|
||||
"{name}: stdio command {command:?} is not resolvable ({err})"
|
||||
));
|
||||
}
|
||||
} else {
|
||||
match cwd {
|
||||
Some(cwd) if cwd.to_inferred_path_uri().is_none() => {
|
||||
missing_env
|
||||
.push(format!("{name}: remote stdio cwd is not absolute ({cwd})"));
|
||||
}
|
||||
None => missing_env
|
||||
.push(format!("{name}: remote stdio requires an explicit cwd")),
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
if let Some(env) = env {
|
||||
for key in env.keys().filter(|key| key.trim().is_empty()) {
|
||||
@@ -1541,10 +1557,12 @@ async fn mcp_check_from_servers(servers: &HashMap<String, McpServerConfig>) -> D
|
||||
}
|
||||
for env_var in env_vars {
|
||||
if env_var.is_remote_source() {
|
||||
missing_env.push(format!(
|
||||
"{name}: env_vars entry `{}` uses source `remote`, which requires remote MCP stdio",
|
||||
env_var.name()
|
||||
));
|
||||
if server.is_local_environment() {
|
||||
missing_env.push(format!(
|
||||
"{name}: env_vars entry `{}` uses source `remote`, which requires remote MCP stdio",
|
||||
env_var.name()
|
||||
));
|
||||
}
|
||||
} else if !env_var_present(env_var.name()) {
|
||||
missing_env.push(format!("{name}: env var {} is not set", env_var.name()));
|
||||
}
|
||||
@@ -3863,6 +3881,70 @@ mod tests {
|
||||
}));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mcp_check_skips_host_path_checks_for_remote_stdio() {
|
||||
#[cfg(not(windows))]
|
||||
let cwd = r"C:\Users\openai\share";
|
||||
#[cfg(windows)]
|
||||
let cwd = "/home/openai/share";
|
||||
let cwd = toml::Value::String(cwd.to_string());
|
||||
let remote_server: McpServerConfig = toml::from_str(&format!(
|
||||
r#"
|
||||
command = "definitely-missing-codex-doctor-mcp"
|
||||
environment_id = "remote"
|
||||
cwd = {cwd}
|
||||
required = true
|
||||
env_vars = [{{ name = "REMOTE_ONLY_TOKEN", source = "remote" }}]
|
||||
"#,
|
||||
))
|
||||
.expect("should deserialize remote MCP config");
|
||||
let servers = HashMap::from([("remote".to_string(), remote_server)]);
|
||||
|
||||
let check = mcp_check_from_servers(&servers).await;
|
||||
|
||||
assert_eq!(check.status, CheckStatus::Ok);
|
||||
assert_eq!(check.summary, "MCP configuration is locally consistent");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mcp_check_validates_remote_stdio_cwd() {
|
||||
let missing_cwd: McpServerConfig = toml::from_str(
|
||||
r#"
|
||||
command = "echo"
|
||||
environment_id = "remote"
|
||||
required = true
|
||||
"#,
|
||||
)
|
||||
.expect("should deserialize remote MCP config without cwd");
|
||||
let relative_cwd: McpServerConfig = toml::from_str(
|
||||
r#"
|
||||
command = "echo"
|
||||
environment_id = "remote"
|
||||
cwd = "relative"
|
||||
required = true
|
||||
"#,
|
||||
)
|
||||
.expect("should deserialize remote MCP config with relative cwd");
|
||||
let servers = HashMap::from([
|
||||
("missing".to_string(), missing_cwd),
|
||||
("relative".to_string(), relative_cwd),
|
||||
]);
|
||||
|
||||
let check = mcp_check_from_servers(&servers).await;
|
||||
|
||||
assert_eq!(check.status, CheckStatus::Fail);
|
||||
assert!(
|
||||
check
|
||||
.details
|
||||
.contains(&"missing: remote stdio requires an explicit cwd".to_string())
|
||||
);
|
||||
assert!(
|
||||
check
|
||||
.details
|
||||
.contains(&"relative: remote stdio cwd is not absolute (relative)".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn read_probe_file_rejects_unreadable_file() {
|
||||
|
||||
@@ -640,7 +640,7 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
|
||||
let env_display = format_env_display(env.as_ref(), env_vars);
|
||||
let cwd_display = cwd
|
||||
.as_ref()
|
||||
.map(|path| path.display().to_string())
|
||||
.map(ToString::to_string)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
let status = format_mcp_status(cfg);
|
||||
@@ -897,7 +897,7 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re
|
||||
println!(" args: {args_display}");
|
||||
let cwd_display = cwd
|
||||
.as_ref()
|
||||
.map(|path| path.display().to_string())
|
||||
.map(ToString::to_string)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
println!(" cwd: {cwd_display}");
|
||||
|
||||
Reference in New Issue
Block a user