From d3f2bf743c0eeea0b4ee99f3cba84a8549152bef Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 10 Mar 2026 13:04:32 +0100 Subject: [PATCH 1/3] fix: resolve install script binary verification, uninstall, and version prefix issues - Strip macOS Gatekeeper quarantine attribute before binary verification to prevent "Killed: 9" errors on downloaded binaries - Add binary and npm package removal to uninstall command (previously only cleaned up data/config files, leaving binaries in place) - Fix SDK version tag parsing to strip "openclaw-" prefix, preventing double-prefix download URLs like "vopenclaw-v3.1.18" (404) - Warn when an older npm-installed binary shadows newly installed version Co-Authored-By: Claude Opus 4.6 --- install.sh | 26 +++++++++++++++++ packages/sdk/src/client.ts | 16 +++++++++-- src/cli/lib/core-maintenance.ts | 50 +++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index 4d8a08ef1..98a8eeec7 100755 --- a/install.sh +++ b/install.sh @@ -165,6 +165,7 @@ download_broker_binary() { if curl -fsSL "$download_url" -o "$target_path" 2>/dev/null; then chmod +x "$target_path" + strip_quarantine "$target_path" # Verify binary works (Rust clap binary supports --help) if "$target_path" --help &>/dev/null; then success "Downloaded broker binary (workflow agent spawning)" @@ -217,6 +218,7 @@ download_dashboard_binary() { if gunzip -c "${temp_file}.gz" > "$target_path" 2>/dev/null; then rm -f "${temp_file}.gz" chmod +x "$target_path" + strip_quarantine "$target_path" if "$target_path" --version &>/dev/null; then success "Downloaded standalone dashboard-server binary" @@ -244,6 +246,7 @@ download_dashboard_binary() { if [ "$file_size" -gt 1000000 ]; then chmod +x "$target_path" + strip_quarantine "$target_path" if "$target_path" --version &>/dev/null; then success "Downloaded standalone dashboard-server binary" @@ -325,6 +328,13 @@ has_command() { command -v "$1" &> /dev/null } +# Strip macOS Gatekeeper quarantine attribute from a binary +strip_quarantine() { + if [ "$OS" = "darwin" ] && has_command xattr; then + xattr -d com.apple.quarantine "$1" 2>/dev/null || true + fi +} + # Download relay-acp binary for Zed editor integration download_relay_acp() { step "Downloading relay-acp binary (Zed editor integration)..." @@ -354,6 +364,7 @@ download_relay_acp() { if gunzip -c "${temp_file}.gz" > "$target_path" 2>/dev/null; then rm -f "${temp_file}.gz" chmod +x "$target_path" + strip_quarantine "$target_path" if "$target_path" --help &>/dev/null; then success "Downloaded relay-acp binary (Zed ACP bridge)" @@ -379,6 +390,7 @@ download_relay_acp() { if [ "$file_size" -gt 1000000 ]; then chmod +x "$target_path" + strip_quarantine "$target_path" if "$target_path" --help &>/dev/null; then success "Downloaded relay-acp binary (Zed ACP bridge)" @@ -465,6 +477,7 @@ download_standalone_binary() { if gunzip -c "${temp_file}.gz" > "$target_path" 2>/dev/null; then rm -f "${temp_file}.gz" chmod +x "$target_path" + strip_quarantine "$target_path" # Verify the binary works if "$target_path" --version &>/dev/null; then @@ -501,6 +514,7 @@ download_standalone_binary() { if [ "$file_size" -gt 1000000 ]; then chmod +x "$target_path" + strip_quarantine "$target_path" # Verify the binary works if "$target_path" --version &>/dev/null; then @@ -686,6 +700,18 @@ verify_installation() { if command -v agent-relay &> /dev/null; then local installed_version=$(agent-relay --version 2>/dev/null || echo "unknown") success "agent-relay $installed_version installed successfully!" + + # Warn if another version shadows the one we just installed + local which_path=$(command -v agent-relay) + if [ -x "$BIN_DIR/agent-relay" ] && [ "$which_path" != "$BIN_DIR/agent-relay" ]; then + local other_version=$("$BIN_DIR/agent-relay" --version 2>/dev/null || echo "unknown") + if [ "$installed_version" != "$other_version" ]; then + warn "Another agent-relay ($installed_version) at $which_path shadows the newly installed $other_version at $BIN_DIR/agent-relay" + echo " To fix, either:" + echo " 1. Uninstall the old version: npm uninstall -g agent-relay" + echo " 2. Or ensure $BIN_DIR is earlier in your PATH" + fi + fi elif [ -x "$BIN_DIR/agent-relay" ]; then local installed_version=$("$BIN_DIR/agent-relay" --version 2>/dev/null || echo "unknown") success "agent-relay $installed_version installed to $BIN_DIR" diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index 3ebb5532c..e369ba0a5 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -743,8 +743,10 @@ function getLatestVersionSync(): string | null { timeout: 15_000, stdio: ['pipe', 'pipe', 'pipe'], }).toString(); - const match = result.match(/"tag_name"\s*:\s*"v?([^"]+)"/); - return match?.[1] ?? null; + const match = result.match(/"tag_name"\s*:\s*"([^"]+)"/); + if (!match?.[1]) return null; + // Strip tag prefixes: "openclaw-v3.1.18" -> "3.1.18", "v3.1.18" -> "3.1.18" + return match[1].replace(/^openclaw-/, '').replace(/^v/, ''); } catch { return null; } @@ -784,8 +786,16 @@ function installBrokerBinary(): string { }); fs.chmodSync(targetPath, 0o755); - // macOS: re-sign to avoid Gatekeeper issues + // macOS: strip quarantine attribute and re-sign to avoid Gatekeeper issues if (process.platform === 'darwin') { + try { + execSync(`xattr -d com.apple.quarantine "${targetPath}" 2>/dev/null || true`, { + timeout: 10_000, + stdio: ['pipe', 'pipe', 'pipe'], + }); + } catch { + // Non-fatal + } try { execSync(`codesign --force --sign - "${targetPath}"`, { timeout: 10_000, diff --git a/src/cli/lib/core-maintenance.ts b/src/cli/lib/core-maintenance.ts index ee141509a..27990ffea 100644 --- a/src/cli/lib/core-maintenance.ts +++ b/src/cli/lib/core-maintenance.ts @@ -268,6 +268,56 @@ export async function runUninstallCommand( removeZedConfig(serverName, deps.fs, isDryRun, deps.log); } + // --- Binary removal (standalone binaries + npm packages) --- + const homeDir = os.homedir(); + const standaloneBinDir = path.join(homeDir, '.local', 'bin'); + const installDir = path.join(homeDir, '.agent-relay'); + + // Remove standalone binaries from ~/.local/bin + for (const binaryName of ['agent-relay', 'relay-dashboard-server', 'relay-acp']) { + const binPath = path.join(standaloneBinDir, binaryName); + if (deps.fs.existsSync(binPath)) { + if (isDryRun) { + deps.log(`[dry-run] Would remove binary: ${binPath}`); + } else { + try { + deps.fs.unlinkSync(binPath); + deps.log(`Removed ${binPath}`); + } catch { + // Best-effort. + } + } + } + } + + // Remove broker binary from ~/.agent-relay/bin/ + if (deps.fs.existsSync(installDir)) { + if (isDryRun) { + deps.log(`[dry-run] Would remove directory: ${installDir}`); + } else { + try { + deps.fs.rmSync(installDir, { recursive: true, force: true }); + deps.log(`Removed ${installDir}`); + } catch { + // Best-effort. + } + } + } + + // Remove npm-installed packages + if (!isDryRun) { + for (const pkg of ['agent-relay', '@agent-relay/dashboard-server', '@agent-relay/acp-bridge']) { + try { + await deps.execCommand(`npm uninstall -g ${pkg} 2>/dev/null`); + deps.log(`Uninstalled npm package: ${pkg}`); + } catch { + // Package may not be installed via npm — that's fine. + } + } + } else { + deps.log('[dry-run] Would run: npm uninstall -g agent-relay @agent-relay/dashboard-server @agent-relay/acp-bridge'); + } + // --- Snippet cleanup (CLAUDE.md, GEMINI.md, AGENTS.md) --- if (options.snippets) { for (const fileName of SNIPPET_TARGET_FILES) { From 768241f826416448ff16de52f8837a954b51c3c9 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 10 Mar 2026 13:07:43 +0100 Subject: [PATCH 2/3] fix: remove shell-dependent 2>/dev/null from npm uninstall execCommand The try/catch already handles errors, and the shell redirection is fragile if execCommand ever changes from exec to execFile. Co-Authored-By: Claude Opus 4.6 --- src/cli/lib/core-maintenance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/lib/core-maintenance.ts b/src/cli/lib/core-maintenance.ts index 27990ffea..1f39c6a40 100644 --- a/src/cli/lib/core-maintenance.ts +++ b/src/cli/lib/core-maintenance.ts @@ -308,7 +308,7 @@ export async function runUninstallCommand( if (!isDryRun) { for (const pkg of ['agent-relay', '@agent-relay/dashboard-server', '@agent-relay/acp-bridge']) { try { - await deps.execCommand(`npm uninstall -g ${pkg} 2>/dev/null`); + await deps.execCommand(`npm uninstall -g ${pkg}`); deps.log(`Uninstalled npm package: ${pkg}`); } catch { // Package may not be installed via npm — that's fine. From 476a44a5ed52bb97f7122c3aa9f9a371cd718fd5 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 10 Mar 2026 13:08:29 +0100 Subject: [PATCH 3/3] fix: only remove ~/.agent-relay/bin/ not entire ~/.agent-relay directory The ~/.agent-relay directory is the global data dir (GLOBAL_BASE_DIR) which stores telemetry preferences, dashboard files, and legacy project data. Only the bin/ subdirectory contains installer-managed binaries. Co-Authored-By: Claude Opus 4.6 --- src/cli/lib/core-maintenance.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cli/lib/core-maintenance.ts b/src/cli/lib/core-maintenance.ts index 1f39c6a40..56372ad4b 100644 --- a/src/cli/lib/core-maintenance.ts +++ b/src/cli/lib/core-maintenance.ts @@ -271,7 +271,7 @@ export async function runUninstallCommand( // --- Binary removal (standalone binaries + npm packages) --- const homeDir = os.homedir(); const standaloneBinDir = path.join(homeDir, '.local', 'bin'); - const installDir = path.join(homeDir, '.agent-relay'); + const installBinDir = path.join(homeDir, '.agent-relay', 'bin'); // Remove standalone binaries from ~/.local/bin for (const binaryName of ['agent-relay', 'relay-dashboard-server', 'relay-acp']) { @@ -290,14 +290,14 @@ export async function runUninstallCommand( } } - // Remove broker binary from ~/.agent-relay/bin/ - if (deps.fs.existsSync(installDir)) { + // Remove broker binary from ~/.agent-relay/bin/ (not the parent dir which stores global data) + if (deps.fs.existsSync(installBinDir)) { if (isDryRun) { - deps.log(`[dry-run] Would remove directory: ${installDir}`); + deps.log(`[dry-run] Would remove directory: ${installBinDir}`); } else { try { - deps.fs.rmSync(installDir, { recursive: true, force: true }); - deps.log(`Removed ${installDir}`); + deps.fs.rmSync(installBinDir, { recursive: true, force: true }); + deps.log(`Removed ${installBinDir}`); } catch { // Best-effort. }