Skip to content

fix: validate openshell binary to prevent npm package shadowing#970

Open
SaiSharan2005 wants to merge 2 commits intoNVIDIA:mainfrom
SaiSharan2005:fix/openshell-binary-detection
Open

fix: validate openshell binary to prevent npm package shadowing#970
SaiSharan2005 wants to merge 2 commits intoNVIDIA:mainfrom
SaiSharan2005:fix/openshell-binary-detection

Conversation

@SaiSharan2005
Copy link
Copy Markdown

@SaiSharan2005 SaiSharan2005 commented Mar 26, 2026

Summary

Add isOpenshellCLI() validation to prevent the npm openshell gateway package from shadowing the real OpenShell CLI binary during install. Also surface a clear error with manual install steps when install-openshell.sh fails.

Related Issue

Fixes #967

Changes

  • Added isOpenshellCLI() in bin/lib/resolve-openshell.js — runs openshell --version and verifies output matches openshell <version>
  • Both command -v lookup and fallback candidate paths now verified with isOpenshellCLI()
  • Added opts.checkCLI DI override for testability
  • installOpenshell() in bin/lib/onboard.js now prints manual install commands on failure

Type of Change

  • Code change for a new feature, bug fix, or refactor.

Testing

  • Verified isOpenshellCLI() correctly identifies the OpenShell CLI binary (openshell --versionopenshell x.y.z)
  • Verified isOpenshellCLI() rejects the npm gateway package
  • Verified installOpenshell() prints manual install steps on failure

Checklist

General

Code Changes

  • No secrets, API keys, or credentials committed.

Summary by CodeRabbit

  • Improvements
    • When automatic OpenShell CLI installation fails, users now receive clearer, step-by-step manual-install guidance and copy/paste commands to complete installation.
    • The app now verifies candidate OpenShell CLI binaries before use and accepts an override for the verification step, reducing false positives and improving reliability.

- Add isOpenshellCLI() check to verify the resolved binary is the real OpenShell CLI (via --version output), not the npm gateway package

- Surface clear error message with manual install steps when install-openshell.sh fails

Fixes NVIDIA#967
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

📝 Walkthrough

Walkthrough

Adds CLI verification to resolve candidate OpenShell binaries by invoking --version, and expands install failure output in installOpenshell() to print manual download/extract/install commands when the OpenShell CLI binary fails to install.

Changes

Cohort / File(s) Summary
Install failure messaging
bin/lib/onboard.js
Expanded installOpenshell() failure branch to emit additional stderr lines with a blank line and explicit manual-install instructions (curl download of musl tarball, tar extract, install to /usr/local/bin/openshell).
CLI verification & resolution
bin/lib/resolve-openshell.js
Added exported isOpenshellCLI(binPath) which runs binPath --version and matches /^openshell\s+\d+/. resolveOpenshell() now uses `opts.checkCLI

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Caller Code
    participant Resolve as resolveOpenshell()
    participant CheckExec as checkExecutable()
    participant CheckCLI as isOpenshellCLI()
    participant Binary as openshell Binary

    Caller->>Resolve: resolveOpenshell(opts)
    loop for each candidate path
        Resolve->>CheckExec: checkExecutable(path)
        CheckExec-->>Resolve: executable?
        alt executable
            Resolve->>CheckCLI: opts.checkCLI || isOpenshellCLI(path)
            CheckCLI->>Binary: path --version
            Binary-->>CheckCLI: version output
            CheckCLI-->>Resolve: matches /^openshell\s+\d+/? (true/false)
            alt matches
                Resolve-->>Caller: return verified path
            else no match
                Resolve->>Resolve: continue to next candidate
            end
        else not executable
            Resolve->>Resolve: continue to next candidate
        end
    end
    alt no valid candidate found
        Resolve-->>Caller: return null
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A tiny check, a version peep,
I sniffed the PATH where binaries sleep.
If install trips and things go wrong,
I print the steps to fix it strong.
Hop on, openshell—now we belong! 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: validating the openshell binary to prevent npm package shadowing, which matches the primary objective of the PR.
Linked Issues check ✅ Passed The PR fully addresses both objectives from issue #967: (1) install-openshell.sh failures are now surfaced with manual install commands, and (2) the resolved openshell binary is verified to be the Rust CLI, not the npm gateway package.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issue: enhanced error messaging in onboard.js and binary validation logic in resolve-openshell.js, with no unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
bin/lib/resolve-openshell.js (1)

54-56: ⚠️ Potential issue | 🟠 Major

commandVResult override path skips CLI validation.

Lines [54]-[56] return opts.commandVResult without checkCLI(...), which bypasses the new binary-type guard in DI/test mode.

Suggested fix
-  } else if (opts.commandVResult && opts.commandVResult.startsWith("/")) {
-    return opts.commandVResult;
+  } else if (opts.commandVResult && opts.commandVResult.startsWith("/")) {
+    if (checkCLI(opts.commandVResult)) return opts.commandVResult;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/lib/resolve-openshell.js` around lines 54 - 56, The branch that returns
opts.commandVResult when it starts with "/" bypasses the CLI validation; update
this path to pass opts.commandVResult through checkCLI(...) before returning so
the new binary-type guard runs in DI/test mode—specifically, locate the branch
handling opts.commandVResult (the code that currently returns
opts.commandVResult) and replace the bare return with a return of
checkCLI(opts.commandVResult) (or otherwise validate via the existing checkCLI
helper) so checkCLI is applied consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bin/lib/resolve-openshell.js`:
- Around line 17-22: Replace the unsafe shell interpolation of binPath
(execSync(`"${binPath}" --version`)) with a direct process execution using
execFileSync and argv (e.g., execFileSync(binPath, ['--version'], ...)) to
eliminate command injection risks and preserve encoding/timeout/stdio options;
also ensure the DI-injected path returned as commandVResult is validated by
calling the existing checkCLI function (the same validation used for the
`command -v` branch) before returning it to avoid false-positive resolutions.

---

Outside diff comments:
In `@bin/lib/resolve-openshell.js`:
- Around line 54-56: The branch that returns opts.commandVResult when it starts
with "/" bypasses the CLI validation; update this path to pass
opts.commandVResult through checkCLI(...) before returning so the new
binary-type guard runs in DI/test mode—specifically, locate the branch handling
opts.commandVResult (the code that currently returns opts.commandVResult) and
replace the bare return with a return of checkCLI(opts.commandVResult) (or
otherwise validate via the existing checkCLI helper) so checkCLI is applied
consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f307f723-e422-4169-be3b-13d4b5685e2d

📥 Commits

Reviewing files that changed from the base of the PR and between f0f53e4 and 9785e4f.

📒 Files selected for processing (2)
  • bin/lib/onboard.js
  • bin/lib/resolve-openshell.js

Comment on lines +17 to +22
const out = execSync(`"${binPath}" --version`, {
encoding: "utf-8",
timeout: 5000,
stdio: ["ignore", "pipe", "ignore"],
}).trim();
return /^openshell\s+\d+/.test(out);
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C3 'execSync\(`"\$\{binPath\}" --version`' bin/lib/resolve-openshell.js

Repository: NVIDIA/NemoClaw

Length of output: 268


🏁 Script executed:

cat -n bin/lib/resolve-openshell.js | head -80

Repository: NVIDIA/NemoClaw

Length of output: 3189


Fix command injection vulnerability and add CLI validation to DI override path.

Line 17 interpolates binPath into a shell command string, which is vulnerable if the path contains shell metacharacters. Execute the binary directly with argv using execFileSync. Additionally, lines 54–56 return the DI-injected commandVResult without validating it with checkCLI, unlike the real command -v path at line 52, which creates an inconsistency that could produce false-positive resolution in tests.

Suggested fixes
-const { execSync } = require("child_process");
+const { execSync, execFileSync } = require("child_process");

At line 17:

-    const out = execSync(`"${binPath}" --version`, {
+    const out = execFileSync(binPath, ["--version"], {
       encoding: "utf-8",
       timeout: 5000,
       stdio: ["ignore", "pipe", "ignore"],
     }).trim();

At lines 54–56:

   } else if (opts.commandVResult && opts.commandVResult.startsWith("/")) {
-    return opts.commandVResult;
+    if (checkCLI(opts.commandVResult)) return opts.commandVResult;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/lib/resolve-openshell.js` around lines 17 - 22, Replace the unsafe shell
interpolation of binPath (execSync(`"${binPath}" --version`)) with a direct
process execution using execFileSync and argv (e.g., execFileSync(binPath,
['--version'], ...)) to eliminate command injection risks and preserve
encoding/timeout/stdio options; also ensure the DI-injected path returned as
commandVResult is validated by calling the existing checkCLI function (the same
validation used for the `command -v` branch) before returning it to avoid
false-positive resolutions.

@wscurran wscurran added bug Something isn't working fix labels Mar 30, 2026
@wscurran
Copy link
Copy Markdown
Contributor

✨ Thanks for submitting this fix with a detailed summary, it identifies a bug in the OpenShell binary validation and proposes a solution to prevent npm package shadowing, which could improve the stability and reliability of NemoClaw.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bin/lib/onboard.js`:
- Around line 1386-1393: The fallback OpenShell manual-install message and
commands are platform- and arch-incorrect and ignore install permissions; update
the fallback logic in the OpenShell install block (where the manual
curl/tar/install lines are emitted) to mirror scripts/install-openshell.sh:
detect OS (Darwin vs Linux) and choose the correct release asset prefix, map
uname -m to release arch names (e.g., amd64→x86_64, arm64→aarch64), and check
writability of /usr/local/bin (fall back to ~/.local/bin if not writable) before
printing the manual install commands so the curl URL and target path are correct
for the user’s platform and permissions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 88dd1a39-d3d2-4628-8e02-c34777ed9d22

📥 Commits

Reviewing files that changed from the base of the PR and between 9785e4f and ccc87e2.

📒 Files selected for processing (1)
  • bin/lib/onboard.js

Comment on lines +1386 to +1393
console.error("");
console.error(" OpenShell CLI binary failed to install.");
console.error(" You can install it manually:");
console.error("");
console.error(' curl -fsSL "https://github.com/NVIDIA/OpenShell/releases/latest/download/openshell-$(uname -m)-unknown-linux-musl.tar.gz" -o /tmp/openshell.tar.gz');
console.error(" tar xzf /tmp/openshell.tar.gz -C /tmp");
console.error(" install -m 755 /tmp/openshell /usr/local/bin/openshell");
console.error("");
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Changed fallback block in bin/lib/onboard.js =="
nl -ba bin/lib/onboard.js | sed -n '1378,1405p'

echo
echo "== Locate install-openshell.sh =="
SCRIPT_PATH="$(fd -HI 'install-openshell\.sh$' | head -n1)"
echo "$SCRIPT_PATH"

echo
echo "== Installer script (first 260 lines) =="
nl -ba "$SCRIPT_PATH" | sed -n '1,260p'

echo
echo "== Platform/arch handling tokens in installer =="
rg -n 'darwin|linux|musl|uname -m|uname -s|aarch64|arm64|x86_64|amd64|release' "$SCRIPT_PATH"

Repository: NVIDIA/NemoClaw

Length of output: 169


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Changed fallback block in bin/lib/onboard.js (lines 1378-1405) =="
cat -n bin/lib/onboard.js | sed -n '1378,1405p'

echo ""
echo "== Locate install-openshell.sh =="
SCRIPT_PATH="$(fd -HI 'install-openshell\.sh$' | head -n1)"
echo "Found: $SCRIPT_PATH"

echo ""
echo "== Installer script (first 300 lines) =="
cat -n "$SCRIPT_PATH" | sed -n '1,300p'

echo ""
echo "== Platform/arch handling tokens in installer =="
rg -n 'darwin|linux|musl|uname|aarch64|arm64|x86_64|amd64|release' "$SCRIPT_PATH" | head -30

Repository: NVIDIA/NemoClaw

Length of output: 6127


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for installOpenshell invocation points =="
rg -n 'installOpenshell' bin/lib/onboard.js | head -20

echo ""
echo "== Check if onboard.js is platform-specific or universal =="
rg -n -A5 'function installOpenshell\|const installOpenshell' bin/lib/onboard.js | head -30

echo ""
echo "== Check for any macOS exclusions in onboarding logic =="
rg -n 'Darwin|uname -s|process.platform.*darwin|process.platform.*win32' bin/lib/onboard.js | head -20

Repository: NVIDIA/NemoClaw

Length of output: 254


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Context around installOpenshell call (line 1532) =="
cat -n bin/lib/onboard.js | sed -n '1520,1545p'

echo ""
echo "== Function signature and early lines of installOpenshell =="
cat -n bin/lib/onboard.js | sed -n '1373,1395p'

echo ""
echo "== Check broader context for platform filtering =="
cat -n bin/lib/onboard.js | sed -n '1500,1550p'

echo ""
echo "== Check if process.platform is used to gate onboarding steps =="
rg -n 'process\.platform|process\.env\.OSTYPE' bin/lib/onboard.js | head -20

Repository: NVIDIA/NemoClaw

Length of output: 5528


Fallback install instructions are platform-incompatible and may fail due to incorrect asset naming and missing permission checks.

The hardcoded manual install commands (lines 1390–1392) assume Linux-musl and will fail on macOS where NemoClaw onboarding is supported. The asset name uses $(uname -m) directly, but uname -m returns amd64 or arm64 on some systems, whereas OpenShell releases use x86_64 and aarch64. Additionally, the fallback always attempts installation to /usr/local/bin without checking write permissions; the installer script properly handles this by falling back to ~/.local/bin if needed.

Align the fallback logic with scripts/install-openshell.sh by:

  1. Detecting OS (Darwin vs. Linux) and mapping to correct asset naming
  2. Mapping architecture labels (uname -m → release-compatible names)
  3. Checking /usr/local/bin writability; fall back to ~/.local/bin if needed
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/lib/onboard.js` around lines 1386 - 1393, The fallback OpenShell
manual-install message and commands are platform- and arch-incorrect and ignore
install permissions; update the fallback logic in the OpenShell install block
(where the manual curl/tar/install lines are emitted) to mirror
scripts/install-openshell.sh: detect OS (Darwin vs Linux) and choose the correct
release asset prefix, map uname -m to release arch names (e.g., amd64→x86_64,
arm64→aarch64), and check writability of /usr/local/bin (fall back to
~/.local/bin if not writable) before printing the manual install commands so the
curl URL and target path are correct for the user’s platform and permissions.

Dongni-Yang added a commit to Dongni-Yang/NemoClaw that referenced this pull request Apr 8, 2026
…A#506)

The npmjs.org `nemoclaw` package (0.1.0) is a broken 249-byte placeholder
that clobbers the real CLI. This commit:

- Removes the bare `npm install -g nemoclaw` reference from docs
- Adds `is_real_nemoclaw_cli()` behavioural check (mirrors PR NVIDIA#970
  isOpenshellCLI pattern) — verifies `nemoclaw --version` outputs
  `nemoclaw v<semver>`
- Adds `remove_broken_nemoclaw()` to auto-detect and uninstall any
  globally installed nemoclaw that fails the behavioural check
- Updates `verify_nemoclaw()` to validate every candidate binary
- Adds test for placeholder detection and updates all existing fake
  nemoclaw stubs to output the correct version format

Supersedes NVIDIA#761. Fixes NVIDIA#506.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dongni-Yang added a commit to Dongni-Yang/NemoClaw that referenced this pull request Apr 9, 2026
…A#506)

The npmjs.org `nemoclaw` package (0.1.0) is a broken 249-byte placeholder
that clobbers the real CLI. This commit:

- Removes the bare `npm install -g nemoclaw` reference from docs
- Adds `is_real_nemoclaw_cli()` behavioural check (mirrors PR NVIDIA#970
  isOpenshellCLI pattern) — verifies `nemoclaw --version` outputs
  `nemoclaw v<semver>`
- Adds `remove_broken_nemoclaw()` to auto-detect and uninstall any
  globally installed nemoclaw that fails the behavioural check
- Updates `verify_nemoclaw()` to validate every candidate binary
- Adds test for placeholder detection and updates all existing fake
  nemoclaw stubs to output the correct version format

Supersedes NVIDIA#761. Fixes NVIDIA#506.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
miyoungc pushed a commit that referenced this pull request Apr 9, 2026
## Summary

- Remove bare `npm install -g nemoclaw` from docs — it points to a
broken 249-byte placeholder on npmjs.org that only contains package.json
- Add `is_real_nemoclaw_cli()` behavioural validation to
`verify_nemoclaw()` — runs `nemoclaw --version` and verifies the output
matches `nemoclaw v<semver>` (mirrors the `isOpenshellCLI()` pattern
from PR #970)
- Update `verify_nemoclaw()` to validate every candidate binary before
accepting it, and auto-uninstall if the binary fails the check

Supersedes #761 (docs-only fix, now stale with merge conflicts).

Fixes #506
Relates to #737, #967

## Test plan

Updated all existing fake nemoclaw stubs to output `nemoclaw v<version>`
format so they pass `is_real_nemoclaw_cli()`. Existing test `prints the
HTTPS GitHub remediation when the binary is missing` still asserts bare
`npm install -g nemoclaw` never appears in output.

Signed-off-by: Dongni Yang <dongniy@nvidia.com>

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Updated CLI installation docs to show the shell-script installer (curl
... | bash) as the recommended method instead of npm global install.

* **Bug Fixes**
* Improved installer checks to verify the CLI binary is genuine and
functional.
* Added clearer recovery steps and warnings for broken or missing
installations to reduce installation failures.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Dongni Yang <dongniy@nvidia.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cv
Copy link
Copy Markdown
Contributor

cv commented Apr 9, 2026

I attempted to port this branch across the JS→TS migration and merge the latest main, but it still needs manual follow-up.

Please start with:

git fetch origin
git merge origin/main
npx tsx scripts/ts-migration-assist.ts --base origin/main --write
npm run build:cli
npm run typecheck:cli
npm run lint
npm test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

install.sh fails at step 3/7 — OpenShell CLI binary not installed, npm package shadows it

3 participants