diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index d27a152bb5f..d882f5821a1 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -528,6 +528,27 @@ Upgrade NemoClaw to a version that supports your OpenShell release, or install a For fresh installs, NemoClaw passes the blueprint range to `install-openshell.sh` and resolves a compatible published OpenShell release before downloading. If GitHub release metadata is unavailable, the script uses its bundled fallback pin and the post-install gate still enforces the configured range. +### Installer Reports an OpenShell Gateway Version Mismatch + +On Linux, an existing OpenShell package can provide a systemd user service that starts a different gateway version from the user-local version that NemoClaw installs. +The installer stops before onboarding instead of using the two versions together. +The error reports both gateway versions and binary paths. + + +Do not remove the existing OpenShell package if its gateway manages resources outside NemoClaw. +Package removal can stop that gateway. +Align the package with the version in the installer error, or plan the migration of those resources first. + + +If you no longer need the APT-installed OpenShell package, remove it and rerun the installer: + +```bash +sudo apt remove openshell +curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash +``` + +The next installer run must continue past the OpenShell installation step without reporting a version mismatch. + ### Sandbox build fails during OpenClaw plugin install diff --git a/scripts/install.sh b/scripts/install.sh index 32b7074c10c..77b3eb118b3 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1318,6 +1318,50 @@ upstream_openshell_gateway_user_service_installed() { || [[ -f /lib/systemd/user/openshell-gateway.service ]] } +resolve_upstream_openshell_gateway_bin_for_service() { + local exec_start gateway_bin + local -a gateway_bins=() + exec_start="$(systemctl --user show openshell-gateway.service --property=ExecStart --value 2>/dev/null)" \ + || return 1 + while IFS= read -r gateway_bin; do + gateway_bins+=("$gateway_bin") + done < <( + printf '%s\n' "$exec_start" \ + | grep -oE 'path=[^ ;}]+' \ + | sed 's/^path=//' \ + | sort -u + ) + [[ "${#gateway_bins[@]}" -eq 1 ]] || return 1 + gateway_bin="${gateway_bins[0]}" + [[ "$gateway_bin" == /*/openshell-gateway && -x "$gateway_bin" ]] || return 1 + printf '%s\n' "$gateway_bin" +} + +openshell_binary_version() { + local binary="${1:-}" version_output + [[ -x "$binary" ]] || return 1 + version_output="$("$binary" --version 2>/dev/null)" || return 1 + printf '%s\n' "$version_output" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 +} + +require_compatible_upstream_openshell_gateway_service() { + local nemoclaw_gateway_bin upstream_gateway_bin nemoclaw_version upstream_version + nemoclaw_gateway_bin="$(resolve_openshell_gateway_bin_for_service)" \ + || error "Could not locate the NemoClaw OpenShell gateway binary before checking the existing upstream service." + if ! trusted_openshell_gateway_bin_for_service "$nemoclaw_gateway_bin"; then + error "OpenShell gateway user service binary path is not a trusted install path: $nemoclaw_gateway_bin" + fi + upstream_gateway_bin="$(resolve_upstream_openshell_gateway_bin_for_service)" \ + || error "Could not locate the gateway binary used by the existing upstream OpenShell user service. Remove or repair that OpenShell installation, then rerun the installer." + nemoclaw_version="$(openshell_binary_version "$nemoclaw_gateway_bin")" \ + || error "Could not determine the NemoClaw OpenShell gateway version at $nemoclaw_gateway_bin." + upstream_version="$(openshell_binary_version "$upstream_gateway_bin")" \ + || error "Could not determine the existing upstream OpenShell gateway version at $upstream_gateway_bin." + if [[ "$nemoclaw_version" != "$upstream_version" ]]; then + error "OpenShell gateway version mismatch: NemoClaw installed ${nemoclaw_version} at ${nemoclaw_gateway_bin}, but the existing upstream user service uses ${upstream_version} at ${upstream_gateway_bin}. Align or remove the upstream OpenShell package (for apt installs: sudo apt remove openshell), then rerun the installer." + fi +} + macos_openshell_homebrew_gateway_service_installed() { [[ "$(uname -s)" == "Darwin" ]] || return 1 command -v brew >/dev/null 2>&1 || return 1 @@ -1386,6 +1430,7 @@ install_nemoclaw_openshell_gateway_user_service() { if [[ -f "$service_path" ]] && ! is_nemoclaw_openshell_gateway_user_service "$service_path"; then error "Refusing to replace non-NemoClaw OpenShell gateway user service: $service_path" fi + require_compatible_upstream_openshell_gateway_service info "OpenShell upstream gateway user service is staged; onboarding will select and start it." return 0 fi diff --git a/test/install-openshell-gateway-service.test.ts b/test/install-openshell-gateway-service.test.ts index 95abed95a4f..f14b15912ec 100644 --- a/test/install-openshell-gateway-service.test.ts +++ b/test/install-openshell-gateway-service.test.ts @@ -139,16 +139,30 @@ describe("install.sh OpenShell gateway service", () => { expect(fs.existsSync(servicePath(home))).toBe(false); }); - it("defers a marked unit when an upstream service exists (#6903)", () => { + it("defers a marked unit when the upstream service version matches (#6903)", () => { const home = makeTempRoot(); const unitPath = servicePath(home); + const nemoclawGatewayBin = userGatewayBin(home); + const upstreamGatewayBin = path.join(home, "usr", "bin", "openshell-gateway"); fs.mkdirSync(path.dirname(unitPath), { recursive: true }); fs.writeFileSync(unitPath, "# NEMOCLAW_MANAGED_OPENSHELL_GATEWAY=1\n"); + fs.mkdirSync(path.dirname(upstreamGatewayBin), { recursive: true }); + writeExecutable( + nemoclawGatewayBin, + "#!/usr/bin/env bash\nprintf 'openshell-gateway 0.0.85\\n'\n", + ); + writeExecutable( + upstreamGatewayBin, + "#!/usr/bin/env bash\nprintf 'openshell-gateway 0.0.85\\n'\n", + ); const result = runInstallHelper( home, [ "upstream_openshell_gateway_user_service_installed() { return 0; }", + `resolve_openshell_gateway_bin_for_service() { printf '%s\\n' ${JSON.stringify(nemoclawGatewayBin)}; }`, + `resolve_upstream_openshell_gateway_bin_for_service() { printf '%s\\n' ${JSON.stringify(upstreamGatewayBin)}; }`, + "trusted_openshell_gateway_bin_for_service() { return 0; }", "install_nemoclaw_openshell_gateway_user_service", ].join("\n"), ); @@ -158,6 +172,64 @@ describe("install.sh OpenShell gateway service", () => { expect(result.stdout).toContain("upstream gateway user service is staged"); }); + it("stops before onboarding when the upstream service version differs (#8051)", () => { + const home = makeTempRoot(); + const nemoclawGatewayBin = userGatewayBin(home); + const upstreamGatewayBin = path.join(home, "usr", "bin", "openshell-gateway"); + fs.mkdirSync(path.dirname(upstreamGatewayBin), { recursive: true }); + writeExecutable( + nemoclawGatewayBin, + "#!/usr/bin/env bash\nprintf 'openshell-gateway 0.0.85\\n'\n", + ); + writeExecutable( + upstreamGatewayBin, + "#!/usr/bin/env bash\nprintf 'openshell-gateway 0.0.91\\n'\n", + ); + + const result = runInstallHelper( + home, + [ + "upstream_openshell_gateway_user_service_installed() { return 0; }", + `resolve_openshell_gateway_bin_for_service() { printf '%s\\n' ${JSON.stringify(nemoclawGatewayBin)}; }`, + `resolve_upstream_openshell_gateway_bin_for_service() { printf '%s\\n' ${JSON.stringify(upstreamGatewayBin)}; }`, + "trusted_openshell_gateway_bin_for_service() { return 0; }", + "install_nemoclaw_openshell_gateway_user_service", + ].join("\n"), + ); + + expect(result.status).toBe(1); + expect(result.stderr).toContain("OpenShell gateway version mismatch"); + expect(result.stderr).toContain("0.0.85"); + expect(result.stderr).toContain("0.0.91"); + expect(result.stderr).toContain("sudo apt remove openshell"); + expect(result.stdout).not.toContain("upstream gateway user service is staged"); + expect(fs.existsSync(servicePath(home))).toBe(false); + }); + + it("resolves the gateway from the effective upstream ExecStart (#8051)", () => { + const home = makeTempRoot(); + const conventionalGatewayBin = path.join(home, "usr", "bin", "openshell-gateway"); + const overriddenGatewayBin = path.join(home, "opt", "openshell", "openshell-gateway"); + const systemctlBin = path.join(home, "systemctl-bin"); + fs.mkdirSync(path.dirname(conventionalGatewayBin), { recursive: true }); + fs.mkdirSync(path.dirname(overriddenGatewayBin), { recursive: true }); + fs.mkdirSync(systemctlBin); + writeExecutable(conventionalGatewayBin, "#!/usr/bin/env bash\nexit 0\n"); + writeExecutable(overriddenGatewayBin, "#!/usr/bin/env bash\nexit 0\n"); + writeExecutable( + path.join(systemctlBin, "systemctl"), + `#!/usr/bin/env bash\nprintf '{ path=${overriddenGatewayBin} ; argv[]=${overriddenGatewayBin} ; ignore_errors=no ; }\\n'\n`, + ); + + const result = runInstallHelper(home, "resolve_upstream_openshell_gateway_bin_for_service", { + PATH: `${systemctlBin}:${path.dirname(process.execPath)}:${TEST_SYSTEM_PATH}`, + }); + + expect(result.status).toBe(0); + expect(result.stdout.trim()).toBe(overriddenGatewayBin); + expect(result.stdout).not.toContain(conventionalGatewayBin); + }); + it("does not overwrite a foreign unit at the NemoClaw path (#6903)", () => { const home = makeTempRoot(); const unitPath = servicePath(home);