From 27c475f7fd12a249dd47b80e838d134759363fea Mon Sep 17 00:00:00 2001 From: iroiro147 Date: Sat, 1 Aug 2026 19:29:53 +0530 Subject: [PATCH] fix(managed-node,acp): use runtime arch on Windows + preserve error.data Fix two compounding defects breaking Windows ARM64 hosts (issue #4069): 1. **Runtime architecture detection for managed Node provisioning** - Previously, both `managed_node_paths.rs` and `agent_discovery/managed_node.rs` used `std::env::consts::ARCH` (compile-time) to select the Node platform. An x64-built Buzz binary running under Windows x64 emulation on ARM64 hardware always reports `x86_64`, so it downloaded and installed win-x64 Node. The agent adapter's npm packages (installed under the system ARM64 Node) were then mismatched, and the launch failed with `claude.exe` not found. - Fix: on Windows, resolve the actual machine architecture at runtime via `IsWow64Process2`; fall back to compile-time arch if the API is missing or fails. Add unit tests covering the runtime resolver and the artifact-selection path. Non-Windows behavior is unchanged (compile-time arch remains authoritative). 2. **Preserve JSON-RPC `error.data` in `agent_error_from_json`** - Previously, when the adapter returned `{code, message, data}`, `data` was silently dropped if `message` was a string. The actionable payload ("Claude native binary not found for win32-x64...") was hidden from the user. - Fix: append the serialized `data` payload to the message when both fields are present. Add a unit test covering this case. Signed-off-by: Sarthak Singh --- crates/buzz-acp/src/acp.rs | 33 ++++++- .../commands/agent_discovery/managed_node.rs | 95 ++++++++++++++++--- .../src/managed_agents/managed_node_paths.rs | 78 ++++++++++++++- 3 files changed, 189 insertions(+), 17 deletions(-) diff --git a/crates/buzz-acp/src/acp.rs b/crates/buzz-acp/src/acp.rs index 8a698954a0..55a32fae6e 100644 --- a/crates/buzz-acp/src/acp.rs +++ b/crates/buzz-acp/src/acp.rs @@ -112,12 +112,20 @@ pub enum AcpError { /// preserving the numeric code. When the `message` field is missing or /// non-string, fall back to the full JSON object so provider-specific /// detail (e.g. a `data` field) is not lost. +/// +/// When both `message` and `data` are present, append the serialized +/// `data` payload to the message so actionable detail (e.g. "Claude +/// native binary not found for win32-x64...") is not dropped. fn agent_error_from_json(error: &serde_json::Value) -> AcpError { let code = error.get("code").and_then(|c| c.as_i64()).unwrap_or(-32000); - let message = match error.get("message").and_then(|m| m.as_str()) { + let base_message = match error.get("message").and_then(|m| m.as_str()) { Some(m) => m.to_string(), None => error.to_string(), }; + let message = match error.get("data") { + Some(data) if !data.is_null() => format!("{base_message} {}", data), + _ => base_message, + }; AcpError::AgentError { code, message } } @@ -4261,6 +4269,29 @@ mod tests { } } + #[test] + fn agent_error_from_json_appends_data_when_message_and_data_present() { + let error = serde_json::json!({ + "code": -32603, + "message": "Internal error", + "data": "Claude native binary not found for win32-x64: install win32-arm64 build" + }); + match super::agent_error_from_json(&error) { + AcpError::AgentError { code, message } => { + assert_eq!(code, -32603); + assert!( + message.contains("Internal error"), + "expected base message preserved, got: {message}" + ); + assert!( + message.contains("win32-x64"), + "expected data payload preserved, got: {message}" + ); + } + other => panic!("expected AgentError, got {other:?}"), + } + } + // ── build_codex_config_env ──────────────────────────────────────────────── fn env(pairs: &[(&str, &str)]) -> Vec<(String, String)> { diff --git a/desktop/src-tauri/src/commands/agent_discovery/managed_node.rs b/desktop/src-tauri/src/commands/agent_discovery/managed_node.rs index 72108f0291..eccb37a3f2 100644 --- a/desktop/src-tauri/src/commands/agent_discovery/managed_node.rs +++ b/desktop/src-tauri/src/commands/agent_discovery/managed_node.rs @@ -16,56 +16,88 @@ struct ManagedNodeArtifact { } #[cfg(all(target_os = "macos", target_arch = "aarch64"))] -const MANAGED_NODE_ARTIFACT: Option = Some(ManagedNodeArtifact { +const COMPILED_NODE_ARTIFACT: Option = Some(ManagedNodeArtifact { platform: "darwin-arm64", filename: "node-v24.18.0-darwin-arm64.tar.gz", sha256: "e1a97e14c99c803e96c7339403282ea05a499c32f8d83defe9ef5ec66f979ed1", }); #[cfg(all(target_os = "macos", target_arch = "x86_64"))] -const MANAGED_NODE_ARTIFACT: Option = Some(ManagedNodeArtifact { +const COMPILED_NODE_ARTIFACT: Option = Some(ManagedNodeArtifact { platform: "darwin-x64", filename: "node-v24.18.0-darwin-x64.tar.gz", sha256: "dfd0dbd3e721503434df7b7205e719f61b3a3a31b2bcf9729b8b91fea240f080", }); #[cfg(all(target_os = "linux", target_arch = "x86_64"))] -const MANAGED_NODE_ARTIFACT: Option = Some(ManagedNodeArtifact { +const COMPILED_NODE_ARTIFACT: Option = Some(ManagedNodeArtifact { platform: "linux-x64", filename: "node-v24.18.0-linux-x64.tar.gz", sha256: "783130984963db7ba9cbd01089eaf2c2efb055c7c1693c943174b967b3050cb8", }); #[cfg(all(target_os = "linux", target_arch = "aarch64"))] -const MANAGED_NODE_ARTIFACT: Option = Some(ManagedNodeArtifact { +const COMPILED_NODE_ARTIFACT: Option = Some(ManagedNodeArtifact { platform: "linux-arm64", filename: "node-v24.18.0-linux-arm64.tar.gz", sha256: "6b4484c2190274175df9aa8f28e2d758a819cb1c1fe6ab481e2f95b463ab8508", }); -#[cfg(all(target_os = "windows", target_arch = "x86_64"))] -const MANAGED_NODE_ARTIFACT: Option = Some(ManagedNodeArtifact { +/// Windows Node.js artifact metadata for both supported architectures. +/// +/// Unlike macOS/Linux, the artifact cannot be selected at compile time via +/// `cfg(target_arch = ...)`: an x64-built Buzz binary running under Windows +/// x64 emulation on ARM64 hardware always reports `target_arch = "x86_64"`, +/// yet the system (ARM64) Node installed the agent's npm packages with +/// ARM64 native modules. We therefore resolve the correct artifact at +/// runtime from the actual machine architecture (see +/// `crate::managed_agents::target_arch`). +#[cfg(target_os = "windows")] +const WIN_X64_ARTIFACT: ManagedNodeArtifact = ManagedNodeArtifact { platform: "win-x64", filename: "node-v24.18.0-win-x64.zip", sha256: "0ae68406b42d7725661da979b1403ec9926da205c6770827f33aac9d8f26e821", -}); +}; -#[cfg(all(target_os = "windows", target_arch = "aarch64"))] -const MANAGED_NODE_ARTIFACT: Option = Some(ManagedNodeArtifact { +#[cfg(target_os = "windows")] +const WIN_ARM64_ARTIFACT: ManagedNodeArtifact = ManagedNodeArtifact { platform: "win-arm64", filename: "node-v24.18.0-win-arm64.zip", sha256: "f274669adb93b1fd0fbf8f21fd078609e9dcc84333d4f2718d2dde3f9a161a01", -}); +}; #[cfg(not(any( all(target_os = "macos", target_arch = "aarch64"), all(target_os = "macos", target_arch = "x86_64"), all(target_os = "linux", target_arch = "x86_64"), all(target_os = "linux", target_arch = "aarch64"), - all(target_os = "windows", target_arch = "x86_64"), - all(target_os = "windows", target_arch = "aarch64") + target_os = "windows" )))] -const MANAGED_NODE_ARTIFACT: Option = None; +const COMPILED_NODE_ARTIFACT: Option = None; + +/// Resolve the Node.js artifact to provision. +/// +/// On non-Windows platforms the compile-time target is authoritative, so we +/// return the const selected by `cfg`. On Windows we must detect the *actual* +/// machine architecture at runtime: an x64 Buzz build running under Windows +/// x64 emulation on ARM64 hardware reports `target_arch = "x86_64"` yet the +/// system-installed Node and the agent's npm native modules are ARM64. +/// Provisioning win-x64 in that case yields a `claude.exe` that does not +/// exist for the ARM64 npm tree. Windows therefore defers to +/// [`crate::managed_agents::target_arch`], which uses `IsWow64Process2`. +#[cfg(target_os = "windows")] +fn managed_node_artifact() -> Option { + match crate::managed_agents::target_arch() { + "aarch64" => Some(WIN_ARM64_ARTIFACT), + "x86_64" => Some(WIN_X64_ARTIFACT), + _ => None, + } +} + +#[cfg(not(target_os = "windows"))] +fn managed_node_artifact() -> Option { + COMPILED_NODE_ARTIFACT +} fn managed_node_unsupported_step() -> InstallStepResult { InstallStepResult { @@ -129,7 +161,7 @@ fn managed_node_install_lock() -> &'static Mutex<()> { } pub(super) fn managed_node_runtime_supported() -> bool { - MANAGED_NODE_ARTIFACT.is_some() && crate::managed_agents::buzz_managed_node_bin_dir().is_some() + managed_node_artifact().is_some() && crate::managed_agents::buzz_managed_node_bin_dir().is_some() } pub(super) fn ensure_managed_node_runtime_blocking() -> Result<(), Box> { @@ -137,7 +169,7 @@ pub(super) fn ensure_managed_node_runtime_blocking() -> Result<(), Box match crate::managed_agents::target_arch() { + "aarch64" => "darwin-arm64", + "x86_64" => "darwin-x64", + other => panic!("unexpected macos arch: {other}"), + }, + "linux" => match crate::managed_agents::target_arch() { + "x86_64" => "linux-x64", + "aarch64" => "linux-arm64", + other => panic!("unexpected linux arch: {other}"), + }, + "windows" => match crate::managed_agents::target_arch() { + "x86_64" => "win-x64", + "aarch64" => "win-arm64", + other => panic!("unexpected windows arch: {other}"), + }, + other => panic!("unsupported os: {other}"), + }; + assert_eq!( + artifact.platform, + runtime_platform, + "artifact platform must match runtime platform (compile-time arch: {}, runtime arch: {})", + std::env::consts::ARCH, + crate::managed_agents::target_arch() + ); + } + #[test] fn test_verify_node_tree_windows_layout_missing_npm_shim_fails() { let tmp = tempfile::TempDir::new().unwrap(); diff --git a/desktop/src-tauri/src/managed_agents/managed_node_paths.rs b/desktop/src-tauri/src/managed_agents/managed_node_paths.rs index ffcdea8181..6da68d7d36 100644 --- a/desktop/src-tauri/src/managed_agents/managed_node_paths.rs +++ b/desktop/src-tauri/src/managed_agents/managed_node_paths.rs @@ -12,7 +12,7 @@ pub(crate) fn buzz_managed_node_root() -> Option { pub(crate) fn buzz_managed_node_bin_dir() -> Option { let (platform, bin_subdir): (&str, Option<&str>) = - match (std::env::consts::OS, std::env::consts::ARCH) { + match (std::env::consts::OS, target_arch()) { ("macos", "aarch64") => ("darwin-arm64", Some("bin")), ("macos", "x86_64") => ("darwin-x64", Some("bin")), ("linux", "x86_64") => ("linux-x64", Some("bin")), @@ -31,6 +31,55 @@ pub(crate) fn buzz_managed_node_bin_dir() -> Option { }) } +/// Detect the actual target architecture at runtime. +/// +/// On Windows ARM64, an x64-built Buzz binary runs under emulation with +/// `std::env::consts::ARCH` returning "x86_64" (compile-time constant). +/// However, npm packages are installed with the system (ARM64) Node's +/// native modules, so we must provision win-arm64 Node to match. +/// +/// On non-Windows platforms, compile-time detection is correct. +#[cfg(target_os = "windows")] +pub(crate) fn target_arch() -> &'static str { + windows_runtime_arch() +} + +#[cfg(not(target_os = "windows"))] +pub(crate) fn target_arch() -> &'static str { + std::env::consts::ARCH +} + +/// IMAGE_FILE_MACHINE_* constants (stable Win32 ABI values). Defined locally +/// because the `windows-sys` feature gate owning them is off in this crate. +#[cfg(target_os = "windows")] +const IMAGE_FILE_MACHINE_ARM64: u16 = 0xAA64; +#[cfg(target_os = "windows")] +const IMAGE_FILE_MACHINE_AMD64: u16 = 0x8664; + +/// On Windows, detect the actual machine architecture using IsWow64Process2. +/// Returns "aarch64" for ARM64 machines, "x86_64" otherwise. Any failure +/// (function missing pre-Win10-1709, call error) falls back to the +/// compile-time arch so behavior is unchanged from before this fix. +#[cfg(target_os = "windows")] +fn windows_runtime_arch() -> &'static str { + use windows_sys::Win32::System::Threading::{GetCurrentProcess, IsWow64Process2}; + + let mut process_machine: u16 = 0; + let mut native_machine: u16 = 0; + + unsafe { + if IsWow64Process2(GetCurrentProcess(), &mut process_machine, &mut native_machine) != 0 { + match native_machine { + IMAGE_FILE_MACHINE_ARM64 => return "aarch64", + IMAGE_FILE_MACHINE_AMD64 => return "x86_64", + _ => return std::env::consts::ARCH, + } + } + } + + std::env::consts::ARCH +} + pub(crate) fn buzz_managed_node_bin_path() -> Option { buzz_managed_node_bin_dir().map(|bin| { #[cfg(windows)] @@ -99,3 +148,30 @@ fn is_executable_file(path: &std::path::Path) -> bool { true } } + +#[cfg(test)] +mod tests { + use super::*; + + /// On non-Windows platforms, runtime arch detection must defer to the + /// compile-time value (cf. `windows_runtime_arch()` on Windows). + #[cfg(not(target_os = "windows"))] + #[test] + fn target_arch_returns_compile_time_value_on_non_windows() { + assert_eq!(target_arch(), std::env::consts::ARCH); + } + + /// On Windows, runtime arch detection must return a supported value and + /// never panic. On x64 hardware under native execution it must return + /// `"x86_64"`; on ARM64 hardware under emulation it must return `"aarch64"`. + #[cfg(target_os = "windows")] + #[test] + fn target_arch_returns_known_value_on_windows() { + let arch = target_arch(); + assert!( + matches!(arch, "x86_64" | "aarch64"), + "unexpected runtime arch: {arch}" +, "unexpected runtime arch: {arch}" + ); + } +}