Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
42 changes: 38 additions & 4 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,23 @@ function getInstalledOpenshellVersion(versionOutput = null) {
return match[1];
}

/** True when `left` >= `right` (semver, major.minor.patch). */
function versionGte(left = "0.0.0", right = "0.0.0") {
const lhs = String(left)
.split(".")
.map((p) => Number.parseInt(p, 10) || 0);
const rhs = String(right)
.split(".")
.map((p) => Number.parseInt(p, 10) || 0);
for (let i = 0; i < Math.max(lhs.length, rhs.length); i++) {
const a = lhs[i] || 0;
const b = rhs[i] || 0;
if (a > b) return true;
if (a < b) return false;
}
return true;
}

function getStableGatewayImageRef(versionOutput = null) {
const version = getInstalledOpenshellVersion(versionOutput);
if (!version) return null;
Expand Down Expand Up @@ -1890,9 +1907,14 @@ function getPortConflictServiceHints(platform = process.platform) {
}

function installOpenshell() {
const pinnedVersion = require("../../package.json").openshellVersion;
const installEnv = { ...process.env };
if (pinnedVersion) {
installEnv.OPENSHELL_PIN_VERSION = pinnedVersion;
}
const result = spawnSync("bash", [path.join(SCRIPTS, "install-openshell.sh")], {
cwd: ROOT,
env: process.env,
env: installEnv,
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf-8",
timeout: 300_000,
Expand Down Expand Up @@ -2045,10 +2067,21 @@ async function preflight() {
console.log(` ✓ Container runtime: ${runtime}`);
}

// OpenShell CLI
// OpenShell CLI — install if missing or upgrade if below pinned version
const pinnedOpenshellVersion = require("../../package.json").openshellVersion;
let openshellInstall = { localBin: null, futureShellPathHint: null };
if (!isOpenshellInstalled()) {
console.log(" openshell CLI not found. Installing...");
const installedOpenshellVersion = isOpenshellInstalled() ? getInstalledOpenshellVersion() : null;
const needsInstall =
!installedOpenshellVersion ||
(pinnedOpenshellVersion && !versionGte(installedOpenshellVersion, pinnedOpenshellVersion));
if (needsInstall) {
if (!installedOpenshellVersion) {
console.log(" openshell CLI not found. Installing...");
} else {
console.log(
` openshell ${installedOpenshellVersion} is below required ${pinnedOpenshellVersion} — upgrading...`,
);
}
openshellInstall = installOpenshell();
if (!openshellInstall.installed) {
console.error(" Failed to install openshell CLI.");
Expand Down Expand Up @@ -4198,4 +4231,5 @@ module.exports = {
shouldIncludeBuildContextPath,
writeSandboxConfigSyncFile,
patchStagedDockerfile,
versionGte,
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "nemoclaw",
"version": "0.1.0",
"openshellVersion": "0.0.22",
"description": "NemoClaw — run OpenClaw inside OpenShell with NVIDIA inference",
"license": "Apache-2.0",
"bin": {
Expand Down
33 changes: 23 additions & 10 deletions scripts/install-openshell.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ esac

info "Detected $OS_LABEL ($ARCH_LABEL)"

# Minimum version required for sandbox persistence across gateway restarts
# (deterministic k3s node name + workspace PVC: NVIDIA/OpenShell#739, #488)
MIN_VERSION="0.0.22"
# OPENSHELL_PIN_VERSION (set by onboard.js from package.json) is the version
# NemoClaw was tested against. When set, download that exact release and treat
# it as the minimum required version. When unset, fall back to the static
# floor for sandbox persistence across gateway restarts
# (deterministic k3s node name + workspace PVC: NVIDIA/OpenShell#739, #488).
REQUIRED_VERSION="${OPENSHELL_PIN_VERSION:-0.0.22}"

version_gte() {
# Returns 0 (true) if $1 >= $2 — portable, no sort -V (BSD compat)
Expand All @@ -53,11 +56,11 @@ version_gte() {

if command -v openshell >/dev/null 2>&1; then
INSTALLED_VERSION="$(openshell --version 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo '0.0.0')"
if version_gte "$INSTALLED_VERSION" "$MIN_VERSION"; then
info "openshell already installed: $INSTALLED_VERSION (>= $MIN_VERSION)"
if version_gte "$INSTALLED_VERSION" "$REQUIRED_VERSION"; then
info "openshell already installed: $INSTALLED_VERSION (>= $REQUIRED_VERSION)"
exit 0
fi
warn "openshell $INSTALLED_VERSION is below minimum $MIN_VERSION — upgrading..."
warn "openshell $INSTALLED_VERSION is below required $REQUIRED_VERSION — upgrading..."
fi

info "Installing openshell CLI..."
Expand All @@ -81,18 +84,28 @@ tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

CHECKSUM_FILE="openshell-checksums-sha256.txt"

# When a pin is set, download that exact tag; otherwise download latest.
GH_TAG_FLAG=()
CURL_TAG_PATH="latest/download"
if [[ -n "${OPENSHELL_PIN_VERSION:-}" ]]; then
GH_TAG_FLAG=(--tag "v${OPENSHELL_PIN_VERSION}")
CURL_TAG_PATH="download/v${OPENSHELL_PIN_VERSION}"
info "Pinned to OpenShell v${OPENSHELL_PIN_VERSION}"
fi
Comment on lines +88 to +95
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate OPENSHELL_PIN_VERSION format before using it.

If the env var is malformed, the script fails later during download/checksum with less actionable errors. Add an early semver-format guard for better reliability.

🛡️ Suggested hardening
 if [[ -n "${OPENSHELL_PIN_VERSION:-}" ]]; then
+  if [[ ! "${OPENSHELL_PIN_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+    fail "Invalid OPENSHELL_PIN_VERSION '${OPENSHELL_PIN_VERSION}' (expected X.Y.Z)"
+  fi
   GH_TAG_FLAG=(--tag "v${OPENSHELL_PIN_VERSION}")
   CURL_TAG_PATH="download/v${OPENSHELL_PIN_VERSION}"
   info "Pinned to OpenShell v${OPENSHELL_PIN_VERSION}"
 fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# When a pin is set, download that exact tag; otherwise download latest.
GH_TAG_FLAG=()
CURL_TAG_PATH="latest/download"
if [[ -n "${OPENSHELL_PIN_VERSION:-}" ]]; then
GH_TAG_FLAG=(--tag "v${OPENSHELL_PIN_VERSION}")
CURL_TAG_PATH="download/v${OPENSHELL_PIN_VERSION}"
info "Pinned to OpenShell v${OPENSHELL_PIN_VERSION}"
fi
# When a pin is set, download that exact tag; otherwise download latest.
GH_TAG_FLAG=()
CURL_TAG_PATH="latest/download"
if [[ -n "${OPENSHELL_PIN_VERSION:-}" ]]; then
if [[ ! "${OPENSHELL_PIN_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
fail "Invalid OPENSHELL_PIN_VERSION '${OPENSHELL_PIN_VERSION}' (expected X.Y.Z)"
fi
GH_TAG_FLAG=(--tag "v${OPENSHELL_PIN_VERSION}")
CURL_TAG_PATH="download/v${OPENSHELL_PIN_VERSION}"
info "Pinned to OpenShell v${OPENSHELL_PIN_VERSION}"
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/install-openshell.sh` around lines 88 - 95, Add a validation step for
OPENSHELL_PIN_VERSION before using it: check OPENSHELL_PIN_VERSION against a
strict semver regex (e.g., major.minor.patch with optional pre-release/build)
and if it fails print a clear error via info or echo and exit non‑zero; only
then set GH_TAG_FLAG and CURL_TAG_PATH and log "Pinned to OpenShell
v${OPENSHELL_PIN_VERSION}". Update the branch that assigns GH_TAG_FLAG,
CURL_TAG_PATH and the info message (references: GH_TAG_FLAG, CURL_TAG_PATH,
OPENSHELL_PIN_VERSION) to perform this guard so malformed values won't reach the
download/checksum stages.


download_with_curl() {
curl -fsSL "https://github.com/NVIDIA/OpenShell/releases/latest/download/$ASSET" \
curl -fsSL "https://github.com/NVIDIA/OpenShell/releases/${CURL_TAG_PATH}/$ASSET" \
-o "$tmpdir/$ASSET"
curl -fsSL "https://github.com/NVIDIA/OpenShell/releases/latest/download/$CHECKSUM_FILE" \
curl -fsSL "https://github.com/NVIDIA/OpenShell/releases/${CURL_TAG_PATH}/$CHECKSUM_FILE" \
-o "$tmpdir/$CHECKSUM_FILE"
}

if command -v gh >/dev/null 2>&1; then
if GH_PROMPT_DISABLED=1 GH_TOKEN="${GH_TOKEN:-${GITHUB_TOKEN:-}}" gh release download --repo NVIDIA/OpenShell \
--pattern "$ASSET" --dir "$tmpdir" 2>/dev/null \
"${GH_TAG_FLAG[@]}" --pattern "$ASSET" --dir "$tmpdir" 2>/dev/null \
&& GH_PROMPT_DISABLED=1 GH_TOKEN="${GH_TOKEN:-${GITHUB_TOKEN:-}}" gh release download --repo NVIDIA/OpenShell \
--pattern "$CHECKSUM_FILE" --dir "$tmpdir" 2>/dev/null; then
"${GH_TAG_FLAG[@]}" --pattern "$CHECKSUM_FILE" --dir "$tmpdir" 2>/dev/null; then
: # gh succeeded
else
warn "gh CLI download failed (auth may not be configured) — falling back to curl"
Expand Down
9 changes: 9 additions & 0 deletions test/onboard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
printSandboxCreateRecoveryHints,
resolveDashboardForwardTarget,
shouldIncludeBuildContextPath,
versionGte,
writeSandboxConfigSyncFile,
} from "../bin/lib/onboard";
import { buildWebSearchDockerConfig } from "../dist/lib/web-search";
Expand Down Expand Up @@ -306,6 +307,14 @@ describe("onboard helpers", () => {
expect(getStableGatewayImageRef("bogus")).toBe(null);
});

it("versionGte compares semver correctly for OpenShell pin checks", () => {
expect(versionGte("0.0.22", "0.0.22")).toBe(true);
expect(versionGte("0.0.23", "0.0.22")).toBe(true);
expect(versionGte("0.1.0", "0.0.22")).toBe(true);
expect(versionGte("0.0.21", "0.0.22")).toBe(false);
expect(versionGte("0.0.7", "0.0.22")).toBe(false);
});

it("treats the gateway as healthy only when nemoclaw is running and connected", () => {
expect(
isGatewayHealthy(
Expand Down
14 changes: 14 additions & 0 deletions test/runner.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,20 @@ describe("regression guards", () => {
expect(src).toContain("shasum -a 256 -c");
});

it("install-openshell.sh respects OPENSHELL_PIN_VERSION for tagged downloads", () => {
const src = fs.readFileSync(
path.join(import.meta.dirname, "..", "scripts", "install-openshell.sh"),
"utf-8",
);
// Env-var driven pin replaces hardcoded MIN_VERSION
expect(src).toContain("OPENSHELL_PIN_VERSION");
expect(src).toContain("REQUIRED_VERSION");
// gh path uses --tag flag when pin is set
expect(src).toContain('--tag "v${OPENSHELL_PIN_VERSION}"');
// curl path uses versioned download URL when pin is set
expect(src).toContain("download/v${OPENSHELL_PIN_VERSION}");
});

it("install-openshell.sh falls back to curl when gh fails (#1318)", () => {
const src = fs.readFileSync(
path.join(import.meta.dirname, "..", "scripts", "install-openshell.sh"),
Expand Down
Loading