fix(windows): strip \\?\ from reviewed MCP child argv/cwd

After the LPAC realpath fallback, native_mcp activation succeeds on Windows
and exposes the next failure: freeze_plugin_stdio_paths hands Path::canonicalize
verbatim paths (\\?\C:\...) to Node as the peer.mjs argv/cwd. Node's ESM loader
exits 1 before handshake, so connect_all reports Connection closed / exit code 1.
Reviewed stderr is scrubbed, so the job log only showed the exit status.

Strip the verbatim prefix at the BrokerSession spawn boundary only —
containment checks keep the prefixed form. Same rule as runtime_api job cwd
and Git for Windows. Also make the mixed_native_graphs scope.path assertion
separator-agnostic via Path::ends_with (Windows keeps backslashes).

Evidence: plain_windows_child_path unit coverage; standalone helper check on
this box. Hosted Windows Test is the LPAC proof.

Signed-off-by: CodeWhale Bot <[email protected]>
This commit is contained in:
CodeWhale Bot authored and Hunter Bown committed 2026-10-05 11:50:58 -07:00
1 parent f600c9ebf3
commit 3684e2a776
3 files changed
+89 -17

No files matched your search

@@ -9,6 +9,7 @@ use crate::extension_host::{ExtensionHostManager, HostAttachment, TestManagerGua
use crate::mcp::{McpBackend, McpPool};
use crate::plugins::activation::TestPolicyGuard;
use serde_json::json;
use std::path::Path;
use std::time::Duration;
fn selected(fixture: &FixturePlugins, tag: &str) -> Arc<PluginRegistry> {
@@ -112,21 +113,18 @@ async fn mixed_native_graphs_use_one_core_catalog_and_selected_child_pool_on_bot
);
// Same owner and public names, distinct exact selected entry handles.
assert_eq!(a_defs[0].0, b_defs[0].0);
// Path::ends_with is separator-agnostic. A string suffix of "native/a.mjs"
// fails on Windows because scope paths keep backslashes (and may carry a
// verbatim prefix from canonicalize).
assert!(
a_defs[0]
.3
.registration
.scope
.path
.ends_with("native/a.mjs")
Path::new(&a_defs[0].3.registration.scope.path).ends_with(Path::new("native/a.mjs")),
"{}",
a_defs[0].3.registration.scope.path
);
assert!(
b_defs[0]
.3
.registration
.scope
.path
.ends_with("native/b.mjs")
Path::new(&b_defs[0].3.registration.scope.path).ends_with(Path::new("native/b.mjs")),
"{}",
b_defs[0].3.registration.scope.path
);
for backend in [McpBackend::Rust, McpBackend::Host] {
let mut parent = pool(&fixture, &a, backend);
+63
View File
@@ -7015,6 +7015,43 @@ fn resolve_project_mcp_cwd(workspace: &Path, cwd: Option<&Path>) -> Result<PathB
Ok(resolved)
}
/// Drop the Win32 verbatim prefix before handing a path to an external child
/// (Node MCP peers, etc.). `Path::canonicalize` returns `\\?\C:\...`; Node's
/// ESM loader and several Windows tools refuse or mis-handle that spelling for
/// ordinary paths under MAX_PATH. Containment checks keep the prefixed form;
/// only argv and current_dir are rewritten. Same rule as runtime_api job cwd
/// and Git for Windows.
#[cfg(any(windows, test))]
fn plain_windows_child_path(path: &str) -> String {
if let Some(rest) = path.strip_prefix(r"\\?\UNC\") {
return format!(r"\\{rest}");
}
match path.strip_prefix(r"\\?\") {
Some(rest)
if rest.as_bytes().first().is_some_and(u8::is_ascii_alphabetic)
&& rest.as_bytes().get(1) == Some(&b':') =>
{
rest.to_string()
}
_ => path.to_string(),
}
}
#[cfg(windows)]
pub(crate) fn strip_windows_verbatim_for_child(
path: impl AsRef<std::ffi::OsStr>,
) -> std::ffi::OsString {
let raw = path.as_ref().to_string_lossy();
std::ffi::OsString::from(plain_windows_child_path(&raw))
}
#[cfg(not(windows))]
pub(crate) fn strip_windows_verbatim_for_child(
path: impl AsRef<std::ffi::OsStr>,
) -> std::ffi::OsString {
path.as_ref().to_os_string()
}
fn normalize_path_components(path: &Path) -> PathBuf {
let mut normalized = PathBuf::new();
for component in path.components() {
@@ -7656,6 +7693,32 @@ fn snapshot_from_config(
}
}
#[cfg(test)]
mod windows_child_path_tests {
use super::plain_windows_child_path;
#[test]
fn plain_windows_child_path_drops_only_the_verbatim_prefix() {
assert_eq!(
plain_windows_child_path(r"\\?\C:\ws\peer.mjs"),
r"C:\ws\peer.mjs"
);
assert_eq!(
plain_windows_child_path(r"\\?\UNC\host\share\peer.mjs"),
r"\\host\share\peer.mjs"
);
for unchanged in [
r"C:\ws\peer.mjs",
r"\\host\share",
r"\\?\Volume{0}\ws",
"/tmp/peer.mjs",
"peer.mjs",
] {
assert_eq!(plain_windows_child_path(unchanged), unchanged);
}
}
}
#[cfg(test)]
mod qualified_plugin_server_name_tests {
use super::{qualified_plugin_server_name, split_qualified_plugin_server_name};
+16 -5
View File
@@ -108,15 +108,26 @@ impl BrokerSession {
} else {
None
};
// Windows: Path::canonicalize yields a verbatim prefix. Node's ESM loader
// (and several other Windows tools) refuse that spelling for ordinary
// paths under MAX_PATH, so the reviewed peer exits 1 before handshake.
// Strip only at the child boundary; containment kept the prefixed form.
let mut cmd = reviewed_launch.as_ref().map_or_else(
|| {
let mut command_process = tokio::process::Command::new(command);
command_process.args(&config.args);
let mut command_process =
tokio::process::Command::new(super::strip_windows_verbatim_for_child(command));
for arg in &config.args {
command_process.arg(super::strip_windows_verbatim_for_child(arg.as_str()));
}
command_process
},
|launch| {
let mut command_process = tokio::process::Command::new(&launch.command);
command_process.args(&launch.args);
let mut command_process = tokio::process::Command::new(
super::strip_windows_verbatim_for_child(&launch.command),
);
for arg in &launch.args {
command_process.arg(super::strip_windows_verbatim_for_child(arg));
}
command_process
},
);
@@ -130,7 +141,7 @@ impl BrokerSession {
.and_then(|launch| launch.cwd.as_ref())
.or(config.cwd.as_ref().filter(|_| reviewed_launch.is_none()));
if let Some(cwd) = launch_cwd {
cmd.current_dir(cwd);
cmd.current_dir(super::strip_windows_verbatim_for_child(cwd.as_os_str()));
}
#[cfg(unix)]
if let Some(cwd_fd) = reviewed_launch