From 42525312d4d52390fafa1700332f16364dbb77b6 Mon Sep 17 00:00:00 2001 From: Deepak Jain Date: Fri, 3 Apr 2026 09:52:56 -0700 Subject: [PATCH 1/3] fix(onboard): upgrade stale openshell during preflight Fixes #1404 Signed-off-by: Deepak Jain --- bin/lib/onboard.js | 54 +++++++++++++++++++++-- scripts/install-openshell.sh | 26 +++++++++++- test/install-openshell.test.js | 78 ++++++++++++++++++++++++++++++++++ test/onboard.test.js | 6 +++ 4 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 test/install-openshell.test.js diff --git a/bin/lib/onboard.js b/bin/lib/onboard.js index 5ab4e8629..03dc36a60 100644 --- a/bin/lib/onboard.js +++ b/bin/lib/onboard.js @@ -438,6 +438,32 @@ function getInstalledOpenshellVersion(versionOutput = null) { return match[1]; } +function compareVersions(left = "0.0.0", right = "0.0.0") { + const lhs = String(left).split(".").map((part) => Number.parseInt(part, 10) || 0); + const rhs = String(right).split(".").map((part) => Number.parseInt(part, 10) || 0); + const length = Math.max(lhs.length, rhs.length); + for (let index = 0; index < length; index += 1) { + const a = lhs[index] || 0; + const b = rhs[index] || 0; + if (a > b) return 1; + if (a < b) return -1; + } + return 0; +} + +function getMinimumOpenshellVersion(blueprintText = null) { + const rawBlueprint = blueprintText == null + ? fs.readFileSync(path.join(ROOT, "nemoclaw-blueprint", "blueprint.yaml"), "utf-8") + : String(blueprintText); + const match = rawBlueprint.match(/^\s*min_openshell_version:\s*"?(?[0-9]+\.[0-9]+\.[0-9]+)"?\s*$/m); + return match?.groups?.version || null; +} + +function shouldUpgradeOpenshell(installedVersion = null, minimumVersion = null) { + if (!installedVersion || !minimumVersion) return false; + return compareVersions(installedVersion, minimumVersion) < 0; +} + function getStableGatewayImageRef(versionOutput = null) { const version = getInstalledOpenshellVersion(versionOutput); if (!version) return null; @@ -1594,10 +1620,14 @@ function getPortConflictServiceHints(platform = process.platform) { ]; } -function installOpenshell() { +function installOpenshell(opts = {}) { + const env = { ...process.env }; + if (opts.minimumVersion) { + env.NEMOCLAW_MIN_OPENSHELL_VERSION = opts.minimumVersion; + } const result = spawnSync("bash", [path.join(SCRIPTS, "install-openshell.sh")], { cwd: ROOT, - env: process.env, + env, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8", timeout: 300_000, @@ -1746,14 +1776,30 @@ async function preflight() { // OpenShell CLI let openshellInstall = { localBin: null, futureShellPathHint: null }; + const requiredOpenshellVersion = getMinimumOpenshellVersion(); + const installedOpenshellVersion = isOpenshellInstalled() + ? getInstalledOpenshellVersion() + : null; if (!isOpenshellInstalled()) { console.log(" openshell CLI not found. Installing..."); - openshellInstall = installOpenshell(); + openshellInstall = installOpenshell({ minimumVersion: requiredOpenshellVersion }); if (!openshellInstall.installed) { console.error(" Failed to install openshell CLI."); console.error(" Install manually: https://github.com/NVIDIA/OpenShell/releases"); process.exit(1); } + } else if (shouldUpgradeOpenshell(installedOpenshellVersion, requiredOpenshellVersion)) { + console.log( + ` openshell CLI ${installedOpenshellVersion} is below required ${requiredOpenshellVersion}. Upgrading...`, + ); + openshellInstall = installOpenshell({ minimumVersion: requiredOpenshellVersion }); + if (!openshellInstall.installed) { + console.error( + ` Failed to upgrade openshell CLI to meet NemoClaw's minimum ${requiredOpenshellVersion}.`, + ); + console.error(" Install manually: https://github.com/NVIDIA/OpenShell/releases"); + process.exit(1); + } } console.log( ` ✓ openshell CLI: ${runCaptureOpenshell(["--version"], { ignoreError: true }) || "unknown"}`, @@ -3774,6 +3820,7 @@ module.exports = { getGatewayReuseState, getSandboxInferenceConfig, getInstalledOpenshellVersion, + getMinimumOpenshellVersion, getRequestedModelHint, getRequestedProviderHint, getStableGatewayImageRef, @@ -3796,6 +3843,7 @@ module.exports = { repairRecordedSandbox, recoverGatewayRuntime, resolveDashboardForwardTarget, + shouldUpgradeOpenshell, startGatewayForRecovery, runCaptureOpenshell, setupInference, diff --git a/scripts/install-openshell.sh b/scripts/install-openshell.sh index b0a0baaac..10f3a6564 100755 --- a/scripts/install-openshell.sh +++ b/scripts/install-openshell.sh @@ -4,6 +4,9 @@ set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' @@ -33,8 +36,27 @@ esac info "Detected $OS_LABEL ($ARCH_LABEL)" -# Minimum version required for cgroup v2 fix (NVIDIA/OpenShell#329) -MIN_VERSION="0.0.7" +resolve_min_version() { + if [[ -n "${NEMOCLAW_MIN_OPENSHELL_VERSION:-}" ]]; then + printf "%s" "${NEMOCLAW_MIN_OPENSHELL_VERSION}" + return + fi + + local blueprint="${REPO_ROOT}/nemoclaw-blueprint/blueprint.yaml" + if [[ -f "$blueprint" ]]; then + local version + version="$(sed -nE 's/^[[:space:]]*min_openshell_version:[[:space:]]*"([^"]+)".*/\1/p' "$blueprint" | head -1)" + if [[ -n "$version" ]]; then + printf "%s" "$version" + return + fi + fi + + # Keep a safe fallback for contexts that only have this script. + printf "%s" "0.1.0" +} + +MIN_VERSION="$(resolve_min_version)" version_gte() { # Returns 0 (true) if $1 >= $2 — portable, no sort -V (BSD compat) diff --git a/test/install-openshell.test.js b/test/install-openshell.test.js new file mode 100644 index 000000000..c8606952b --- /dev/null +++ b/test/install-openshell.test.js @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { describe, expect, it } from "vitest"; + +const INSTALL_OPEN_SHELL = path.join(import.meta.dirname, "..", "scripts", "install-openshell.sh"); +const TEST_SYSTEM_PATH = "/usr/bin:/bin"; + +function writeExecutable(target, contents) { + fs.writeFileSync(target, contents, { mode: 0o755 }); +} + +describe("install-openshell.sh", () => { + it("uses the blueprint minimum version when no override is provided", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-install-openshell-blueprint-")); + const fakeBin = path.join(tmp, "bin"); + fs.mkdirSync(fakeBin); + + writeExecutable( + path.join(fakeBin, "openshell"), + `#!/usr/bin/env bash +if [ "$1" = "--version" ]; then + echo "openshell 0.1.0" + exit 0 +fi +exit 0 +`, + ); + + const result = spawnSync("bash", [INSTALL_OPEN_SHELL], { + cwd: path.join(import.meta.dirname, ".."), + encoding: "utf-8", + env: { + ...process.env, + HOME: tmp, + PATH: `${fakeBin}:${TEST_SYSTEM_PATH}`, + }, + }); + + expect(result.status).toBe(0); + expect(`${result.stdout}${result.stderr}`).toMatch(/openshell already installed: 0\.1\.0 \(>= 0\.1\.0\)/); + }); + + it("honors an explicit minimum-version override from the caller", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-install-openshell-override-")); + const fakeBin = path.join(tmp, "bin"); + fs.mkdirSync(fakeBin); + + writeExecutable( + path.join(fakeBin, "openshell"), + `#!/usr/bin/env bash +if [ "$1" = "--version" ]; then + echo "openshell 0.0.21" + exit 0 +fi +exit 0 +`, + ); + + const result = spawnSync("bash", [INSTALL_OPEN_SHELL], { + cwd: path.join(import.meta.dirname, ".."), + encoding: "utf-8", + env: { + ...process.env, + HOME: tmp, + PATH: `${fakeBin}:${TEST_SYSTEM_PATH}`, + NEMOCLAW_MIN_OPENSHELL_VERSION: "0.0.20", + }, + }); + + expect(result.status).toBe(0); + expect(`${result.stdout}${result.stderr}`).toMatch(/openshell already installed: 0\.0\.21 \(>= 0\.0\.20\)/); + }); +}); diff --git a/test/onboard.test.js b/test/onboard.test.js index 267696119..28bcced63 100644 --- a/test/onboard.test.js +++ b/test/onboard.test.js @@ -16,6 +16,7 @@ import { getFutureShellPathHint, getSandboxInferenceConfig, getInstalledOpenshellVersion, + getMinimumOpenshellVersion, getRequestedModelHint, getRequestedProviderHint, getRequestedSandboxNameHint, @@ -30,6 +31,7 @@ import { patchStagedDockerfile, printSandboxCreateRecoveryHints, resolveDashboardForwardTarget, + shouldUpgradeOpenshell, shouldIncludeBuildContextPath, writeSandboxConfigSyncFile, } from "../bin/lib/onboard"; @@ -245,6 +247,10 @@ describe("onboard helpers", () => { expect(getInstalledOpenshellVersion("openshell 0.0.12")).toBe("0.0.12"); expect(getInstalledOpenshellVersion("openshell 0.0.13-dev.8+gbbcaed2ea")).toBe("0.0.13"); expect(getInstalledOpenshellVersion("bogus")).toBe(null); + expect(getMinimumOpenshellVersion('min_openshell_version: "0.1.0"\n')).toBe("0.1.0"); + expect(shouldUpgradeOpenshell("0.0.21", "0.1.0")).toBe(true); + expect(shouldUpgradeOpenshell("0.1.0", "0.1.0")).toBe(false); + expect(shouldUpgradeOpenshell("0.1.2", "0.1.0")).toBe(false); expect(getStableGatewayImageRef("openshell 0.0.12")).toBe( "ghcr.io/nvidia/openshell/cluster:0.0.12", ); From 146574034654e0e154be691fa935133f0452bda5 Mon Sep 17 00:00:00 2001 From: Deepak Jain Date: Sat, 4 Apr 2026 20:02:06 -0700 Subject: [PATCH 2/3] fix(onboard): keep openshell min-version parsing aligned Fixes #1404 Signed-off-by: Deepak Jain --- bin/lib/onboard.js | 8 ++++++-- scripts/install-openshell.sh | 10 ++++++++++ scripts/lib/openshell-version.js | 33 ++++++++++++++++++++++++++++++++ test/onboard.test.js | 7 +++++++ 4 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 scripts/lib/openshell-version.js diff --git a/bin/lib/onboard.js b/bin/lib/onboard.js index 03dc36a60..46ecea04e 100644 --- a/bin/lib/onboard.js +++ b/bin/lib/onboard.js @@ -53,6 +53,10 @@ const registry = require("./registry"); const nim = require("./nim"); const onboardSession = require("./onboard-session"); const policies = require("./policies"); +const { + DEFAULT_MIN_OPENSHELL_VERSION, + parseMinimumOpenshellVersion, +} = require("../../scripts/lib/openshell-version"); const { ensureUsageNoticeConsent } = require("./usage-notice"); const { checkPortAvailable, ensureSwap, getMemoryInfo } = require("./preflight"); @@ -455,8 +459,7 @@ function getMinimumOpenshellVersion(blueprintText = null) { const rawBlueprint = blueprintText == null ? fs.readFileSync(path.join(ROOT, "nemoclaw-blueprint", "blueprint.yaml"), "utf-8") : String(blueprintText); - const match = rawBlueprint.match(/^\s*min_openshell_version:\s*"?(?[0-9]+\.[0-9]+\.[0-9]+)"?\s*$/m); - return match?.groups?.version || null; + return parseMinimumOpenshellVersion(rawBlueprint); } function shouldUpgradeOpenshell(installedVersion = null, minimumVersion = null) { @@ -3818,6 +3821,7 @@ module.exports = { getFutureShellPathHint, getGatewayStartEnv, getGatewayReuseState, + DEFAULT_MIN_OPENSHELL_VERSION, getSandboxInferenceConfig, getInstalledOpenshellVersion, getMinimumOpenshellVersion, diff --git a/scripts/install-openshell.sh b/scripts/install-openshell.sh index 10f3a6564..c02d1d74b 100755 --- a/scripts/install-openshell.sh +++ b/scripts/install-openshell.sh @@ -43,6 +43,16 @@ resolve_min_version() { fi local blueprint="${REPO_ROOT}/nemoclaw-blueprint/blueprint.yaml" + local parser="${SCRIPT_DIR}/lib/openshell-version.js" + if [[ -f "$blueprint" && -f "$parser" && -n "$(command -v node || true)" ]]; then + local version + version="$(node "$parser" "$blueprint" 2>/dev/null || true)" + if [[ -n "$version" ]]; then + printf "%s" "$version" + return + fi + fi + if [[ -f "$blueprint" ]]; then local version version="$(sed -nE 's/^[[:space:]]*min_openshell_version:[[:space:]]*"([^"]+)".*/\1/p' "$blueprint" | head -1)" diff --git a/scripts/lib/openshell-version.js b/scripts/lib/openshell-version.js new file mode 100644 index 000000000..5dce54126 --- /dev/null +++ b/scripts/lib/openshell-version.js @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const fs = require("fs"); +const path = require("path"); + +const DEFAULT_MIN_OPENSHELL_VERSION = "0.1.0"; +const MIN_OPENSHELL_VERSION_PATTERN = + /^[\t ]*min_openshell_version:[\t ]*"([^"]+)".*$/m; + +function parseMinimumOpenshellVersion(blueprintText = "") { + const match = String(blueprintText ?? "").match(MIN_OPENSHELL_VERSION_PATTERN); + return match?.[1] || DEFAULT_MIN_OPENSHELL_VERSION; +} + +function getMinimumOpenshellVersionFromFile( + blueprintPath = path.join(__dirname, "..", "..", "nemoclaw-blueprint", "blueprint.yaml"), +) { + if (!fs.existsSync(blueprintPath)) { + return DEFAULT_MIN_OPENSHELL_VERSION; + } + return parseMinimumOpenshellVersion(fs.readFileSync(blueprintPath, "utf-8")); +} + +if (require.main === module) { + process.stdout.write(getMinimumOpenshellVersionFromFile(process.argv[2])); +} + +module.exports = { + DEFAULT_MIN_OPENSHELL_VERSION, + getMinimumOpenshellVersionFromFile, + parseMinimumOpenshellVersion, +}; diff --git a/test/onboard.test.js b/test/onboard.test.js index 28bcced63..da322a157 100644 --- a/test/onboard.test.js +++ b/test/onboard.test.js @@ -15,6 +15,7 @@ import { getPortConflictServiceHints, getFutureShellPathHint, getSandboxInferenceConfig, + DEFAULT_MIN_OPENSHELL_VERSION, getInstalledOpenshellVersion, getMinimumOpenshellVersion, getRequestedModelHint, @@ -248,6 +249,12 @@ describe("onboard helpers", () => { expect(getInstalledOpenshellVersion("openshell 0.0.13-dev.8+gbbcaed2ea")).toBe("0.0.13"); expect(getInstalledOpenshellVersion("bogus")).toBe(null); expect(getMinimumOpenshellVersion('min_openshell_version: "0.1.0"\n')).toBe("0.1.0"); + expect(getMinimumOpenshellVersion("min_openshell_version: 0.2.0\n")).toBe( + DEFAULT_MIN_OPENSHELL_VERSION, + ); + expect(getMinimumOpenshellVersion("name: no-version-here\n")).toBe( + DEFAULT_MIN_OPENSHELL_VERSION, + ); expect(shouldUpgradeOpenshell("0.0.21", "0.1.0")).toBe(true); expect(shouldUpgradeOpenshell("0.1.0", "0.1.0")).toBe(false); expect(shouldUpgradeOpenshell("0.1.2", "0.1.0")).toBe(false); From d403cb2fc5b21aab2fea56fd1cc1b1b9f4fc9f7d Mon Sep 17 00:00:00 2001 From: Deepak Jain Date: Sat, 4 Apr 2026 20:51:43 -0700 Subject: [PATCH 3/3] fix(onboard): use shared openshell version file loader Fixes #1404 Signed-off-by: Deepak Jain --- bin/lib/onboard.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/bin/lib/onboard.js b/bin/lib/onboard.js index 46ecea04e..33820471e 100644 --- a/bin/lib/onboard.js +++ b/bin/lib/onboard.js @@ -55,6 +55,7 @@ const onboardSession = require("./onboard-session"); const policies = require("./policies"); const { DEFAULT_MIN_OPENSHELL_VERSION, + getMinimumOpenshellVersionFromFile, parseMinimumOpenshellVersion, } = require("../../scripts/lib/openshell-version"); const { ensureUsageNoticeConsent } = require("./usage-notice"); @@ -456,10 +457,12 @@ function compareVersions(left = "0.0.0", right = "0.0.0") { } function getMinimumOpenshellVersion(blueprintText = null) { - const rawBlueprint = blueprintText == null - ? fs.readFileSync(path.join(ROOT, "nemoclaw-blueprint", "blueprint.yaml"), "utf-8") - : String(blueprintText); - return parseMinimumOpenshellVersion(rawBlueprint); + if (blueprintText == null) { + return getMinimumOpenshellVersionFromFile( + path.join(ROOT, "nemoclaw-blueprint", "blueprint.yaml"), + ); + } + return parseMinimumOpenshellVersion(String(blueprintText)); } function shouldUpgradeOpenshell(installedVersion = null, minimumVersion = null) {