Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion crates/buzz-acp/src/acp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}

Expand Down Expand Up @@ -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)> {
Expand Down
95 changes: 80 additions & 15 deletions desktop/src-tauri/src/commands/agent_discovery/managed_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,56 +16,88 @@ struct ManagedNodeArtifact {
}

#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
const MANAGED_NODE_ARTIFACT: Option<ManagedNodeArtifact> = Some(ManagedNodeArtifact {
const COMPILED_NODE_ARTIFACT: Option<ManagedNodeArtifact> = 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<ManagedNodeArtifact> = Some(ManagedNodeArtifact {
const COMPILED_NODE_ARTIFACT: Option<ManagedNodeArtifact> = 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<ManagedNodeArtifact> = Some(ManagedNodeArtifact {
const COMPILED_NODE_ARTIFACT: Option<ManagedNodeArtifact> = 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<ManagedNodeArtifact> = Some(ManagedNodeArtifact {
const COMPILED_NODE_ARTIFACT: Option<ManagedNodeArtifact> = 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<ManagedNodeArtifact> = 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<ManagedNodeArtifact> = 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<ManagedNodeArtifact> = None;
const COMPILED_NODE_ARTIFACT: Option<ManagedNodeArtifact> = 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<ManagedNodeArtifact> {
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<ManagedNodeArtifact> {
COMPILED_NODE_ARTIFACT
}

fn managed_node_unsupported_step() -> InstallStepResult {
InstallStepResult {
Expand Down Expand Up @@ -129,15 +161,15 @@ 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<InstallStepResult>> {
if managed_node_runtime_ready() {
return Ok(());
}

let Some(artifact) = MANAGED_NODE_ARTIFACT else {
let Some(artifact) = managed_node_artifact() else {
return Err(Box::new(managed_node_unsupported_step()));
};
let Some(root) = crate::managed_agents::buzz_managed_node_root() else {
Expand Down Expand Up @@ -733,6 +765,39 @@ mod tests {
assert!(verify_node_tree(tmp.path()).is_err());
}

/// The artifact resolver must return an artifact whose `platform` string
/// matches the runtime arch. On Windows this exercises the runtime
/// detection path instead of a compile-time constant.
#[test]
fn managed_node_artifact_returns_artifact_matching_runtime_platform() {
let artifact = managed_node_artifact().expect("supported platform must yield artifact");
let runtime_platform = match std::env::consts::OS {
"macos" => 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();
Expand Down
78 changes: 77 additions & 1 deletion desktop/src-tauri/src/managed_agents/managed_node_paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub(crate) fn buzz_managed_node_root() -> Option<PathBuf> {

pub(crate) fn buzz_managed_node_bin_dir() -> Option<PathBuf> {
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")),
Expand All @@ -31,6 +31,55 @@ pub(crate) fn buzz_managed_node_bin_dir() -> Option<PathBuf> {
})
}

/// 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<PathBuf> {
buzz_managed_node_bin_dir().map(|bin| {
#[cfg(windows)]
Expand Down Expand Up @@ -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}"
);
}
}