Skip to content
Closed
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
279 changes: 141 additions & 138 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,170 +2,173 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Thin bootstrap for the NemoClaw installer.
# Public curl|bash installs should select a ref once, clone that ref, then
# execute installer logic from that same clone. Historical tags that predate
# the extracted payload fall back to their own root install.sh.
# NemoClaw installer — installs Node.js, Ollama (if GPU present), and NemoClaw.

set -euo pipefail

if [[ -n "${BASH_SOURCE[0]:-}" ]]; then
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
else
SCRIPT_DIR=""
fi
LOCAL_PAYLOAD="${SCRIPT_DIR:+${SCRIPT_DIR}/scripts/install.sh}"
BOOTSTRAP_TMPDIR=""
PAYLOAD_MARKER="NEMOCLAW_VERSIONED_INSTALLER_PAYLOAD=1"
DEFAULT_INSTALL_REF="lkg"

resolve_release_tag() {
if [[ -n "${NEMOCLAW_INSTALL_REF:-}" ]]; then
printf "%s" "${NEMOCLAW_INSTALL_REF}"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
info() { printf '\033[1;34m[INFO]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[WARN]\033[0m %s\n' "$*"; }
error() { printf '\033[1;31m[ERROR]\033[0m %s\n' "$*"; exit 1; }

command_exists() { command -v "$1" &>/dev/null; }

# Compare two semver strings (major.minor.patch). Returns 0 if $1 >= $2.
version_gte() {
local IFS=.
local -a a=($1) b=($2)
for i in 0 1 2; do
local ai=${a[$i]:-0} bi=${b[$i]:-0}
if (( ai > bi )); then return 0; fi
if (( ai < bi )); then return 1; fi
done
return 0
}

# ---------------------------------------------------------------------------
# 1. Node.js
# ---------------------------------------------------------------------------
install_nodejs() {
if command_exists node; then
info "Node.js found: $(node --version)"
return
fi
printf "%s" "${NEMOCLAW_INSTALL_TAG:-$DEFAULT_INSTALL_REF}"

info "Node.js not found — installing via nvm…"
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Avoid executing remote installer scripts without integrity verification.

These curl | bash/sh paths execute mutable network content as the current user. Download to a temp file, verify a pinned checksum/signature, then execute the verified file.

Also applies to: 89-96

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@install.sh` at line 40, The nvm install flow is executing a remote script
directly via curl | bash, which skips integrity checks. Update the install logic
in the nvm bootstrap section to first download the installer to a temporary
file, verify it against a pinned checksum or signature, and only then run it;
apply the same pattern to the other curl | bash/sh usages referenced in the
install script so all remote executables are verified before execution.

Source: Linters/SAST tools

\. "$HOME/.nvm/nvm.sh"
nvm install 24
Comment on lines +34 to +42

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the target script and related helpers
git ls-files | rg '(^|/)install\.sh$|(^|/)README\.md$|(^|/)\.sh$'

# Show the install.sh file with line numbers
wc -l install.sh
cat -n install.sh | sed -n '1,220p'

# Search for helper functions and later npm/node usage
rg -n "command_exists|version_gte|install_nodejs|install_nemoclaw|npm|node --version|nvm install|nvm use" install.sh

Repository: NVIDIA/NemoClaw

Length of output: 9025


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect declared runtime requirements
wc -l package.json 2>/dev/null || true
cat -n package.json 2>/dev/null | sed -n '1,220p' || true

# Look for Node engine/version constraints and install docs
rg -n '"engines"|node|npm|npx|install_nodejs|install_nemoclaw|v24|24\.0\.0' package.json README.md install.sh scripts/install.sh

Repository: NVIDIA/NemoClaw

Length of output: 24212


Check both node and npm before skipping nvm. Any existing node currently bypasses the upgrade path, so Node < 22.16.0 or Node-without-npm setups still reach npm install later and fail.

🧰 Tools
🪛 ast-grep (0.44.0)

[error] 39-39: Remote content fetched with curl/wget is piped directly into a shell interpreter, so any server compromise, MITM, or tampered mirror results in arbitrary code execution on this host. Download the script to a file first, verify its integrity (checksum/signature) and inspect it, then run the verified local copy.
Context: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash
Note: [CWE-494] Download of Code Without Integrity Check.

(curl-pipe-to-shell-bash)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@install.sh` around lines 34 - 42, The install flow in install.sh currently
exits early in the node-check path, but it should verify both node and npm
before skipping nvm. Update the existing command_exists logic around the node
version check so it only returns when both node and npm are present and the Node
version meets the minimum required version; otherwise continue through the nvm
install/upgrade path in the same install block.

info "Node.js installed: $(node --version)"
}

verify_downloaded_script() {
local file="$1" label="${2:-installer}" expected_hash="${3:-}"
if [[ ! -s "$file" ]]; then
printf "[ERROR] %s download is empty or missing\n" "$label" >&2
exit 1
# ---------------------------------------------------------------------------
# 2. Ollama
# ---------------------------------------------------------------------------
OLLAMA_MIN_VERSION="0.18.0"

get_ollama_version() {
# `ollama --version` outputs something like "ollama version 0.18.0"
ollama --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1
Comment on lines +51 to +53

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make version parsing non-fatal.

With pipefail, an unparseable ollama --version exits the installer before the current:-unknown upgrade path can run.

Proposed fix
 get_ollama_version() {
   # `ollama --version` outputs something like "ollama version 0.18.0"
-  ollama --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1
+  ollama --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true
 }
📝 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
get_ollama_version() {
# `ollama --version` outputs something like "ollama version 0.18.0"
ollama --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1
get_ollama_version() {
# `ollama --version` outputs something like "ollama version 0.18.0"
ollama --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@install.sh` around lines 51 - 53, The version parsing in get_ollama_version()
is too brittle under pipefail, causing install.sh to exit before the
current:-unknown upgrade path can run. Update get_ollama_version() so the ollama
--version pipeline is non-fatal when parsing fails, and make it return an
empty/unknown result instead of aborting; use the existing get_ollama_version
symbol and the current:-unknown fallback path to keep the installer progressing.

}

detect_gpu() {
# Returns 0 if a GPU is detected
if command_exists nvidia-smi; then
nvidia-smi &>/dev/null && return 0
fi
if ! head -1 "$file" | grep -qE '^#!.*(sh|bash)'; then
printf "[ERROR] %s does not start with a shell shebang\n" "$label" >&2
exit 1
return 1
}
Comment on lines +56 to +62

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Align GPU detection with the macOS VRAM fallback.

get_vram_mb supports macOS unified memory, but detect_gpu only recognizes nvidia-smi; Apple Silicon/macOS hosts will always skip Ollama before that fallback is used.

Proposed fix
 detect_gpu() {
   # Returns 0 if a GPU is detected
   if command_exists nvidia-smi; then
     nvidia-smi &>/dev/null && return 0
   fi
+  if [[ "$(uname -s)" == "Darwin" ]] && command_exists sysctl; then
+    return 0
+  fi
   return 1
 }

Also applies to: 92-100

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@install.sh` around lines 56 - 62, The GPU detection logic in detect_gpu only
checks nvidia-smi, so macOS/Apple Silicon hosts are treated as GPU-less even
though get_vram_mb can fall back to unified memory. Update detect_gpu to
recognize the macOS path used by get_vram_mb (or otherwise treat Apple
Silicon/macOS as GPU-capable) so Ollama is not skipped before the VRAM fallback
can run, and make sure the related install flow that consumes detect_gpu follows
the same logic.


get_vram_mb() {
# Returns total VRAM in MiB (NVIDIA only). Falls back to 0.
if command_exists nvidia-smi; then
nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null \
| awk '{s += $1} END {print s+0}'
return
fi
if [[ -n "$expected_hash" ]]; then
local actual_hash=""
if command -v sha256sum >/dev/null 2>&1; then
actual_hash="$(sha256sum "$file" | awk '{print $1}')"
elif command -v shasum >/dev/null 2>&1; then
actual_hash="$(shasum -a 256 "$file" | awk '{print $1}')"
fi
if [[ -z "$actual_hash" ]]; then
printf "[ERROR] No SHA-256 tool available — cannot verify %s integrity\n" "$label" >&2
exit 1
fi
if [[ "$actual_hash" != "$expected_hash" ]]; then
rm -f "$file"
printf "[ERROR] %s integrity check failed\n Expected: %s\n Actual: %s\n" "$label" "$expected_hash" "$actual_hash" >&2
exit 1
fi
# macOS — report unified memory as VRAM
if [[ "$(uname -s)" == "Darwin" ]] && command_exists sysctl; then
local bytes
bytes=$(sysctl -n hw.memsize 2>/dev/null || echo 0)
echo $(( bytes / 1024 / 1024 ))
return
fi
echo 0
}

has_payload_marker() {
local file="$1"
[[ -f "$file" ]] && grep -q "$PAYLOAD_MARKER" "$file"
}
install_or_upgrade_ollama() {
if detect_gpu && command_exists ollama; then
local current
current=$(get_ollama_version)
if [[ -n "$current" ]] && version_gte "$current" "$OLLAMA_MIN_VERSION"; then
info "Ollama v${current} meets minimum requirement (>= v${OLLAMA_MIN_VERSION})"
else
info "Ollama v${current:-unknown} is below v${OLLAMA_MIN_VERSION} — upgrading…"
curl -fsSL https://ollama.com/install.sh | sh
info "Ollama upgraded to $(get_ollama_version)"
fi
else
# No ollama — only install if a GPU is present
if detect_gpu; then
info "GPU detected — installing Ollama…"
curl -fsSL https://ollama.com/install.sh | sh
info "Ollama installed: v$(get_ollama_version)"
else
warn "No GPU detected — skipping Ollama installation."
return
fi
fi

clone_nemoclaw_ref() {
local ref="$1" dest="$2"
# Pull the appropriate model based on VRAM
local vram_mb
vram_mb=$(get_vram_mb)
local vram_gb=$(( vram_mb / 1024 ))
info "Detected ${vram_gb} GB VRAM"

git init --quiet "$dest"
git -C "$dest" remote add origin https://github.com/NVIDIA/NemoClaw.git
if ! git -C "$dest" fetch --quiet --depth 1 origin "$ref"; then
printf "[ERROR] Requested install ref '%s' is not available from https://github.com/NVIDIA/NemoClaw.git.\n" "$ref" >&2
printf " Check NEMOCLAW_INSTALL_TAG/NEMOCLAW_INSTALL_REF and try again.\n" >&2
exit 1
if (( vram_gb >= 120 )); then
info "Pulling nemotron-3-super:120b…"
ollama pull nemotron-3-super:120b
else
info "Pulling nemotron-3-nano:30b…"
ollama pull nemotron-3-nano:30b
fi
git -C "$dest" -c advice.detachedHead=false checkout --quiet --detach FETCH_HEAD
}

exec_installer_from_ref() {
local ref="$1"
shift

local tmpdir source_root payload_script legacy_script
tmpdir="$(mktemp -d)"
BOOTSTRAP_TMPDIR="$tmpdir"
trap 'rm -rf "${BOOTSTRAP_TMPDIR:-}"' EXIT
source_root="${tmpdir}/source"

clone_nemoclaw_ref "$ref" "$source_root"
# ---------------------------------------------------------------------------
# 3. NemoClaw
# ---------------------------------------------------------------------------
install_nemoclaw() {
# Ensure npm global bin is on PATH (nvm installs to a non-standard location)
local npm_bin
npm_bin="$(npm config get prefix)/bin"
if ! echo "$PATH" | tr ':' '\n' | grep -qx "$npm_bin"; then
export PATH="$npm_bin:$PATH"
fi

payload_script="${source_root}/scripts/install.sh"
legacy_script="${source_root}/install.sh"
if [[ -f "./package.json" ]] && grep -q '"name": "nemoclaw"' ./package.json 2>/dev/null; then
info "NemoClaw package.json found in current directory — installing from source…"
npm install && npm link
else
info "Installing NemoClaw from npm…"
npm install -g nemoclaw
fi

if has_payload_marker "$payload_script"; then
verify_downloaded_script "$payload_script" "versioned installer"
NEMOCLAW_INSTALL_REF="$ref" NEMOCLAW_INSTALL_TAG="$ref" NEMOCLAW_BOOTSTRAP_PAYLOAD=1 \
bash "$payload_script" "$@"
return
# Persist npm global bin for future shells
local profile="$HOME/.bashrc"
[ -f "$HOME/.zshrc" ] && profile="$HOME/.zshrc"
if ! grep -q 'npm config get prefix' "$profile" 2>/dev/null; then
echo 'export PATH="$(npm config get prefix)/bin:$PATH"' >> "$profile"
info "Added npm global bin to $profile"
fi
Comment on lines +138 to 144

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Persist the npm global bin to both supported shell profiles.

The PR objective says .bashrc and .zshrc are updated, but this code only updates .zshrc when it exists, otherwise .bashrc.

Proposed fix
-  local profile="$HOME/.bashrc"
-  [ -f "$HOME/.zshrc" ] && profile="$HOME/.zshrc"
-  if ! grep -q 'npm config get prefix' "$profile" 2>/dev/null; then
-    echo 'export PATH="$(npm config get prefix)/bin:$PATH"' >> "$profile"
-    info "Added npm global bin to $profile"
-  fi
+  local profile
+  for profile in "$HOME/.bashrc" "$HOME/.zshrc"; do
+    touch "$profile"
+    if ! grep -Fqx 'export PATH="$(npm config get prefix)/bin:$PATH"' "$profile"; then
+      echo 'export PATH="$(npm config get prefix)/bin:$PATH"' >> "$profile"
+      info "Added npm global bin to $profile"
+    fi
+  done
📝 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
# Persist npm global bin for future shells
local profile="$HOME/.bashrc"
[ -f "$HOME/.zshrc" ] && profile="$HOME/.zshrc"
if ! grep -q 'npm config get prefix' "$profile" 2>/dev/null; then
echo 'export PATH="$(npm config get prefix)/bin:$PATH"' >> "$profile"
info "Added npm global bin to $profile"
fi
# Persist npm global bin for future shells
local profile
for profile in "$HOME/.bashrc" "$HOME/.zshrc"; do
touch "$profile"
if ! grep -Fqx 'export PATH="$(npm config get prefix)/bin:$PATH"' "$profile"; then
echo 'export PATH="$(npm config get prefix)/bin:$PATH"' >> "$profile"
info "Added npm global bin to $profile"
fi
done
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@install.sh` around lines 138 - 144, The npm global bin persistence logic only
writes to one shell profile based on which file exists, but it should update
both supported profiles. In the install.sh profile-handling block, adjust the
shell setup so the PATH export is appended to both .bashrc and .zshrc when
present, instead of choosing one via the profile variable. Keep the existing
duplicate-check behavior, and make sure the change is localized around the npm
global bin persistence logic.


verify_downloaded_script "$legacy_script" "legacy installer"
NEMOCLAW_INSTALL_TAG="$ref" bash "$legacy_script" "$@"
if ! command_exists nemoclaw; then
error "nemoclaw not found in PATH after install. Try: source ~/.bashrc && nemoclaw --help"
fi
info "nemoclaw is ready: $(nemoclaw --version 2>/dev/null || echo 'installed')"
}

bootstrap_version() {
printf "nemoclaw-installer\n"
# ---------------------------------------------------------------------------
# 4. Onboard
# ---------------------------------------------------------------------------
run_onboard() {
info "Running nemoclaw onboard…"
npx nemoclaw onboard
}

bootstrap_usage() {
printf "\n"
printf " NemoClaw Installer\n\n"
printf " Usage:\n"
printf " curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash\n"
printf " curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash -s -- [options]\n\n"
printf " Options:\n"
printf " --non-interactive Skip prompts (uses env vars / defaults)\n"
printf " --yes-i-accept-third-party-software Accept the third-party software notice without prompting\n"
printf " --fresh Discard any failed/interrupted onboarding session and start over\n"
printf " --version, -v Print installer version and exit\n"
printf " --help, -h Show this help message and exit\n\n"
printf " Environment:\n"
printf " NEMOCLAW_INSTALL_REF Exact Git ref/SHA to install\n"
printf " NEMOCLAW_INSTALL_TAG Git ref to install (default: lkg)\n"
printf " In curl pipes, set this on bash or export it first.\n"
printf " Example: curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_INSTALL_TAG=v0.0.56 bash\n"
printf " NEMOCLAW_NON_INTERACTIVE=1 Same as --non-interactive\n"
printf " NEMOCLAW_FRESH=1 Same as --fresh\n"
printf " NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 Same as --yes-i-accept-third-party-software\n"
printf " NEMOCLAW_NO_EXPRESS=1 Skip express install prompt on supported platforms\n"
printf " NEMOCLAW_SANDBOX_NAME Sandbox name to create/use\n"
printf " NEMOCLAW_ACCEPT_EXPERIMENTAL_OPENSHELL_UPGRADE=1\n"
printf " Allow automatic pre-0.0.37 OpenShell gateway upgrade\n"
printf " NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1\n"
printf " Continue after manually backing up and retiring old gateway\n"
printf " NEMOCLAW_PROVIDER build | openai | anthropic | anthropicCompatible\n"
printf " | gemini | ollama | custom | nim-local | vllm | routed\n"
printf " | hermes-provider\n"
printf " (aliases: cloud -> build, nim -> nim-local)\n"
printf " NEMOCLAW_POLICY_MODE suggested | custom | skip\n"
printf "\n"
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
main() {
info "=== NemoClaw Installer ==="

bootstrap_main() {
for arg in "$@"; do
case "$arg" in
--help | -h)
bootstrap_usage
return 0
;;
--version | -v)
bootstrap_version
return 0
;;
esac
done
install_nodejs
# install_or_upgrade_ollama

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Either wire in Ollama setup or remove the advertised Ollama install path.

The installer header and Ollama functions say GPU hosts get Ollama/model setup, but main never calls it. Re-enable this after addressing the installer verification issues above, or update the installer scope to avoid a dead path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@install.sh` at line 167, The Ollama install path is advertised but never
executed from main, leaving a dead code path. Either call
install_or_upgrade_ollama from the main installer flow after the existing
verification steps, or remove the Ollama-related header/docs and helper
functions if Ollama is no longer in scope. Use the existing main entrypoint and
install_or_upgrade_ollama symbol to locate the wiring point.

install_nemoclaw
run_onboard

local ref
ref="$(resolve_release_tag)"
exec_installer_from_ref "$ref" "$@"
info "=== Installation complete ==="
}

if has_payload_marker "$LOCAL_PAYLOAD"; then
# shellcheck source=/dev/null
. "$LOCAL_PAYLOAD"
fi

if [[ "${BASH_SOURCE[0]:-}" == "$0" ]] || { [[ -z "${BASH_SOURCE[0]:-}" ]] && { [[ "$0" == "bash" ]] || [[ "$0" == "-bash" ]]; }; }; then
if has_payload_marker "$LOCAL_PAYLOAD"; then
main "$@"
else
bootstrap_main "$@"
fi
fi
main "$@"