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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ The sandbox image is approximately 2.4 GB compressed. During image push, the Doc
| Container runtime | Supported runtime installed and running |
| [OpenShell](https://github.com/NVIDIA/OpenShell) | Installed |

#### OpenShell Compatibility

NemoClaw 0.1.0 was validated with OpenShell 0.0.7.
During onboarding, NemoClaw derives the OpenShell gateway image from the installed `openshell` CLI version, so upgrading OpenShell independently can rebuild the sandbox with a different runtime layout than the current NemoClaw release expects.

| NemoClaw | OpenShell | Notes |
|----------|-----------|-------|
| 0.1.0 | 0.0.7 | Use `nemoclaw onboard` to create or recreate NemoClaw-managed sandboxes. Avoid `openshell self-update`, `npm update -g openshell`, `openshell gateway start --recreate`, or `openshell sandbox create` directly unless you intend to manage OpenShell separately and then re-onboard. |

#### Container Runtimes

| Platform | Supported runtimes | Notes |
Expand Down
64 changes: 63 additions & 1 deletion bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const os = require("os");
const path = require("path");
const { spawn, spawnSync } = require("child_process");
const pRetry = require("p-retry");
const { version: NEMOCLAW_VERSION } = require("../../package.json");

/** Parse a numeric env var, returning `fallback` when unset or non-finite. */
function envInt(name, fallback) {
Expand Down Expand Up @@ -88,6 +89,46 @@ function cleanupTempDir(filePath, expectedPrefix) {
}

const EXPERIMENTAL = process.env.NEMOCLAW_EXPERIMENTAL === "1";
const OPENSHELL_COMPATIBILITY = Object.freeze({
"0.1.0": Object.freeze(["0.0.7"]),
});

function parseSemver(version) {
const match = String(version || "")
.trim()
.match(/^(\d+)\.(\d+)\.(\d+)$/);
if (!match) return null;
return match.slice(1).map((part) => Number(part));
}

function isExactSemverMatch(version, supportedVersion) {
const actual = parseSemver(version);
const supported = parseSemver(supportedVersion);
return (
Array.isArray(actual) &&
Array.isArray(supported) &&
actual[0] === supported[0] &&
actual[1] === supported[1] &&
actual[2] === supported[2]
);
}

function getSupportedOpenshellVersions(nemoclawVersion = NEMOCLAW_VERSION) {
return OPENSHELL_COMPATIBILITY[nemoclawVersion] || [];
}

function isSupportedOpenshellVersion(version, supportedVersions = getSupportedOpenshellVersions()) {
if (!version || supportedVersions.length === 0) return false;
return supportedVersions.some((supportedVersion) =>
isExactSemverMatch(version, supportedVersion),
);
}

function formatSupportedOpenshellVersions(supportedVersions) {
if (supportedVersions.length === 0) return null;
if (supportedVersions.length === 1) return supportedVersions[0];
return `${supportedVersions.slice(0, -1).join(", ")}, or ${supportedVersions.at(-1)}`;
}
const USE_COLOR = !process.env.NO_COLOR && !!process.stdout.isTTY;
const DIM = USE_COLOR ? "\x1b[2m" : "";
const RESET = USE_COLOR ? "\x1b[0m" : "";
Expand Down Expand Up @@ -583,6 +624,23 @@ function getStableGatewayImageRef(versionOutput = null) {
return `ghcr.io/nvidia/openshell/cluster:${version}`;
}

function getOpenshellCompatibilityNotice(version = null) {
const supportedVersions = getSupportedOpenshellVersions();
const supportedLabel = formatSupportedOpenshellVersions(supportedVersions);
if (version && supportedLabel && !isSupportedOpenshellVersion(version, supportedVersions)) {
return [
` OpenShell compatibility warning: NemoClaw ${NEMOCLAW_VERSION} was validated with OpenShell ${supportedLabel}, but found ${version}.`,
` Install OpenShell ${supportedLabel}, then rerun \`nemoclaw onboard\` before creating or reusing NemoClaw-managed sandboxes.`,
" Avoid `openshell self-update`, `openshell gateway start --recreate`, or `openshell sandbox create` directly unless you are also updating the NemoClaw compatibility baseline.",
];
}
const suffix = version ? ` (${version})` : "";
return [
` OpenShell compatibility: NemoClaw derives the gateway image from the installed openshell CLI${suffix}.`,
" Use `nemoclaw onboard` to recreate NemoClaw-managed sandboxes, and avoid `openshell self-update`, `openshell gateway start --recreate`, or `openshell sandbox create` directly.",
];
}

function getOpenshellBinary() {
if (OPENSHELL_BIN) return OPENSHELL_BIN;
const resolved = resolveOpenshell();
Expand Down Expand Up @@ -2212,6 +2270,10 @@ async function preflight() {

async function startGatewayWithOptions(_gpu, { exitOnFailure = true } = {}) {
step(2, 7, "Starting OpenShell gateway");
const gatewayEnv = getGatewayStartEnv();
for (const line of getOpenshellCompatibilityNotice(gatewayEnv.IMAGE_TAG || null)) {
console.log(line);
}

const gatewayStatus = runCaptureOpenshell(["status"], { ignoreError: true });
const gwInfo = runCaptureOpenshell(["gateway", "info", "-g", GATEWAY_NAME], {
Expand Down Expand Up @@ -2239,7 +2301,6 @@ async function startGatewayWithOptions(_gpu, { exitOnFailure = true } = {}) {
// sandbox itself does not need direct GPU access. Passing --gpu causes
// FailedPrecondition errors when the gateway's k3s device plugin cannot
// allocate GPUs. See: https://build.nvidia.com/spark/nemoclaw/instructions
const gatewayEnv = getGatewayStartEnv();
if (gatewayEnv.OPENSHELL_CLUSTER_IMAGE) {
console.log(` Using pinned OpenShell gateway image: ${gatewayEnv.OPENSHELL_CLUSTER_IMAGE}`);
}
Expand Down Expand Up @@ -4184,6 +4245,7 @@ module.exports = {
getGatewayStartEnv,
getGatewayReuseState,
getSandboxInferenceConfig,
getOpenshellCompatibilityNotice,
getInstalledOpenshellVersion,
getRequestedModelHint,
getRequestedProviderHint,
Expand Down
6 changes: 3 additions & 3 deletions docs/about/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ Respect CLI boundaries
Supply chain safety
: Blueprint artifacts are immutable, versioned, and digest-verified before execution.

OpenShell-native for new installs
: For users without an existing OpenClaw installation, NemoClaw recommends `openshell sandbox create` directly
rather than forcing a plugin-driven bootstrap.
OpenShell-backed lifecycle
: NemoClaw orchestrates OpenShell resources under the hood, but `nemoclaw onboard`
is the supported operator entry point for creating or recreating NemoClaw-managed sandboxes.

Reproducible setup
: Running setup again recreates the sandbox from the same blueprint and policy definitions.
Expand Down
9 changes: 9 additions & 0 deletions docs/get-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ The sandbox image is approximately 2.4 GB compressed. During image push, the Doc
| Container runtime | Supported runtime installed and running |
| [OpenShell](https://github.com/NVIDIA/OpenShell) | Installed |

:::{warning} OpenShell compatibility
NemoClaw 0.1.0 was validated with OpenShell 0.0.7.
During onboarding, NemoClaw derives the OpenShell gateway image from the installed `openshell` CLI version, so upgrading OpenShell independently can rebuild the sandbox with a different runtime layout than the current NemoClaw release expects.
:::

| NemoClaw | OpenShell | Notes |
|----------|-----------|-------|
| 0.1.0 | 0.0.7 | Use `nemoclaw onboard` to create or recreate NemoClaw-managed sandboxes. Avoid `openshell self-update`, `npm update -g openshell`, `openshell gateway start --recreate`, or `openshell sandbox create` directly unless you intend to manage OpenShell separately and then re-onboard. |

### Container Runtimes

| Platform | Supported runtimes | Notes |
Expand Down
5 changes: 5 additions & 0 deletions docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ Use this command for new installs and for recreating a sandbox after changes to
$ nemoclaw onboard
```

:::{warning}
For NemoClaw-managed environments, use `nemoclaw onboard` when you need to create or recreate the OpenShell gateway or sandbox.
NemoClaw derives the gateway image from the installed `openshell` CLI version, so `openshell self-update`, `npm update -g openshell`, `openshell gateway start --recreate`, or `openshell sandbox create` can replace the runtime with a different OpenShell release than NemoClaw 0.1.0 was validated against.
:::

The wizard prompts for a provider first, then collects the provider credential if needed.
Supported non-experimental choices include NVIDIA Endpoints, OpenAI, Anthropic, Google Gemini, and compatible OpenAI or Anthropic endpoints.
Credentials are stored in `~/.nemoclaw/credentials.json`.
Expand Down
34 changes: 34 additions & 0 deletions test/onboard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getGatewayReuseState,
getPortConflictServiceHints,
getFutureShellPathHint,
getOpenshellCompatibilityNotice,
getSandboxInferenceConfig,
getInstalledOpenshellVersion,
getRequestedModelHint,
Expand All @@ -35,6 +36,8 @@ import {
} from "../bin/lib/onboard";
import { buildWebSearchDockerConfig } from "../dist/lib/web-search";

const ROOT = path.resolve(import.meta.dirname, "..");

describe("onboard helpers", () => {
it("classifies sandbox create timeout failures and tracks upload progress", () => {
expect(
Expand Down Expand Up @@ -306,6 +309,37 @@ describe("onboard helpers", () => {
expect(getStableGatewayImageRef("bogus")).toBe(null);
});

it("prints a compatibility note when the gateway version is pinned", () => {
expect(getOpenshellCompatibilityNotice("0.0.7")).toEqual([
" OpenShell compatibility: NemoClaw derives the gateway image from the installed openshell CLI (0.0.7).",
" Use `nemoclaw onboard` to recreate NemoClaw-managed sandboxes, and avoid `openshell self-update`, `openshell gateway start --recreate`, or `openshell sandbox create` directly.",
]);
expect(getOpenshellCompatibilityNotice()).toEqual([
" OpenShell compatibility: NemoClaw derives the gateway image from the installed openshell CLI.",
" Use `nemoclaw onboard` to recreate NemoClaw-managed sandboxes, and avoid `openshell self-update`, `openshell gateway start --recreate`, or `openshell sandbox create` directly.",
]);
});

it("warns when the installed OpenShell version falls outside the supported baseline", () => {
expect(getOpenshellCompatibilityNotice("0.0.8")).toEqual([
" OpenShell compatibility warning: NemoClaw 0.1.0 was validated with OpenShell 0.0.7, but found 0.0.8.",
" Install OpenShell 0.0.7, then rerun `nemoclaw onboard` before creating or reusing NemoClaw-managed sandboxes.",
" Avoid `openshell self-update`, `openshell gateway start --recreate`, or `openshell sandbox create` directly unless you are also updating the NemoClaw compatibility baseline.",
]);
});

it("prints the compatibility notice before the healthy gateway reuse early return", () => {
const content = fs.readFileSync(path.join(ROOT, "bin/lib/onboard.js"), "utf-8");
const startGwBlock = content.match(/async function startGatewayWithOptions[\s\S]*?^}/m);
expect(startGwBlock).toBeTruthy();

const noticeIndex = startGwBlock[0].indexOf("getOpenshellCompatibilityNotice(");
const healthyCheckIndex = startGwBlock[0].indexOf("if (isGatewayHealthy(");
expect(noticeIndex).toBeGreaterThanOrEqual(0);
expect(healthyCheckIndex).toBeGreaterThanOrEqual(0);
expect(noticeIndex).toBeLessThan(healthyCheckIndex);
});

it("treats the gateway as healthy only when nemoclaw is running and connected", () => {
expect(
isGatewayHealthy(
Expand Down