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
61 changes: 58 additions & 3 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ const registry = require("./registry");
const nim = require("./nim");
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");
const { checkPortAvailable, ensureSwap, getMemoryInfo } = require("./preflight");

Expand Down Expand Up @@ -438,6 +443,33 @@ 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) {
if (blueprintText == null) {
return getMinimumOpenshellVersionFromFile(
path.join(ROOT, "nemoclaw-blueprint", "blueprint.yaml"),
);
}
return parseMinimumOpenshellVersion(String(blueprintText));
}

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;
Expand Down Expand Up @@ -1594,10 +1626,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,
Expand Down Expand Up @@ -1746,14 +1782,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"}`,
Expand Down Expand Up @@ -3772,8 +3824,10 @@ module.exports = {
getFutureShellPathHint,
getGatewayStartEnv,
getGatewayReuseState,
DEFAULT_MIN_OPENSHELL_VERSION,
getSandboxInferenceConfig,
getInstalledOpenshellVersion,
getMinimumOpenshellVersion,
getRequestedModelHint,
getRequestedProviderHint,
getStableGatewayImageRef,
Expand All @@ -3796,6 +3850,7 @@ module.exports = {
repairRecordedSandbox,
recoverGatewayRuntime,
resolveDashboardForwardTarget,
shouldUpgradeOpenshell,
startGatewayForRecovery,
runCaptureOpenshell,
setupInference,
Expand Down
36 changes: 34 additions & 2 deletions scripts/install-openshell.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -33,8 +36,37 @@ 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"
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)"
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)
Expand Down
33 changes: 33 additions & 0 deletions scripts/lib/openshell-version.js
Original file line number Diff line number Diff line change
@@ -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,
};
78 changes: 78 additions & 0 deletions test/install-openshell.test.js
Original file line number Diff line number Diff line change
@@ -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\)/);
});
});
13 changes: 13 additions & 0 deletions test/onboard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import {
getPortConflictServiceHints,
getFutureShellPathHint,
getSandboxInferenceConfig,
DEFAULT_MIN_OPENSHELL_VERSION,
getInstalledOpenshellVersion,
getMinimumOpenshellVersion,
getRequestedModelHint,
getRequestedProviderHint,
getRequestedSandboxNameHint,
Expand All @@ -30,6 +32,7 @@ import {
patchStagedDockerfile,
printSandboxCreateRecoveryHints,
resolveDashboardForwardTarget,
shouldUpgradeOpenshell,
shouldIncludeBuildContextPath,
writeSandboxConfigSyncFile,
} from "../bin/lib/onboard";
Expand Down Expand Up @@ -245,6 +248,16 @@ 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(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);
expect(getStableGatewayImageRef("openshell 0.0.12")).toBe(
"ghcr.io/nvidia/openshell/cluster:0.0.12",
);
Expand Down