fix(onboard): enforce nemoclaw-blueprint min_openshell_version at preflight (#1317)#1564
fix(onboard): enforce nemoclaw-blueprint min_openshell_version at preflight (#1317)#1564ColinM-sys wants to merge 1 commit intoNVIDIA:mainfrom
Conversation
…flight The blueprint declares 'min_openshell_version' but neither install.sh nor 'nemoclaw onboard' validated the installed OpenShell version against it. Users could complete a full install + onboard with an OpenShell that pre-dated required CLI surface (e.g. 'sandbox exec', '--upload') and then hit silent failures inside the sandbox at runtime. This adds the missing preflight check: - bin/lib/onboard.js: introduce versionGte() and getBlueprintMinOpenshellVersion(). The reader is intentionally lenient - missing file, missing field, malformed YAML, non-string value, and off-shape strings (e.g. 'latest') all return null so a busted local blueprint cannot become a hard onboard blocker. Then in preflight(), if both installed and minimum versions are present and the installed one is below the minimum, abort with a clear upgrade message that points at the OpenShell releases page and offers a one-line uninstall command so the bundled installer can re-fetch a current build. - test/onboard.test.js: four regression tests covering versionGte ordering edge cases, the happy path, every reachable getter null branch (missing dir, missing field, malformed YAML, non-string value, off-shape string), and a sanity check against the real shipped blueprint.yaml so a future edit that drops or breaks the field is caught by CI rather than at a user's onboard time. Closes NVIDIA#1317
📝 WalkthroughWalkthroughThis PR adds OpenShell version validation to the onboarding preflight checks. It introduces semver comparison utilities and a blueprint reader to enforce minimum OpenShell version requirements, failing onboarding with an error if the installed version is below the configured minimum. Changes
Sequence DiagramsequenceDiagram
participant Onboard as Onboard Process
participant Preflight as Preflight Step
participant CLI as OpenShell CLI
participant FS as File System
participant Compare as Version Comparison
Onboard->>Preflight: Start preflight checks
Preflight->>CLI: Execute 'openshell --version'
CLI-->>Preflight: Return version string (e.g., 0.0.20)
Preflight->>FS: Read blueprint.yaml
FS-->>Preflight: Return YAML content
Preflight->>Preflight: Parse installed version
Preflight->>Preflight: Extract min_openshell_version from blueprint
Preflight->>Compare: Compare(installed, minimum)
alt Version >= Minimum
Compare-->>Preflight: true
Preflight-->>Onboard: Continue onboarding
else Version < Minimum
Compare-->>Preflight: false
Preflight->>Onboard: Exit with error message
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
test/onboard.test.js (1)
337-396: Add one malformed-shape case with a numeric prefix (e.g.0.1.0-rc1).That case protects against prefix-matching validation and ensures malformed-but-prefix-valid values are treated as
null(non-blocking), which is the intended lenient behavior.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/onboard.test.js` around lines 337 - 396, Add a new test case in the same spec that creates a temp "nemoclaw-blueprint" directory and writes a blueprint.yaml with min_openshell_version: "0.1.0-rc1" (a numeric prefix plus suffix), then call getBlueprintMinOpenshellVersion(theTempDir) and assert it returns null, finally removing the temp dir in a finally block; this follows the pattern used for badShapeDir and ensures getBlueprintMinOpenshellVersion treats prefix-like malformed values (e.g. "0.1.0-rc1") as null.
🤖 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 388-395: The versionGte function currently uses parseInt which
partially accepts non-numeric suffixes; instead validate each dot-separated
segment with a strict /^\d+$/ check and consider a version malformed if any
segment fails that test; then implement lenient/null behavior: if the minimum
version (the right parameter, e.g., min_openshell_version) is malformed, return
true to avoid enforcing a bogus minimum, and if the tested version (left) is
malformed return false (cannot confirm it meets the minimum). Update versionGte
(and the same logic used around the code at the other affected spots) to perform
these strict numeric checks before comparing segments.
---
Nitpick comments:
In `@test/onboard.test.js`:
- Around line 337-396: Add a new test case in the same spec that creates a temp
"nemoclaw-blueprint" directory and writes a blueprint.yaml with
min_openshell_version: "0.1.0-rc1" (a numeric prefix plus suffix), then call
getBlueprintMinOpenshellVersion(theTempDir) and assert it returns null, finally
removing the temp dir in a finally block; this follows the pattern used for
badShapeDir and ensures getBlueprintMinOpenshellVersion treats prefix-like
malformed values (e.g. "0.1.0-rc1") as null.
🪄 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: 6733bb0e-2351-4c1f-814f-728af2e20ac5
📒 Files selected for processing (2)
bin/lib/onboard.jstest/onboard.test.js
| function versionGte(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); |
There was a problem hiding this comment.
Tighten version parsing/validation so malformed blueprint values can’t accidentally enforce a minimum.
Right now, min_openshell_version values with a valid prefix (e.g. 0.1.0-foo) pass the regex and can still influence gating. That conflicts with the intended “lenient/null on malformed” behavior. Also, parseInt partially parses non-numeric parts, which doesn’t match the function comment.
💡 Proposed fix
function versionGte(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 toParts = (value) =>
+ String(value)
+ .split(".")
+ .map((part) => (/^[0-9]+$/.test(part) ? Number(part) : 0));
+ const lhs = toParts(left);
+ const rhs = toParts(right);
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 (!/^[0-9]+\.[0-9]+\.[0-9]+/.test(trimmed)) return null;
+ if (!/^[0-9]+\.[0-9]+\.[0-9]+$/.test(trimmed)) return null;
return trimmed;Also applies to: 425-426
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@bin/lib/onboard.js` around lines 388 - 395, The versionGte function currently
uses parseInt which partially accepts non-numeric suffixes; instead validate
each dot-separated segment with a strict /^\d+$/ check and consider a version
malformed if any segment fails that test; then implement lenient/null behavior:
if the minimum version (the right parameter, e.g., min_openshell_version) is
malformed, return true to avoid enforcing a bogus minimum, and if the tested
version (left) is malformed return false (cannot confirm it meets the minimum).
Update versionGte (and the same logic used around the code at the other affected
spots) to perform these strict numeric checks before comparing segments.
|
✨ Thanks for submitting this fix, which proposes a way to enforce minimum OpenShell version requirements during preflight by reading the blueprint constraint. This helps prevent silent feature failures caused by outdated installations. |
Summary
Closes #1317.
nemoclaw-blueprint/blueprint.yamldeclaresmin_openshell_version: "0.1.0"but neitherinstall.shnornemoclaw onboardever validate the installed OpenShell against it. Users can complete a full install + onboard againstopenshell 0.0.20, then hit silent failures inside the sandbox at runtime when NemoClaw tries to call CLI surface that doesn't exist in the older binary (e.g.sandbox exec,--upload, divergent device-pairing behavior). The reporter saw a successful onboard followed by features quietly breaking with no clear cause.Fix
Add the missing preflight check.
bin/lib/onboard.jsTwo new helpers:
versionGte(left, right)— compare semver-likex.y.zstrings, defaulting missing components to 0. Mirrors the helper already present inbin/nemoclaw.js(kept independent rather than refactored to avoid touching unrelated callers in this PR).getBlueprintMinOpenshellVersion(rootDir = ROOT)— readnemoclaw-blueprint/blueprint.yaml, extractmin_openshell_version. Intentionally lenient: missing file, missing field, malformed YAML, non-string value, and off-shape strings (e.g."latest") all returnnull. This is so a busted local blueprint can never become a hard onboard blocker — the check only fires when the constraint is unambiguously legible.In
preflight(), immediately after the existing openshell version detection, compare installed against the blueprint minimum. If both are present and the installed version is below the minimum, abort with an actionable upgrade message:The second hint is the important one: it gives the user a one-liner that lets the bundled installer fetch a current build on the next run, rather than forcing them to figure out the manual upgrade path.
test/onboard.test.jsFour regression tests:
versionGteordering edge cases — equal, greater, lesser, missing components, empty string defaults.getBlueprintMinOpenshellVersionhappy path — point at a temp blueprint withmin_openshell_version: "0.1.0", assert"0.1.0"comes back.getBlueprintMinOpenshellVersionreturns null on every reachable failure mode — missing directory, missing field, malformed YAML, non-string value (min_openshell_version: 1.5), off-shape string (min_openshell_version: "latest").getBlueprintMinOpenshellVersion(repoRoot)must return a parseablex.y.zstring. Catches a future edit that accidentally drops or breaks the field, at CI time rather than at a user's onboard time.Test plan
vitest run test/onboard.test.js -t "1317"— 4/4 new regression tests pass.vitest run test/onboard.test.js -t "openshell"— pre-existing openshell tests still pass; same 2 unrelated failures appear onmainwith or without this change (verified by stash + re-run).blueprint.yamlfrom this branch with a Node REPL:getBlueprintMinOpenshellVersion()returns"0.1.0", and feeding"openshell 0.0.20"into the samegetInstalledOpenshellVersion()→versionGte()chain thatpreflight()uses correctly returnsfalse(would abort), while"openshell 0.1.0","0.1.5", and"1.0.0"correctly returntrue(would allow onboard).nemoclaw onboardagainst an actualopenshell 0.0.20install on a Linux box. The helpers are unit-tested in isolation and the wiring point inpreflight()reuses the same version output already captured for the existing display line, so the integration should follow, but a maintainer or reporter with the original repro environment is welcome to confirm.Why this approach over silently upgrading openshell automatically
install.shalready attempts to install openshell when none is present, but it does not currently replace an existing-but-too-old binary, and silently overwriting a binary the user installed manually is intrusive enough that it deserves a separate design discussion. The error message in this PR points users at both the manual upgrade page and a one-liner that removes the stale binary so the existing installer logic re-fetches a current build on the next run — effectively letting them opt into the auto-upgrade path with a single command, without making that the default behavior.Summary by CodeRabbit
New Features
Tests