mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 08:04:25 -10:00
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:
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);
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user