Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
bb53acb
fix(onboard): color preflight WARN/ERROR check lines (#6004)
jason-ma-nv Jun 30, 2026
b1096dd
fix(onboard): apply warnLine to bridge container start probe inconclu…
prekshivyas Jul 1, 2026
6f58de2
fix(onboard): route image-pull DNS warning through warnLine (#6004)
jason-ma-nv Jul 2, 2026
a7fbec4
Merge remote-tracking branch 'origin/main' into fix/6004-preflight-wa…
jason-ma-nv Jul 3, 2026
3c9cb0c
fix(onboard): color preflight WARN/ERROR by target stream via styleTe…
jason-ma-nv Jul 3, 2026
61a3ecc
fix(onboard): keep onboard.ts net-neutral under growth guardrail (#6004)
jason-ma-nv Jul 3, 2026
d004a26
fix(onboard): extract preflight severity messages so onboard.ts lines…
jason-ma-nv Jul 3, 2026
a3bbfe4
test(onboard): drop unused beforeEach import in terminal-style test (…
jason-ma-nv Jul 3, 2026
1bd7555
fix(onboard): color Docker-unreachable preflight
cv Jul 3, 2026
210c6a0
fix(onboard): tighten preflight severity helpers
cv Jul 3, 2026
7ee50d1
test(onboard): isolate severity regressions
cv Jul 3, 2026
bed601a
merge: refresh preflight severity fixes with main
cv Jul 3, 2026
e54e9c4
fix(onboard): complete stderr severity coverage
cv Jul 3, 2026
dc83915
merge: refresh preflight severity fixes with main
cv Jul 3, 2026
e074fd7
test(onboard): linearize stream restoration
cv Jul 3, 2026
5054368
fix(onboard): harden preflight severity output
apurvvkumaria Jul 4, 2026
ce84032
fix(onboard): color fatal CDI preflight
apurvvkumaria Jul 4, 2026
9e06a53
fix(onboard): color deferred gateway warning
apurvvkumaria Jul 4, 2026
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
6 changes: 3 additions & 3 deletions ci/platform-matrix.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
"name": "Local NVIDIA NIM",
"status": "experimental",
"endpoint_type": "Local OpenAI-compatible",
"notes": "Requires `NEMOCLAW_EXPERIMENTAL=1` and a NIM-capable NVIDIA GPU. Host must have the NVIDIA Container Toolkit installed and a CDI spec present (`onboard` asserts CDI presence with `assertCdiNvidiaGpuSpecPresent`, `src/lib/onboard/fatal-runtime-preflight.ts:78`). NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag because it does not auto-select a NIM image for the host today. You must explicitly pick from the validated image list. Managed vLLM has host-specific default models and is not gated on the same boxes. Validated images referenced in `src/lib/inference/config.ts` and `nemoclaw/src/index.ts`: `nvidia/nemotron-3-super-120b-a12b` (default cloud model), `nvidia/nemotron-3-nano-30b-a3b`, `nvidia/llama-3.3-nemotron-super-49b-v1.5`."
"notes": "Requires `NEMOCLAW_EXPERIMENTAL=1` and a NIM-capable NVIDIA GPU. Host must have the NVIDIA Container Toolkit installed and a CDI spec present (`onboard` asserts CDI presence with `assertCdiNvidiaGpuSpecPresent`, `src/lib/onboard/fatal-runtime-preflight.ts`). NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag because it does not auto-select a NIM image for the host today. You must explicitly pick from the validated image list. Managed vLLM has host-specific default models and is not gated on the same boxes. Validated images referenced in `src/lib/inference/config.ts` and `nemoclaw/src/index.ts`: `nvidia/nemotron-3-super-120b-a12b` (default cloud model), `nvidia/nemotron-3-nano-30b-a3b`, `nvidia/llama-3.3-nemotron-super-49b-v1.5`."
},
{
"name": "Local vLLM (already running)",
Expand Down Expand Up @@ -218,7 +218,7 @@
{
"name": "Podman / other container runtimes",
"status": "unsupported",
"notes": "Onboard surfaces an explicit unsupported-runtime error for Podman (`src/lib/onboard/fatal-runtime-preflight.ts:50` prints the rejection; `src/lib/onboard/preflight.ts:677` flags the unsupported runtime upstream). Only Docker Engine, Docker Desktop, and Colima are supported. See issue #420 (closed)."
"notes": "Onboard surfaces an explicit unsupported-runtime error for Podman (`src/lib/onboard/fatal-runtime-preflight.ts` prints the rejection; `src/lib/onboard/preflight.ts` flags the unsupported runtime upstream). Only Docker Engine, Docker Desktop, and Colima are supported. See issue #420 (closed)."
},
{
"name": "Intel Mac (macOS x86_64)",
Expand Down Expand Up @@ -248,7 +248,7 @@
{
"name": "Non-NVIDIA GPUs (AMD/ROCm, Intel Arc, Apple Metal)",
"status": "unsupported",
"notes": "Local vLLM and NIM paths assert NVIDIA CDI presence with `assertCdiNvidiaGpuSpecPresent` (`src/lib/onboard/fatal-runtime-preflight.ts:78`). NemoClaw does not install non-NVIDIA accelerator drivers."
"notes": "Local vLLM and NIM paths assert NVIDIA CDI presence with `assertCdiNvidiaGpuSpecPresent` (`src/lib/onboard/fatal-runtime-preflight.ts`). NemoClaw does not install non-NVIDIA accelerator drivers."
},
{
"name": "Other LangChain, AutoGen, CrewAI, or non-listed agent harnesses",
Expand Down
2 changes: 1 addition & 1 deletion docs/inference/inference-options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ NemoClaw uses provider-specific local tokens for those routes, and rebuilds of l
| Google Gemini | Tested | OpenAI-compatible | Uses Google's OpenAI-compatible endpoint |
| Hermes Provider | Hermes only | OpenAI-compatible route | Available when onboarding Hermes Agent through `nemohermes` |
| Local Ollama | Tested with limitations | Local Ollama API | Available when Ollama is installed or running on the host. Validated default models: `qwen3.6:35b` (high VRAM), `nemotron-3-nano:30b` (medium VRAM), `qwen3.5:9b` (low VRAM fallback). |
| Local NVIDIA NIM | Experimental | Local OpenAI-compatible | Requires `NEMOCLAW_EXPERIMENTAL=1` and a NIM-capable NVIDIA GPU. Host must have the NVIDIA Container Toolkit installed and a CDI spec present (`onboard` asserts CDI presence with `assertCdiNvidiaGpuSpecPresent`, `src/lib/onboard/fatal-runtime-preflight.ts:78`). NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag because it does not auto-select a NIM image for the host today. You must explicitly pick from the validated image list. Managed vLLM has host-specific default models and is not gated on the same boxes. Validated images referenced in `src/lib/inference/config.ts` and `nemoclaw/src/index.ts`: `nvidia/nemotron-3-super-120b-a12b` (default cloud model), `nvidia/nemotron-3-nano-30b-a3b`, `nvidia/llama-3.3-nemotron-super-49b-v1.5`. |
| Local NVIDIA NIM | Experimental | Local OpenAI-compatible | Requires `NEMOCLAW_EXPERIMENTAL=1` and a NIM-capable NVIDIA GPU. Host must have the NVIDIA Container Toolkit installed and a CDI spec present (`onboard` asserts CDI presence with `assertCdiNvidiaGpuSpecPresent`, `src/lib/onboard/fatal-runtime-preflight.ts`). NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag because it does not auto-select a NIM image for the host today. You must explicitly pick from the validated image list. Managed vLLM has host-specific default models and is not gated on the same boxes. Validated images referenced in `src/lib/inference/config.ts` and `nemoclaw/src/index.ts`: `nvidia/nemotron-3-super-120b-a12b` (default cloud model), `nvidia/nemotron-3-nano-30b-a3b`, `nvidia/llama-3.3-nemotron-super-49b-v1.5`. |
| Local vLLM (already running) | Tested with limitations | Local OpenAI-compatible | Appears in the onboarding menu when NemoClaw detects a server already on `localhost:8000`. No flag required. Model is whatever the existing server serves. |
| Local vLLM (managed install/start) | Tested with limitations | Local OpenAI-compatible | Appears by default on DGX Spark and DGX Station. Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`. Host must have the NVIDIA Container Toolkit installed and a CDI spec present (`onboard` asserts CDI presence). NemoClaw pulls or starts the stable NGC vLLM container for each host profile. See `src/lib/inference/vllm.ts:55,177` for the pins. DGX Spark and DGX Station use `nvcr.io/nvidia/vllm:26.05.post1-py3`; generic Linux NVIDIA GPU hosts use `nvcr.io/nvidia/vllm:26.03.post1-py3`. Validated defaults are listed in `src/lib/inference/vllm-models.ts`: DGX Spark uses `nvidia/Qwen3.6-35B-A3B-NVFP4`, DGX Station uses `deepseek-ai/DeepSeek-V4-Flash`, and Linux NVIDIA GPU uses `nvidia/NVIDIA-Nemotron-3-Nano-4B-FP8`. Image pulls require NGC registry login (`docker login nvcr.io`); onboard prompts for the NGC API key when authentication is missing. |
{/* provider-status:end */}
Expand Down
6 changes: 3 additions & 3 deletions docs/reference/platform-support.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ NemoClaw routes inference through the OpenShell gateway. Each row below is a pro
| Google Gemini | Tested | OpenAI-compatible | Uses Google's OpenAI-compatible endpoint |
| Hermes Provider | Hermes only | OpenAI-compatible route | Available when onboarding Hermes Agent through `nemohermes` |
| Local Ollama | Tested with limitations | Local Ollama API | Available when Ollama is installed or running on the host. Validated default models: `qwen3.6:35b` (high VRAM), `nemotron-3-nano:30b` (medium VRAM), `qwen3.5:9b` (low VRAM fallback). |
| Local NVIDIA NIM | Experimental | Local OpenAI-compatible | Requires `NEMOCLAW_EXPERIMENTAL=1` and a NIM-capable NVIDIA GPU. Host must have the NVIDIA Container Toolkit installed and a CDI spec present (`onboard` asserts CDI presence with `assertCdiNvidiaGpuSpecPresent`, `src/lib/onboard/fatal-runtime-preflight.ts:78`). NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag because it does not auto-select a NIM image for the host today. You must explicitly pick from the validated image list. Managed vLLM has host-specific default models and is not gated on the same boxes. Validated images referenced in `src/lib/inference/config.ts` and `nemoclaw/src/index.ts`: `nvidia/nemotron-3-super-120b-a12b` (default cloud model), `nvidia/nemotron-3-nano-30b-a3b`, `nvidia/llama-3.3-nemotron-super-49b-v1.5`. |
| Local NVIDIA NIM | Experimental | Local OpenAI-compatible | Requires `NEMOCLAW_EXPERIMENTAL=1` and a NIM-capable NVIDIA GPU. Host must have the NVIDIA Container Toolkit installed and a CDI spec present (`onboard` asserts CDI presence with `assertCdiNvidiaGpuSpecPresent`, `src/lib/onboard/fatal-runtime-preflight.ts`). NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag because it does not auto-select a NIM image for the host today. You must explicitly pick from the validated image list. Managed vLLM has host-specific default models and is not gated on the same boxes. Validated images referenced in `src/lib/inference/config.ts` and `nemoclaw/src/index.ts`: `nvidia/nemotron-3-super-120b-a12b` (default cloud model), `nvidia/nemotron-3-nano-30b-a3b`, `nvidia/llama-3.3-nemotron-super-49b-v1.5`. |
| Local vLLM (already running) | Tested with limitations | Local OpenAI-compatible | Appears in the onboarding menu when NemoClaw detects a server already on `localhost:8000`. No flag required. Model is whatever the existing server serves. |
| Local vLLM (managed install/start) | Tested with limitations | Local OpenAI-compatible | Appears by default on DGX Spark and DGX Station. Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`. Host must have the NVIDIA Container Toolkit installed and a CDI spec present (`onboard` asserts CDI presence). NemoClaw pulls or starts the stable NGC vLLM container for each host profile. See `src/lib/inference/vllm.ts:55,177` for the pins. DGX Spark and DGX Station use `nvcr.io/nvidia/vllm:26.05.post1-py3`; generic Linux NVIDIA GPU hosts use `nvcr.io/nvidia/vllm:26.03.post1-py3`. Validated defaults are listed in `src/lib/inference/vllm-models.ts`: DGX Spark uses `nvidia/Qwen3.6-35B-A3B-NVFP4`, DGX Station uses `deepseek-ai/DeepSeek-V4-Flash`, and Linux NVIDIA GPU uses `nvidia/NVIDIA-Nemotron-3-Nano-4B-FP8`. Image pulls require NGC registry login (`docker login nvcr.io`); onboard prompts for the NGC API key when authentication is missing. |
{/* provider-status-full:end */}
Expand Down Expand Up @@ -160,13 +160,13 @@ They are listed here so launch material, sales conversations, and support triage
{/* out-of-scope:begin */}
| Item | Status | Why |
|------|--------|-----|
| Podman / other container runtimes | Unsupported | Onboard surfaces an explicit unsupported-runtime error for Podman (`src/lib/onboard/fatal-runtime-preflight.ts:50` prints the rejection; `src/lib/onboard/preflight.ts:677` flags the unsupported runtime upstream). Only Docker Engine, Docker Desktop, and Colima are supported. See issue #420 (closed). |
| Podman / other container runtimes | Unsupported | Onboard surfaces an explicit unsupported-runtime error for Podman (`src/lib/onboard/fatal-runtime-preflight.ts` prints the rejection; `src/lib/onboard/preflight.ts` flags the unsupported runtime upstream). Only Docker Engine, Docker Desktop, and Colima are supported. See issue #420 (closed). |
| Intel Mac (macOS x86_64) | Unsupported | OpenShell does not publish macOS x86_64 standalone gateway assets. Install hard-fails on x86_64 macOS (`scripts/install-openshell.sh:663`). See issue #954 (closed). |
| Non-Ubuntu/Debian Linux distros | Unsupported | Installer assumes `apt-get`. Fedora/Rocky/Alma/Arch/NixOS are not validated and the installer's package-manager probes do not cover them. See open issue #899 (Fedora hang). |
| Native Kubernetes or OpenShift deployments | Unsupported | NemoClaw runs the sandbox as a Docker container, not a Kubernetes pod. The default Docker-driver topology does not embed k3s. Operator-managed K8s/OpenShift deployments are out of scope; see issue #407 (community OpenShift through agent-sandbox CRD). |
| Air-gapped / offline installs | Unsupported | Onboard assumes network reachability for package fetches, container pulls, and provider validation. See open issues #4872 and #2218 (production-deployment epic covering air-gapped support, China network guidance, multi-host topology). |
| Windows-on-ARM GPU passthrough | Unsupported | Windows-on-ARM CPU paths run under WSL2 'tested with limitations', but GPU passthrough on WOA is denylisted (`src/lib/onboard/wsl-docker-desktop-gpu.ts:188`, `src/lib/inference/gpu-trust.test.ts:70`). See closed issue #4565. |
| Non-NVIDIA GPUs (AMD/ROCm, Intel Arc, Apple Metal) | Unsupported | Local vLLM and NIM paths assert NVIDIA CDI presence with `assertCdiNvidiaGpuSpecPresent` (`src/lib/onboard/fatal-runtime-preflight.ts:78`). NemoClaw does not install non-NVIDIA accelerator drivers. |
| Non-NVIDIA GPUs (AMD/ROCm, Intel Arc, Apple Metal) | Unsupported | Local vLLM and NIM paths assert NVIDIA CDI presence with `assertCdiNvidiaGpuSpecPresent` (`src/lib/onboard/fatal-runtime-preflight.ts`). NemoClaw does not install non-NVIDIA accelerator drivers. |
| Other LangChain, AutoGen, CrewAI, or non-listed agent harnesses | Unsupported | LangChain Deep Agents Code is the only integrated LangChain-family harness (see the Agents section above; status `Experimental`). Other LangChain harnesses, AutoGen, CrewAI, and any agent runtime not listed in the Agents table are not integrated. Bringing more harnesses is tracked as a research epic (see open issue #4861) but is not on the current roadmap. |
| Multi-user host sharing | Unsupported | Sandboxes are scoped to a single host user. NemoClaw treats multi-user hosts as a risk and warns at onboard; see `docs/security/openclaw-controls.mdx` Multi-user detection. |
| Hosted SaaS / managed NemoClaw | Unsupported | There is no managed offering. Supported deployment paths are Local CLI onboard, Remote GPU with Brev CLI, and Brev web UI. |
Expand Down
101 changes: 99 additions & 2 deletions src/lib/cli/terminal-style.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";

import { B, D, G, R, RD, YW } from "./terminal-style";
import { B, D, failLine, G, R, RD, warnLine, YW } from "./terminal-style";

describe("terminal-style", () => {
it("exports terminal style strings", () => {
Expand All @@ -12,3 +12,100 @@ describe("terminal-style", () => {
}
});
});

const ORIGINAL_STDOUT = {
isTTY: process.stdout.isTTY,
getColorDepth: process.stdout.getColorDepth,
};
const ORIGINAL_STDERR = {
isTTY: process.stderr.isTTY,
getColorDepth: process.stderr.getColorDepth,
};

// styleText decides color from the target stream's reported color depth
// (`getColorDepth()`), which is where a real terminal folds in isTTY, NO_COLOR,
// NODE_DISABLE_COLORS and FORCE_COLOR. Depth 1 = no color (what NO_COLOR / a
// redirected pipe / CI report); depth 24 = truecolor. Model both directly so
// each case is deterministic regardless of the worker's own TTY/env.
function stubStream(stream: NodeJS.WriteStream, isTTY: boolean, colorDepth: number): void {
Object.defineProperty(stream, "isTTY", { value: isTTY, configurable: true });
Object.defineProperty(stream, "getColorDepth", { value: () => colorDepth, configurable: true });
}

function restoreStream(
stream: NodeJS.WriteStream,
original: { isTTY: boolean | undefined; getColorDepth: unknown },
): void {
Object.defineProperty(stream, "isTTY", { value: original.isTTY, configurable: true });
Object.defineProperty(stream, "getColorDepth", {
value: original.getColorDepth,
configurable: true,
});
}

async function withRestoredStreams<T>(callback: () => T | Promise<T>): Promise<T> {
try {
return await callback();
} finally {
restoreStream(process.stdout, ORIGINAL_STDOUT);
restoreStream(process.stderr, ORIGINAL_STDERR);
}
}

// styleText's `yellow`/`red`/`green` formats (as of Node 22.16) wrap text in
// SGR color codes with a `39` (default-foreground) reset.
const YELLOW = (s: string) => `\x1b[33m${s}\x1b[39m`;
const RED = (s: string) => `\x1b[31m${s}\x1b[39m`;
describe("preflight severity lines (#6004)", () => {
afterEach(() => {
vi.unstubAllEnvs();
vi.resetModules();
});

it("colors warn/error from stderr — their real stream — not stdout (#6004)", async () => {
await withRestoredStreams(() => {
// stdout redirected to a file, terminal still on stderr: warn/error must
// stay colored because they land on the color-capable stderr.
vi.stubEnv("NO_COLOR", "");
stubStream(process.stderr, true, 24);
stubStream(process.stdout, false, 1);
expect(warnLine("disk low")).toBe(` ${YELLOW("⚠ disk low")}`);
expect(failLine("docker down")).toBe(` ${RED("✗ docker down")}`);
});
});

it("drops warn/error color when stderr is redirected but stdout is a TTY (#6004)", async () => {
await withRestoredStreams(() => {
// The inverse leak: stderr redirected to a log, stdout still a terminal.
// warn/error must go plain so no raw ANSI lands in the log.
vi.stubEnv("NO_COLOR", "");
stubStream(process.stdout, true, 24);
stubStream(process.stderr, false, 1);
expect(warnLine("disk low")).toBe(" ⚠ disk low");
expect(failLine("docker down")).toBe(" ✗ docker down");
});
});

it("keeps NO_COLOR authoritative when FORCE_COLOR is also set", async () => {
await withRestoredStreams(() => {
vi.stubEnv("NO_COLOR", "1");
vi.stubEnv("FORCE_COLOR", "1");
stubStream(process.stdout, true, 24);
stubStream(process.stderr, true, 24);
expect(warnLine("a")).toBe(" ⚠ a");
expect(failLine("b")).toBe(" ✗ b");
});
});

it("selects the legacy true-color green when configured before import", async () => {
await withRestoredStreams(async () => {
stubStream(process.stdout, true, 24);
vi.stubEnv("NO_COLOR", "");
vi.stubEnv("COLORTERM", "truecolor");
vi.resetModules();

const freshStyles = await import("./terminal-style");
expect(freshStyles.G).toBe("\x1b[38;2;118;185;0m");
});
});
});
25 changes: 25 additions & 0 deletions src/lib/cli/terminal-style.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { styleText } from "node:util";

/**
* Legacy color constants (`G`, `B`, `D`, `R`, `RD`, `YW`) are frozen at module
* import time; import after `NO_COLOR` and TTY state are configured. Prefer the
* call-time severity helpers below for new output. The constants intentionally
* retain their historical raw ANSI values, while new output uses `styleText`
* so color capability is evaluated for the destination stream at call time.
*/
const useColor = !process.env.NO_COLOR && !!process.stdout.isTTY;
const trueColor =
useColor && (process.env.COLORTERM === "truecolor" || process.env.COLORTERM === "24bit");
Expand All @@ -11,3 +20,19 @@ export const D = useColor ? "\x1b[2m" : "";
export const R = useColor ? "\x1b[0m" : "";
export const RD = useColor ? "\x1b[1;31m" : "";
export const YW = useColor ? "\x1b[1;33m" : "";

// WARN and ERROR lines are emitted on stderr. `styleText({ stream })` therefore
// keys color off stderr's capability and honors NO_COLOR / NODE_DISABLE_COLORS /
// FORCE_COLOR (#6004). The old output keyed color off stdout, which dropped
// color on `onboard >log` and leaked ANSI into `onboard 2>log`.
function stderrSeverityLine(
marker: "⚠ " | "✗ ",
format: "yellow" | "red",
message: string,
): string {
const line = `${marker}${message}`;
return ` ${process.env.NO_COLOR ? line : styleText(format, line, { stream: process.stderr })}`;
}

export const warnLine = (message: string): string => stderrSeverityLine("⚠ ", "yellow", message);
export const failLine = (message: string): string => stderrSeverityLine("✗ ", "red", message);
Loading
Loading