From aea180f0252750881a94c1b44721afb19bf8da19 Mon Sep 17 00:00:00 2001 From: San Dang Date: Fri, 4 Sep 2026 16:09:55 +0700 Subject: [PATCH 01/19] fix(onboard): restore N1x standard onboarding Signed-off-by: San Dang --- ci/platform-matrix.json | 8 ++-- docs/get-started/quickstart-hermes.mdx | 12 +++--- .../quickstart-langchain-deepagents-code.mdx | 12 +++--- docs/get-started/quickstart.mdx | 14 +++--- docs/inference/choose-inference-provider.mdx | 12 +++--- .../choose-local-inference-server.mdx | 14 +++--- docs/inference/set-up-vllm.mdx | 14 +++--- docs/reference/commands.mdx | 4 +- docs/reference/platform-support.mdx | 6 +-- docs/reference/system-readiness.mdx | 4 +- docs/reference/troubleshooting.mdx | 2 +- docs/resources/starter-prompt.md | 18 ++++---- docs/security/best-practices.mdx | 9 ++-- scripts/install.sh | 14 +++--- src/lib/inference/serving/resolver.ts | 2 +- .../onboard/fatal-runtime-preflight.test.ts | 14 +++++- src/lib/onboard/fatal-runtime-preflight.ts | 7 ++- src/lib/onboard/vllm-menu.test.ts | 2 +- src/lib/readiness/onboard-admission.test.ts | 42 ++++++++++++++++-- src/lib/readiness/onboard-admission.ts | 12 ++++-- .../installer-express-prompt-pty-harness.ts | 8 ++-- test/helpers/installer-readiness-stubs.ts | 2 + test/install/install-express-n1x.test.ts | 31 +++++++------ .../installer-host-preflight.test.ts | 43 +++++++++++++++++++ 24 files changed, 206 insertions(+), 100 deletions(-) diff --git a/ci/platform-matrix.json b/ci/platform-matrix.json index 251437d6708..4500c85eb87 100644 --- a/ci/platform-matrix.json +++ b/ci/platform-matrix.json @@ -1,7 +1,7 @@ { "$comment": "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\nSPDX-License-Identifier: Apache-2.0\n\nSingle source of truth for NemoClaw launch claims and platform support. Covers platforms, inference providers, supported agents, messaging integrations, and deployment paths. Scripts read this to generate README and docs tables. QA/CI update platform/provider rows; the engineering owner reviews other rows. Docs are derived.", "version": "1.1", - "updated": "2026-08-14", + "updated": "2026-09-04", "project_status": { "stage": "alpha", @@ -74,7 +74,7 @@ "status": "deferred", "ci_tested": false, "prerequisites_notes": "N1x Express remains Deferred until a physical NemoClaw Express E2E test passes. Host identity requires Linux `arm64`, a trusted `/etc/fastos-release` marker with `NAME=\"N1x FASTOS\"`, and NVIDIA vendor ID `10de` on a display-class PCI device. CUDA and Container Device Interface (CDI) readiness are still required.", - "notes": "The accepted Deferred N1x Express preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. It does not activate DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Host identity requires Linux `arm64`, a regular `/etc/fastos-release` file of 1 through 4,096 bytes that is owned by UID 0 and GID 0, is not a symbolic link, is not group- or world-writable, and contains exactly one `NAME=\"N1x FASTOS\"` line. It also requires NVIDIA vendor ID `10de` on a display-class PCI device. Generic DMI values are supplemental and FastOS version is not pinned. Physical CUDA and CDI checks passed on one N1x host, but full NemoClaw Express E2E validation and dedicated CI coverage are pending. `host.platform.supported` remains absent; only explicit managed-vLLM preview intent can waive the pending-validation finding. Do not claim N1x as supported until that validation passes and this row is promoted." + "notes": "The accepted Deferred N1x Express preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. It does not activate DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Host identity requires Linux `arm64`, a regular `/etc/fastos-release` file of 1 through 4,096 bytes that is owned by UID 0 and GID 0, is not a symbolic link, is not group- or world-writable, and contains exactly one `NAME=\"N1x FASTOS\"` line. It also requires NVIDIA vendor ID `10de` on a display-class PCI device. Generic DMI values are supplemental and FastOS version is not pinned. Physical CUDA and CDI checks passed on one N1x host, but full NemoClaw Express E2E validation and dedicated CI coverage are pending. `host.platform.supported` remains absent. After identity qualifies, accepting Express, declining or disabling Express, or setting `NEMOCLAW_PROVIDER` can waive only the pending-validation finding. Existing provider requirements still apply, and this waiver does not promote N1x. Do not claim N1x as supported until validation passes and this row is promoted." }, { "name": "NVIDIA RTX (consumer and Pro workstation GPUs)", @@ -157,13 +157,13 @@ "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 healthy CDI spec. Onboarding evaluates the canonical `host.gpu.nvidia_available`, `host.gpu.container_toolkit_available`, and `host.gpu.cdi_healthy` readiness capabilities before gateway, image, or sandbox lifecycle effects. NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag. Onboarding filters the validated image list by usable memory. In non-interactive mode, onboarding selects the first compatible model unless the operator sets `NEMOCLAW_MODEL`. Model selection uses detected free GPU memory. If free memory is unavailable, NemoClaw uses total GPU memory. On DGX Spark, NemoClaw caps usable memory at 50 percent of total memory to match NIM's reported unified-memory limit. The Nemotron 3 Super catalog minimum includes NIM's reported runtime allocation. Local NVIDIA NIM is unavailable on N1x. NemoClaw omits the provider from onboarding and rejects `NEMOCLAW_PROVIDER=nim-local` on N1x. Use the Deferred managed-vLLM preview on N1x. On Linux arm64 DGX Spark and DGX Station hosts, onboarding warns that some NIM images may not publish a `linux/arm64` manifest; the warning is advisory, and the selected image pull can still fail when the registry has no matching platform manifest. 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 healthy CDI spec. Onboarding evaluates the canonical `host.gpu.nvidia_available`, `host.gpu.container_toolkit_available`, and `host.gpu.cdi_healthy` readiness capabilities before gateway, image, or sandbox lifecycle effects. NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag. Onboarding filters the validated image list by usable memory. In non-interactive mode, onboarding selects the first compatible model unless the operator sets `NEMOCLAW_MODEL`. Model selection uses detected free GPU memory. If free memory is unavailable, NemoClaw uses total GPU memory. On DGX Spark, NemoClaw caps usable memory at 50 percent of total memory to match NIM's reported unified-memory limit. The Nemotron 3 Super catalog minimum includes NIM's reported runtime allocation. Local NVIDIA NIM is unavailable on N1x. NemoClaw omits the provider from onboarding and rejects `NEMOCLAW_PROVIDER=nim-local` on N1x. Select another standard provider or use the Deferred managed-vLLM preview on N1x. On Linux arm64 DGX Spark and DGX Station hosts, onboarding warns that some NIM images may not publish a `linux/arm64` manifest; the warning is advisory, and the selected image pull can still fail when the registry has no matching platform manifest. 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)", "status": "caveated", "endpoint_type": "Local OpenAI-compatible", - "notes": "Unavailable on N1x. On other hosts, it appears in the onboarding menu when NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. No flag is required. The model is whatever the existing server serves." + "notes": "On a qualified N1x host, this route requires explicit standard-onboarding intent and remains unvalidated. On other hosts, no explicit intent is required. It appears in the onboarding menu when NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. The model is whatever the existing server serves." }, { "name": "Local vLLM (managed install/start)", diff --git a/docs/get-started/quickstart-hermes.mdx b/docs/get-started/quickstart-hermes.mdx index 517eed11b51..2e4440aa6bf 100644 --- a/docs/get-started/quickstart-hermes.mdx +++ b/docs/get-started/quickstart-hermes.mdx @@ -61,14 +61,14 @@ Review the [Prerequisites](prerequisites) before you begin. Press Enter to use the recommended express install mode for that platform. This mode applies preset settings and runs the remaining onboarding non-interactively. Express install mode preserves the explicit Hermes selection in the install command. - On supported non-N1x express platforms, enter `n` if you want to select a sandbox name, an inference provider, and a model interactively. - On N1x, entering `n` stops installation because the Deferred managed-vLLM preview is the only admitted onboarding path. + Enter `n` if you want to select a sandbox name, an inference provider, and a model interactively. + On N1x, the selected provider must meet its existing requirements, and local NVIDIA NIM remains unavailable. Refer to [Use a container runtime and supported platforms](#use-a-container-runtime-and-supported-platforms) for more information. The install command creates the `my-hermes` sandbox used in the commands that follow. If you accept Express setup, wait for the installer to finish, then continue with **Confirm the Sandbox Is Ready**; Express selects the provider and model non-interactively. - If the installer does not offer Express setup, or if you enter `n` at the Express prompt on a supported non-N1x host, choose an inference provider and model, then provide its credential when prompted. + If the installer does not offer Express setup, or if you enter `n` at the Express prompt, choose an inference provider and model, then provide its credential when prompted. For that interactive path, skip optional web search and messaging setup on a first run, then accept the suggested network policy tier. With the OpenShell Docker driver, stock Hermes onboarding normally uses the release's exact managed-image digest. If registry or catalog availability prevents resolution, stock onboarding stops before sandbox creation and does not build a shipped Dockerfile. @@ -267,9 +267,9 @@ Use these details when your first-run path needs more control. N1x remains outside the supported-platform set until that validation passes and the platform matrix status is promoted. Press Enter at the express install prompt to apply preset settings, switch the remaining onboarding to non-interactive mode, and select the managed local inference path for that platform. On N1x, accept the preview prompt to apply those settings as explicit Deferred preview intent. - On supported non-N1x express platforms, enter `n` to continue with interactive onboarding when you want to select the agent or other settings yourself. - On N1x, declining the preview or setting only `NEMOCLAW_NO_EXPRESS=1` stops installation before onboarding. - Accept the preview, or set `NEMOCLAW_PROVIDER=install-vllm` before installation to provide the required explicit managed-vLLM intent. + Enter `n` to continue with interactive onboarding when you want to select the agent or other settings yourself. + On N1x, setting `NEMOCLAW_NO_EXPRESS=1` also continues with ordinary onboarding. + Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set another provider to enter standard onboarding directly. The first Hermes start can take several minutes while OpenShell pulls the managed image when it is not already cached. The N1x preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. Refer to [Set Up vLLM](../inference/local-inference/set-up-vllm) for managed model profiles and headless setup. diff --git a/docs/get-started/quickstart-langchain-deepagents-code.mdx b/docs/get-started/quickstart-langchain-deepagents-code.mdx index 2ddbfbef6a6..f37a137590e 100644 --- a/docs/get-started/quickstart-langchain-deepagents-code.mdx +++ b/docs/get-started/quickstart-langchain-deepagents-code.mdx @@ -57,14 +57,14 @@ If you use the coding-agent prompt in the preceding section, you can skip this p Press Enter to use the recommended express install mode for that platform. This mode applies preset settings and runs the remaining onboarding non-interactively. Express install mode preserves the explicit Deep Agents selection in the install command. - On supported non-N1x express platforms, enter `n` if you want to select a sandbox name, an inference provider, and a model interactively. - On N1x, entering `n` stops installation because the Deferred managed-vLLM preview is the only admitted onboarding path. + Enter `n` if you want to select a sandbox name, an inference provider, and a model interactively. + On N1x, the selected provider must meet its existing requirements, and local NVIDIA NIM remains unavailable. Refer to [Use a container runtime and supported platforms](#use-a-container-runtime-and-supported-platforms) for more information. The install command creates the `my-deepagents` sandbox used in the commands that follow. If you accept Express setup, wait for the installer to finish, then continue with **Confirm the Sandbox Is Ready**; Express selects the provider and model non-interactively. - If the installer does not offer Express setup, or if you enter `n` at the Express prompt on a supported non-N1x host, choose an inference provider and model, then provide its credential when prompted. + If the installer does not offer Express setup, or if you enter `n` at the Express prompt, choose an inference provider and model, then provide its credential when prompted. For that interactive path, accept the suggested network policy tier on a first run. With the OpenShell Docker driver, stock Deep Agents Code onboarding normally uses the release's exact managed-image digest. If registry or catalog availability prevents resolution, stock onboarding stops before sandbox creation and does not build a shipped Dockerfile. @@ -127,9 +127,9 @@ Use these details when you need more control during setup or after the first san N1x remains outside the supported-platform set until that validation passes and the platform matrix status is promoted. Press Enter at the express install prompt to apply preset settings, switch the remaining onboarding to non-interactive mode, and select the managed local inference path for that platform. On N1x, accept the preview prompt to apply those settings as explicit Deferred preview intent. - On supported non-N1x express platforms, enter `n` to continue with interactive onboarding when you want to select the agent or other settings yourself. - On N1x, declining the preview or setting only `NEMOCLAW_NO_EXPRESS=1` stops installation before onboarding. - Accept the preview, or set `NEMOCLAW_PROVIDER=install-vllm` before installation to provide the required explicit managed-vLLM intent. + Enter `n` to continue with interactive onboarding when you want to select the agent or other settings yourself. + On N1x, setting `NEMOCLAW_NO_EXPRESS=1` also continues with ordinary onboarding. + Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set another provider to enter standard onboarding directly. The N1x preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. diff --git a/docs/get-started/quickstart.mdx b/docs/get-started/quickstart.mdx index 1dde40622a4..292662074a0 100644 --- a/docs/get-started/quickstart.mdx +++ b/docs/get-started/quickstart.mdx @@ -61,9 +61,9 @@ Review the [Prerequisites](prerequisites) before you begin. Press Enter to use the recommended preset settings on a qualified platform or to explicitly opt into the Deferred N1x preview. Express install mode completes the remaining onboarding non-interactively. If you accept, wait for the installer to finish, then continue with **Confirm the Sandbox Is Ready**. - On N1x, entering `n` stops installation because the Deferred managed-vLLM preview is the only admitted onboarding path. - Re-run and accept the preview, or set `NEMOCLAW_PROVIDER=install-vllm` before installation. - On the other express platforms, enter `n` to continue with the interactive onboarding flow described in this step. + On N1x, enter `n` to continue with the interactive onboarding flow described in this step. + N1x remains Deferred, and the provider that you select must meet its existing requirements. + Local NVIDIA NIM remains unavailable on N1x. @@ -197,14 +197,14 @@ Use these details when your first-run path needs more control. After you press Enter at the express install prompt, or explicitly accept the Deferred N1x preview, the installer switches the remaining onboarding to non-interactive mode and selects the managed local inference path for that platform. Express install mode uses `my-assistant` unless you set `NEMOCLAW_SANDBOX_NAME`, and it applies the suggested Balanced policy. It can still prompt for `sudo` when host setup needs it. - On the other express platforms, set `NEMOCLAW_NO_EXPRESS=1` to skip the express prompt, or set `NEMOCLAW_PROVIDER` to choose a provider yourself. + On any express platform, set `NEMOCLAW_NO_EXPRESS=1` to skip the express prompt, or set `NEMOCLAW_PROVIDER` to choose a provider yourself. N1x Express selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. N1x detection requires Linux `arm64`, a trusted `/etc/fastos-release` marker with `NAME="N1x FASTOS"`, and NVIDIA vendor ID `10de` on a display-class PCI device. The N1x path does not use DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. - Accepting the N1x prompt supplies the narrow preview intent required to pass the pending-validation readiness finding. - Declining the N1x prompt or setting only `NEMOCLAW_NO_EXPRESS=1` stops installation before onboarding; neither path opens interactive provider selection. - Set `NEMOCLAW_PROVIDER=install-vllm` when you need to bypass the N1x preview prompt with the same explicit intent. + Accepting the N1x prompt supplies the narrow managed-vLLM preview intent required to pass the pending-validation readiness finding. + Declining the N1x prompt or setting `NEMOCLAW_NO_EXPRESS=1` continues with ordinary interactive onboarding. + Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set another provider to enter standard onboarding directly. Refer to [Platform Support](../reference/platform-support) before you rely on this path. Before the Station express confirmation, the installer reports whether the Hugging Face download is authenticated without displaying the token value. diff --git a/docs/inference/choose-inference-provider.mdx b/docs/inference/choose-inference-provider.mdx index 8f123ad2836..6fc7cec405e 100644 --- a/docs/inference/choose-inference-provider.mdx +++ b/docs/inference/choose-inference-provider.mdx @@ -13,8 +13,8 @@ Choose a provider based on where the model runs, which API it exposes, and which NemoClaw validates the provider and model before it creates the sandbox. -On native N1x FASTOS, the Deferred managed-vLLM preview is the only admitted inference route. -Accept the native N1x Express prompt or set `NEMOCLAW_PROVIDER=install-vllm` to provide the required explicit preview intent. +On native N1x FASTOS, accept the Deferred managed-vLLM preview or enter standard onboarding by declining or disabling Express or setting `NEMOCLAW_PROVIDER`. +N1x remains Deferred, each standard provider keeps its existing requirements, and local NVIDIA NIM remains unavailable. On a qualifying Windows WSL N1x host, Windows Express uses managed llama.cpp after Docker Desktop GPU passthrough succeeds. Other Windows WSL hosts retain Windows-host or WSL-local Ollama. @@ -37,8 +37,8 @@ Use this status table to distinguish validated provider integrations from adapte | 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 llama.cpp (already running) | Experimental | Local OpenAI-compatible | Attaches only to an authenticated operator-managed server on loopback port `8081` that passes cooperative native llama.cpp fingerprinting. NemoClaw does not own its server, model, or lifecycle. | | Local llama.cpp (managed DGX Spark) | Experimental | Local OpenAI-compatible | Uses the repository-owned YAML recipe for NVIDIA Nemotron 3 Nano 30B-A3B, exact GGUF verification, and Docker lifecycle on a qualified DGX Spark host. The recipe remains unqualified for agent support. | -| 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 healthy CDI spec. Onboarding evaluates the canonical `host.gpu.nvidia_available`, `host.gpu.container_toolkit_available`, and `host.gpu.cdi_healthy` readiness capabilities before gateway, image, or sandbox lifecycle effects. NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag. Onboarding filters the validated image list by usable memory. In non-interactive mode, onboarding selects the first compatible model unless the operator sets `NEMOCLAW_MODEL`. Model selection uses detected free GPU memory. If free memory is unavailable, NemoClaw uses total GPU memory. On DGX Spark, NemoClaw caps usable memory at 50 percent of total memory to match NIM's reported unified-memory limit. The Nemotron 3 Super catalog minimum includes NIM's reported runtime allocation. Local NVIDIA NIM is unavailable on N1x. NemoClaw omits the provider from onboarding and rejects `NEMOCLAW_PROVIDER=nim-local` on N1x. Use the Deferred managed-vLLM preview on N1x. On Linux arm64 DGX Spark and DGX Station hosts, onboarding warns that some NIM images may not publish a `linux/arm64` manifest; the warning is advisory, and the selected image pull can still fail when the registry has no matching platform manifest. 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 | Unavailable on N1x. On other hosts, it appears in the onboarding menu when NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. No flag is required. The model is whatever the existing server serves. | +| 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 healthy CDI spec. Onboarding evaluates the canonical `host.gpu.nvidia_available`, `host.gpu.container_toolkit_available`, and `host.gpu.cdi_healthy` readiness capabilities before gateway, image, or sandbox lifecycle effects. NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag. Onboarding filters the validated image list by usable memory. In non-interactive mode, onboarding selects the first compatible model unless the operator sets `NEMOCLAW_MODEL`. Model selection uses detected free GPU memory. If free memory is unavailable, NemoClaw uses total GPU memory. On DGX Spark, NemoClaw caps usable memory at 50 percent of total memory to match NIM's reported unified-memory limit. The Nemotron 3 Super catalog minimum includes NIM's reported runtime allocation. Local NVIDIA NIM is unavailable on N1x. NemoClaw omits the provider from onboarding and rejects `NEMOCLAW_PROVIDER=nim-local` on N1x. Select another standard provider or use the Deferred managed-vLLM preview on N1x. On Linux arm64 DGX Spark and DGX Station hosts, onboarding warns that some NIM images may not publish a `linux/arm64` manifest; the warning is advisory, and the selected image pull can still fail when the registry has no matching platform manifest. 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 | On a qualified N1x host, this route requires explicit standard-onboarding intent and remains unvalidated. On other hosts, no explicit intent is required. It appears in the onboarding menu when NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. The model is whatever the existing server serves. | | Local vLLM (managed install/start) | Tested with limitations | Local OpenAI-compatible | Managed install/start requires Docker on `PATH`; attaching an already-running local vLLM server is a separate path. Appears by default on DGX Spark and qualifying DGX Station GB300 hosts. A labeled Deferred preview appears on hosts that match the N1x identity requirements; N1x remains Deferred until a physical NemoClaw Express E2E test passes. DGX Station is Tested with limitations across qualified profiles on one physical DGX Station GB300. Full Station Express end-to-end qualification for the accepted no-OTA DGX OS `7.6.x` profile is pending. Dual-Station configurations are not yet validated, and dedicated CI coverage is not available. For canonical Station qualification and host preparation, see the Additional Setup page for [OpenClaw](/user-guide/openclaw/get-started/additional-setup/dgx-station-preparation), [Hermes](/user-guide/hermes/get-started/additional-setup/dgx-station-preparation), or [Deep Agents](/user-guide/deepagents/get-started/additional-setup/dgx-station-preparation). Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`, NVIDIA Container Toolkit, and CDI. NemoClaw pins runtime images to immutable digests. Station Express defaults to `nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-NVFP4`; `--station-deepseek` selects `deepseek-ai/DeepSeek-V4-Flash`. Direct managed-vLLM defaults are `nvidia/Qwen3.6-35B-A3B-NVFP4` on DGX Spark and the Deferred N1x preview, `deepseek-ai/DeepSeek-V4-Flash` on DGX Station, and `nvidia/NVIDIA-Nemotron-3-Nano-4B-FP8` on generic Linux NVIDIA GPU hosts. Image pulls from `nvcr.io` require NGC registry login. | {/* provider-status:end */} @@ -66,7 +66,7 @@ Local providers run on the host and keep inference traffic on infrastructure tha | Provider | When it appears | Setup guide | |---|---|---| | Local Ollama | Ollama is installed or running on the host. | [Use Ollama](../local-inference/set-up-ollama) | -| Local vLLM already running | On hosts other than native N1x FASTOS, NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. | [Set Up vLLM](../local-inference/set-up-vllm) | +| Local vLLM already running | NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. Native N1x requires explicit standard-onboarding intent. | [Set Up vLLM](../local-inference/set-up-vllm) | | Local vLLM managed install | Docker is available on `PATH`, the host matches a supported GPU profile, and any required opt-in is present. Attaching an already-running local vLLM server is separate. | [Set Up vLLM](../local-inference/set-up-vllm) | | Local llama.cpp already running | You explicitly select an authenticated server on loopback port `8081`. | [Set Up llama.cpp](../local-inference/set-up-llama-cpp) | | Local llama.cpp managed install | A qualified DGX Spark or Windows WSL N1x host satisfies the declarative recipe requirements. | [Set Up llama.cpp](../local-inference/set-up-llama-cpp) | @@ -76,7 +76,7 @@ Local providers run on the host and keep inference traffic on infrastructure tha | Provider | When it appears | Setup guide | |---|---|---| -| Local vLLM already running | On hosts other than native N1x FASTOS, NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. | [Set Up vLLM](../local-inference/set-up-vllm) | +| Local vLLM already running | NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. Native N1x requires explicit standard-onboarding intent. | [Set Up vLLM](../local-inference/set-up-vllm) | | Local vLLM managed install | Docker is available on `PATH`, the host matches a supported GPU profile, and any required opt-in is present. Attaching an already-running local vLLM server is separate. | [Set Up vLLM](../local-inference/set-up-vllm) | | Local llama.cpp already running | You explicitly select an authenticated server on loopback port `8081`. | [Set Up llama.cpp](../local-inference/set-up-llama-cpp) | | Local llama.cpp managed install | A qualified DGX Spark or Windows WSL N1x host satisfies the declarative recipe requirements. | [Set Up llama.cpp](../local-inference/set-up-llama-cpp) | diff --git a/docs/inference/choose-local-inference-server.mdx b/docs/inference/choose-local-inference-server.mdx index ebd1119a38b..b02308402b8 100644 --- a/docs/inference/choose-local-inference-server.mdx +++ b/docs/inference/choose-local-inference-server.mdx @@ -16,8 +16,8 @@ The agent inside the sandbox sends inference traffic to `inference.local`. OpenShell intercepts that traffic and forwards it to the local endpoint configured during onboarding. -On native N1x FASTOS, use the Deferred managed-vLLM preview. -It is the only admitted local inference route on native N1x FASTOS. +On native N1x FASTOS, use the Deferred managed-vLLM preview or enter standard onboarding explicitly. +N1x remains Deferred, each standard provider keeps its existing requirements, and local NVIDIA NIM remains unavailable. A qualifying Windows WSL N1x host instead uses the explicit managed llama.cpp recipe through Windows Express. @@ -27,8 +27,8 @@ A qualifying Windows WSL N1x host instead uses the explicit managed llama.cpp re | Option | When to use it | Availability | Runtime API | |---|---|---|---| | Ollama | You want the default local option and want NemoClaw to install, start, or use Ollama on supported hosts. | Appears when Ollama is installed or running, and the wizard can offer installation on supported hosts. | Ollama through the managed local route. | -| Existing vLLM | You already run vLLM on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. | Unavailable on native N1x FASTOS. On other hosts, it appears when NemoClaw detects the server. | `/v1/chat/completions`. | -| Managed vLLM | You want NemoClaw to pull an image, download model weights, and manage the server container. | Requires Docker on `PATH`. Appears by default on DGX Spark and DGX Station. Native N1x FASTOS offers this route as its only admitted provider through a Deferred preview. Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`. Attaching an already-running local vLLM server is separate. | `/v1/chat/completions`. | +| Existing vLLM | You already run vLLM on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. | Appears when NemoClaw detects the server. Native N1x requires explicit standard-onboarding intent, and this route remains unvalidated there. | `/v1/chat/completions`. | +| Managed vLLM | You want NemoClaw to pull an image, download model weights, and manage the server container. | Requires Docker on `PATH`. Appears by default on DGX Spark and DGX Station. Native N1x FASTOS offers this route through a Deferred preview. Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`. Attaching an already-running local vLLM server is separate. | `/v1/chat/completions`. | | Fixed vLLM profile | You need the fixed catalog-selected vLLM model and serving configuration. | Appears as option 2 in DGX Spark Express. Direct installation can use the dedicated installer flag. The profile does not appear in the provider menu. | `/v1/chat/completions`. | | Existing llama.cpp | You already operate an authenticated llama.cpp server on loopback port `8081`. | Experimental. Always available for explicit selection and attaches only after cooperative fingerprinting succeeds. | `/v1/chat/completions`. | | Managed llama.cpp | You want NemoClaw to acquire a verified GGUF file and manage an authenticated llama.cpp container. | Experimental. Lists compatible profiles on qualified DGX Spark hosts. Qualifying Windows WSL N1x Express hosts use the explicit Qwen 3.6 recipe. | `/v1/chat/completions`. | @@ -38,8 +38,8 @@ A qualifying Windows WSL N1x host instead uses the explicit managed llama.cpp re | Option | When to use it | Availability | Runtime API | |---|---|---|---| -| Existing vLLM | You already run vLLM on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. | Unavailable on native N1x FASTOS. On other hosts, it appears when NemoClaw detects the server. | `/v1/chat/completions`. | -| Managed vLLM | You want NemoClaw to pull an image, download model weights, and manage the server container. | Requires Docker on `PATH`. Appears by default on DGX Spark and DGX Station. Native N1x FASTOS offers this route as its only admitted provider through a Deferred preview. Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`. Attaching an already-running local vLLM server is separate. | `/v1/chat/completions`. | +| Existing vLLM | You already run vLLM on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. | Appears when NemoClaw detects the server. Native N1x requires explicit standard-onboarding intent, and this route remains unvalidated there. | `/v1/chat/completions`. | +| Managed vLLM | You want NemoClaw to pull an image, download model weights, and manage the server container. | Requires Docker on `PATH`. Appears by default on DGX Spark and DGX Station. Native N1x FASTOS offers this route through a Deferred preview. Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`. Attaching an already-running local vLLM server is separate. | `/v1/chat/completions`. | | Fixed vLLM profile | You need the fixed catalog-selected vLLM model and serving configuration. | Appears as option 2 in DGX Spark Express. Direct installation can use the dedicated installer flag. The profile does not appear in the provider menu. | `/v1/chat/completions`. | | Existing llama.cpp | You already operate an authenticated llama.cpp server on loopback port `8081`. | Experimental. Always available for explicit selection and attaches only after cooperative fingerprinting succeeds. | `/v1/chat/completions`. | | Managed llama.cpp | You want NemoClaw to acquire a verified GGUF file and manage an authenticated llama.cpp container. | Experimental. Lists compatible profiles on qualified DGX Spark hosts. Qualifying Windows WSL N1x Express hosts use the explicit Qwen 3.6 recipe. | `/v1/chat/completions`. | @@ -188,7 +188,7 @@ The managed procedure moved to [Set Up llama.cpp](set-up-llama-cpp#install-manag Choose NVIDIA NIM when you want a managed NIM container and your host has a NIM-capable NVIDIA GPU. This path is unavailable on N1x. -Use the [Deferred managed-vLLM preview](set-up-vllm#use-n1x-express) on N1x. +Select another standard provider or use the [Deferred managed-vLLM preview](set-up-vllm#use-n1x-express) on N1x. The path is experimental, requires NGC registry access, and can fail when a selected image does not publish a manifest for the host architecture. Refer to [Set Up NVIDIA NIM](set-up-nvidia-nim). diff --git a/docs/inference/set-up-vllm.mdx b/docs/inference/set-up-vllm.mdx index 3ee65fd5f78..59c6203c0a6 100644 --- a/docs/inference/set-up-vllm.mdx +++ b/docs/inference/set-up-vllm.mdx @@ -36,7 +36,8 @@ Follow [Set Up vLLM on Two DGX Sparks](set-up-vllm-on-two-dgx-sparks) or [Set Up ## Use an Existing Server -On hosts other than N1x, when vLLM is already running on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`, NemoClaw detects it and queries `/v1/models` for the loaded model. +When vLLM is already running on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`, NemoClaw detects it and queries `/v1/models` for the loaded model. +On N1x, decline or disable Express, or set `NEMOCLAW_PROVIDER=vllm`, to supply explicit standard-onboarding intent. Run onboarding. @@ -46,7 +47,7 @@ $$nemoclaw onboard Select **Local vLLM**. On generic hosts, the entry includes an experimental label. -On N1x, the readiness gate admits only explicit managed-preview intent, so the user-facing onboarding flow does not offer the existing-server entry. +On N1x, the existing-server route remains unvalidated even after standard-onboarding intent admits it. NemoClaw validates the detected endpoint and records the model reported by `/v1/models`. When you request a vLLM serving profile, the detected server must report the profile's served model alias or a safe `root` that matches the profile's declared model. @@ -152,12 +153,13 @@ $$nemoclaw onboard --profile --vllm-gpu-device The single-host managed vLLM runtime is host-global. Destroying a sandbox does not stop it, and the container remains available for other sandboxes. -On hosts other than N1x, later onboarding normally reuses a server that is already listening on the configured vLLM port instead of replacing it automatically. +Later standard onboarding normally reuses a server that is already listening on the configured vLLM port instead of replacing it automatically. When a managed GPU selection accompanies explicit managed-install intent, NemoClaw preserves that intent and reports the running-server conflict. Choose one action: -- On hosts other than N1x, to reuse the running server, omit `--vllm-gpu-device` and select **Local vLLM**. +- To reuse the running server, omit `--vllm-gpu-device` and select **Local vLLM**. + On N1x, decline or disable Express, or set `NEMOCLAW_PROVIDER=vllm`, before onboarding. - To change the GPU or port, inspect the sandbox registry for every gateway-port environment on the host. Run `NEMOCLAW_GATEWAY_PORT= $$nemoclaw list` for port `8080` and for every non-default port represented by a numeric directory under `~/.nemoclaw/gateways/`. Treat every sandbox that reports provider `vllm-local` as a possible consumer because the list output does not distinguish local vLLM from Local NIM. @@ -545,13 +547,15 @@ If no server is running, it selects the managed install or start entry. If neither local entry is available, it selects NVIDIA Endpoints. On DGX Station and other hosts, an unset provider keeps NVIDIA Endpoints as the automatic default. -On hosts other than N1x, use an already-running server. +Use an already-running server. ```bash NEMOCLAW_PROVIDER=vllm \ $$nemoclaw onboard --non-interactive ``` +On N1x, this explicit provider supplies standard-onboarding intent, but the route remains unvalidated. + Install or start managed vLLM on a supported host, or explicitly enter the Deferred N1x preview. ```bash diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index b73954e9705..e1c1c08bf15 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -3572,7 +3572,7 @@ The following variables let you tune onboarding without editing the Dockerfile o | Variable | Format | Effect | | --- | --- | --- | -| `NEMOCLAW_PROVIDER` | provider key (e.g. `build`, `openrouter`, `openai`, `anthropic`, `anthropicCompatible`, `gemini`, `ollama`, `custom`, `vllm`, `nim-local`, `routed`, `hermes-provider`, `llama-cpp`, `install-llama-cpp`, `install-vllm`, `install-ollama`, `install-windows-ollama`, `start-windows-ollama`) | Selects the inference provider during onboarding. The wizard skips the provider menu in both interactive and non-interactive runs when this is set. On a qualified N1x host, only `install-vllm` supplies the explicit Deferred preview intent; another provider stops installation before onboarding. `llama-cpp` selects attachment of an authenticated, operator-managed llama.cpp server on loopback port `8081`. Set `NEMOCLAW_LLAMACPP_LOCAL_TOKEN`; set `NEMOCLAW_MODEL` to the served alias when the server exposes multiple models. `install-llama-cpp` selects the experimental DGX Spark managed path and rejects `NEMOCLAW_MODEL`; use `NEMOCLAW_LLAMACPP_RECIPE` for its declarative selection. If an operator-managed server does not provide consistent native llama.cpp evidence, select `custom`. Aliases: `cloud` → `build`, `open-router` / `openrouterai` → `openrouter`, `nim` → `nim-local`, `hermes` / `nous` / `nous-portal` → `hermes-provider`, `anthropiccompatible` → `anthropicCompatible`. Invalid values fail fast with the list of accepted keys. | +| `NEMOCLAW_PROVIDER` | provider key (e.g. `build`, `openrouter`, `openai`, `anthropic`, `anthropicCompatible`, `gemini`, `ollama`, `custom`, `vllm`, `nim-local`, `routed`, `hermes-provider`, `llama-cpp`, `install-llama-cpp`, `install-vllm`, `install-ollama`, `install-windows-ollama`, `start-windows-ollama`) | Selects the inference provider during onboarding. The wizard skips the provider menu in both interactive and non-interactive runs when this is set. On a qualified N1x host, `install-vllm` supplies Deferred preview intent, while another provider supplies standard-onboarding intent and retains its existing requirements. Local NVIDIA NIM remains unavailable on N1x. `llama-cpp` selects attachment of an authenticated, operator-managed llama.cpp server on loopback port `8081`. Set `NEMOCLAW_LLAMACPP_LOCAL_TOKEN`; set `NEMOCLAW_MODEL` to the served alias when the server exposes multiple models. `install-llama-cpp` selects the experimental DGX Spark managed path and rejects `NEMOCLAW_MODEL`; use `NEMOCLAW_LLAMACPP_RECIPE` for its declarative selection. If an operator-managed server does not provide consistent native llama.cpp evidence, select `custom`. Aliases: `cloud` → `build`, `open-router` / `openrouterai` → `openrouter`, `nim` → `nim-local`, `hermes` / `nous` / `nous-portal` → `hermes-provider`, `anthropiccompatible` → `anthropicCompatible`. Invalid values fail fast with the list of accepted keys. | | `NEMOCLAW_LLAMACPP_RECIPE` | repository-owned managed-inference recipe ID | Selects the managed llama.cpp recipe when `NEMOCLAW_PROVIDER=install-llama-cpp`, including a compatible lower-priority profile. When unset, NemoClaw selects the unique highest-priority compatible automatic profile. An unknown recipe, an ambiguous selection, or a stale or incompatible readiness report fails before image, model, or runtime effects. | | `NEMOCLAW_MODEL` | model ID | Selects an explicit model for a non-interactive onboarding run. NemoClaw preserves it across a detected provider switch, even when it matches the recorded provider's default. When this variable is unset during such a switch, NemoClaw ignores the `NEMOCLAW_PROVIDER_MODEL` compatibility fallback and uses normal provider model selection. | | `NEMOCLAW_TOOL_DISCLOSURE` | `progressive` or `direct` | Selects progressive tool discovery or the prior direct-exposure behavior outside the Portable experimental profile. Defaults to `progressive`; `--tool-disclosure` takes precedence when both are set. Fresh Portable onboarding ignores this variable and defaults to `direct` unless the flag explicitly selects a mode. | @@ -3793,7 +3793,7 @@ The following flags toggle optional behaviors during onboarding. Set them before | `NEMOCLAW_YES` | `1` to enable | Auto-accepts confirmation prompts (`--yes` equivalent) including in helpers like the Ollama proxy auth setup, but does not change managed-vLLM storage-warning handling. Express and other non-interactive setup stop after a verified insufficient-capacity warning, interactive setup still requires an explicit `y` or `yes`, and an inconclusive model-cache check stops non-interactive setup with guidance to rerun interactively. | | `NEMOCLAW_OLLAMA_NO_AUTOSTART` | `1` to enable | Skips the wizard's eager Ollama auto-start during inference-provider selection (equivalent to passing `--no-ollama-autostart`). When set and Ollama is not running on `localhost:11434`, an agent that uses the legacy `16384`-token context floor, currently OpenClaw, prints a warning and selects the default fallback model instead of spawning `ollama serve`. An agent that requires a larger verified runtime context, currently Hermes at `64000` tokens, returns to interactive provider selection or exits when the Ollama provider is pinned or onboarding is non-interactive. The flag covers only the provider-selection step; later setup steps (auth proxy, validation, model warm) still expect a reachable Ollama. On Linux hosts with a systemd Ollama unit, the loopback-override path may still restart the daemon before this gate runs. | | `NEMOCLAW_NON_INTERACTIVE_SUDO_MODE` | `prompt` or empty/unset | When set to `prompt`, allows non-interactive onboarding to use prompt-capable `sudo` for host setup steps that require elevation, which can ask for a password. Empty/unset is the default and uses `sudo -n`, which fails instead of asking for a password. Any other value is rejected. | -| `NEMOCLAW_NO_EXPRESS` | `1` to enable | Installer-only. Skips the DGX Spark, DGX Station, and Windows WSL express prompt, then continues with the normal interactive onboarding flow. On N1x, setting this variable alone stops installation before onboarding. Combine it with `NEMOCLAW_PROVIDER=install-vllm` only when you intend to bypass the preview prompt with explicit Deferred managed-vLLM intent. | +| `NEMOCLAW_NO_EXPRESS` | `1` to enable | Installer-only. Skips the express prompt, including the Deferred N1x preview, then continues with normal onboarding. On a qualified N1x host, this value supplies explicit standard-onboarding intent. Combine it with `NEMOCLAW_PROVIDER=install-vllm` only when you intend to bypass the preview prompt with Deferred managed-vLLM intent. | | `NEMOCLAW_EXPERIMENTAL` | `1` to enable | Surfaces experimental providers and flows in onboarding. | | `NEMOCLAW_IGNORE_RUNTIME_RESOURCES` | `1` to enable | Suppresses the under-provisioned runtime warning during preflight. Use only when you know the sandbox host meets the minimums. | | `NEMOCLAW_DISABLE_OVERLAY_FIX` | `1` to enable | Skips the Docker overlay-fix step during sandbox build. For environments where the fix is incompatible. | diff --git a/docs/reference/platform-support.mdx b/docs/reference/platform-support.mdx index e88076608b4..fad650305c4 100644 --- a/docs/reference/platform-support.mdx +++ b/docs/reference/platform-support.mdx @@ -82,7 +82,7 @@ For install requirements and the shorter setup-oriented platform view, refer to | DGX OS (Station) | Docker | Tested with limitations | P1 | No | The PRD marks this platform as P1. Physical validation on one DGX Station GB300 covers generic Ubuntu 24.04 ARM64, stock DGX OS `7.5.0`, the April 2026 NVIDIA Colossus BaseOS profile, and the June 2026 NVIDIA AI Developer Tools profile. A physical no-OTA DGX OS `7.6.0` host provided the release and hardware profile used for its stable workstation-family classifier and passed read-only eligibility and runtime-command preflight. Full Station Express end-to-end qualification for the accepted no-OTA DGX OS `7.6.x` profile is pending. The profile remains subject to the same physical GB300, driver, ECC, Docker, CDI, and container GPU validation. Clean-host end-to-end validation passed on generic Ubuntu and Colossus BaseOS; stock DGX OS and AI Developer Tools completed Station Express validation. The DGX OS `7.5.0` run used released OpenShell `0.0.85`, local Nemotron Ultra serving, sandbox `cuInit(0)`, and a Hermes write/read file-tool task. A dual-Station configuration has not been validated, and dedicated CI coverage is not available. Direct-GPU policies expose only the exact read-only BDF directory for each discovered display-class PCI device with NVIDIA vendor ID (`0x10de`) and GB300 device ID (`0x31c2` or `0x31c3`) plus required existing topology and module paths; they do not expose `/sys`, the PCI parent subtree, or sysfs write access. During physical validation, reads of `/sys/fs/cgroup/cgroup.controllers` and `/sys/class/net/lo/address` remained denied. For canonical hardware qualification, image requirements, preparation, repair limits, reboot handoff, and the explicit temporary metadata override, see [Prepare DGX Station to Install NemoClaw](../get-started/additional-setup/dgx-station-preparation). | | Linux | Docker | Tested | P0 | Yes | Primary tested path. Ubuntu 24.04 has host-level onboarding validation. A digest-pinned Ubuntu 26.04 userspace lane builds the CLI and runs preflight, installer, and platform contracts on eligible main pushes; Docker-host, AppArmor, Landlock, and live onboarding validation on 26.04 remain pending. Other distros (Ubuntu 22.04, Fedora, Rocky, Alma, NixOS, Arch) may work but are not validated. | | macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | P0 | Yes | Start the container runtime (Colima or Docker Desktop) before installing; NemoClaw verifies the pinned official OpenShell formula and grants formula-scoped trust only around each Homebrew install, inspection, start, or stop operation, requires a legacy or changed formula to be repaired by rerunning the pinned installer, uses the standalone gateway only when Homebrew is absent or both the staged formula and installed keg are absent, requires Homebrew Colima users to install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work, and recommends Xcode Command Line Tools (`xcode-select --install`) for Node native modules. | -| N1x FASTOS | Docker | Deferred | Unset | No | The accepted Deferred N1x Express preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. It does not activate DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Host identity requires Linux `arm64`, a regular `/etc/fastos-release` file of 1 through 4,096 bytes that is owned by UID 0 and GID 0, is not a symbolic link, is not group- or world-writable, and contains exactly one `NAME="N1x FASTOS"` line. It also requires NVIDIA vendor ID `10de` on a display-class PCI device. Generic DMI values are supplemental and FastOS version is not pinned. Physical CUDA and CDI checks passed on one N1x host, but full NemoClaw Express E2E validation and dedicated CI coverage are pending. `host.platform.supported` remains absent; only explicit managed-vLLM preview intent can waive the pending-validation finding. Do not claim N1x as supported until that validation passes and this row is promoted. | +| N1x FASTOS | Docker | Deferred | Unset | No | The accepted Deferred N1x Express preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. It does not activate DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Host identity requires Linux `arm64`, a regular `/etc/fastos-release` file of 1 through 4,096 bytes that is owned by UID 0 and GID 0, is not a symbolic link, is not group- or world-writable, and contains exactly one `NAME="N1x FASTOS"` line. It also requires NVIDIA vendor ID `10de` on a display-class PCI device. Generic DMI values are supplemental and FastOS version is not pinned. Physical CUDA and CDI checks passed on one N1x host, but full NemoClaw Express E2E validation and dedicated CI coverage are pending. `host.platform.supported` remains absent. After identity qualifies, accepting Express, declining or disabling Express, or setting `NEMOCLAW_PROVIDER` can waive only the pending-validation finding. Existing provider requirements still apply, and this waiver does not promote N1x. Do not claim N1x as supported until validation passes and this row is promoted. | | NVIDIA RTX (consumer and Pro workstation GPUs) | Docker | Deferred | P1 | No | The PRD marks this platform as P1. Covers RTX consumer cards and RTX Pro workstation cards on Linux hosts that meet the generic-Linux-GPU requirements (NVIDIA Container Toolkit + CDI present). The provider menu emits managed vLLM behind `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm` for this host class today; the end-to-end onboard path on this hardware is not yet validated in CI. | | Windows WSL2 | Docker Desktop (WSL backend) | Tested with limitations | P1 | No | Requires WSL2 with Docker Desktop backend. Qualifying Windows WSL N1x hosts can use the explicit Experimental managed llama.cpp Qwen 3.6 recipe after local-default Docker authority, N1x product identity, ARM64, 48,000 MiB Docker and GPU memory, driver, NVIDIA integration, and Docker Desktop GPU-passthrough checks pass. Other Windows WSL hosts retain Windows-host or WSL-local Ollama. Dedicated N1x WSL CI and physical end-to-end validation are pending. | {/* platform-matrix-full:end */} @@ -105,8 +105,8 @@ NemoClaw routes inference through the OpenShell gateway. Each row below is a pro | 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 llama.cpp (already running) | Experimental | Local OpenAI-compatible | Attaches only to an authenticated operator-managed server on loopback port `8081` that passes cooperative native llama.cpp fingerprinting. NemoClaw does not own its server, model, or lifecycle. | | Local llama.cpp (managed DGX Spark) | Experimental | Local OpenAI-compatible | Uses the repository-owned YAML recipe for NVIDIA Nemotron 3 Nano 30B-A3B, exact GGUF verification, and Docker lifecycle on a qualified DGX Spark host. The recipe remains unqualified for agent support. | -| 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 healthy CDI spec. Onboarding evaluates the canonical `host.gpu.nvidia_available`, `host.gpu.container_toolkit_available`, and `host.gpu.cdi_healthy` readiness capabilities before gateway, image, or sandbox lifecycle effects. NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag. Onboarding filters the validated image list by usable memory. In non-interactive mode, onboarding selects the first compatible model unless the operator sets `NEMOCLAW_MODEL`. Model selection uses detected free GPU memory. If free memory is unavailable, NemoClaw uses total GPU memory. On DGX Spark, NemoClaw caps usable memory at 50 percent of total memory to match NIM's reported unified-memory limit. The Nemotron 3 Super catalog minimum includes NIM's reported runtime allocation. Local NVIDIA NIM is unavailable on N1x. NemoClaw omits the provider from onboarding and rejects `NEMOCLAW_PROVIDER=nim-local` on N1x. Use the Deferred managed-vLLM preview on N1x. On Linux arm64 DGX Spark and DGX Station hosts, onboarding warns that some NIM images may not publish a `linux/arm64` manifest; the warning is advisory, and the selected image pull can still fail when the registry has no matching platform manifest. 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 | Unavailable on N1x. On other hosts, it appears in the onboarding menu when NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. No flag is required. The model is whatever the existing server serves. | +| 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 healthy CDI spec. Onboarding evaluates the canonical `host.gpu.nvidia_available`, `host.gpu.container_toolkit_available`, and `host.gpu.cdi_healthy` readiness capabilities before gateway, image, or sandbox lifecycle effects. NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag. Onboarding filters the validated image list by usable memory. In non-interactive mode, onboarding selects the first compatible model unless the operator sets `NEMOCLAW_MODEL`. Model selection uses detected free GPU memory. If free memory is unavailable, NemoClaw uses total GPU memory. On DGX Spark, NemoClaw caps usable memory at 50 percent of total memory to match NIM's reported unified-memory limit. The Nemotron 3 Super catalog minimum includes NIM's reported runtime allocation. Local NVIDIA NIM is unavailable on N1x. NemoClaw omits the provider from onboarding and rejects `NEMOCLAW_PROVIDER=nim-local` on N1x. Select another standard provider or use the Deferred managed-vLLM preview on N1x. On Linux arm64 DGX Spark and DGX Station hosts, onboarding warns that some NIM images may not publish a `linux/arm64` manifest; the warning is advisory, and the selected image pull can still fail when the registry has no matching platform manifest. 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 | On a qualified N1x host, this route requires explicit standard-onboarding intent and remains unvalidated. On other hosts, no explicit intent is required. It appears in the onboarding menu when NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. The model is whatever the existing server serves. | | Local vLLM (managed install/start) | Tested with limitations | Local OpenAI-compatible | Managed install/start requires Docker on `PATH`; attaching an already-running local vLLM server is a separate path. Appears by default on DGX Spark and qualifying DGX Station GB300 hosts. A labeled Deferred preview appears on hosts that match the N1x identity requirements; N1x remains Deferred until a physical NemoClaw Express E2E test passes. DGX Station is Tested with limitations across qualified profiles on one physical DGX Station GB300. Full Station Express end-to-end qualification for the accepted no-OTA DGX OS `7.6.x` profile is pending. Dual-Station configurations are not yet validated, and dedicated CI coverage is not available. For canonical Station qualification and host preparation, see the Additional Setup page for [OpenClaw](/user-guide/openclaw/get-started/additional-setup/dgx-station-preparation), [Hermes](/user-guide/hermes/get-started/additional-setup/dgx-station-preparation), or [Deep Agents](/user-guide/deepagents/get-started/additional-setup/dgx-station-preparation). Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`, NVIDIA Container Toolkit, and CDI. NemoClaw pins runtime images to immutable digests. Station Express defaults to `nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-NVFP4`; `--station-deepseek` selects `deepseek-ai/DeepSeek-V4-Flash`. Direct managed-vLLM defaults are `nvidia/Qwen3.6-35B-A3B-NVFP4` on DGX Spark and the Deferred N1x preview, `deepseek-ai/DeepSeek-V4-Flash` on DGX Station, and `nvidia/NVIDIA-Nemotron-3-Nano-4B-FP8` on generic Linux NVIDIA GPU hosts. Image pulls from `nvcr.io` require NGC registry login. | {/* provider-status-full:end */} diff --git a/docs/reference/system-readiness.mdx b/docs/reference/system-readiness.mdx index d9cc8654bb7..64a36befba2 100644 --- a/docs/reference/system-readiness.mdx +++ b/docs/reference/system-readiness.mdx @@ -177,12 +177,12 @@ Generic DMI values do not satisfy either requirement. A matching identity produces a `qualified` `host.platform.n1x` qualification, but `host.platform.supported` remains `absent` while N1x is Deferred. The blocking `host.platform.n1x_validation_pending` finding records the remaining physical NemoClaw Express E2E gate. -Explicit `NEMOCLAW_PROVIDER=install-vllm` intent, including acceptance of the installer N1x preview prompt, lets onboarding waive only that finding after `host.platform.n1x` is `present`. +Accepting Express, declining or disabling Express, or setting `NEMOCLAW_PROVIDER` lets onboarding waive only that finding after `host.platform.n1x` is `present`. The read-only `host probe` report remains `incompatible` and does not apply that onboarding exception. Invalid marker metadata, invalid or ambiguous contents, the wrong PCI identity, the wrong architecture, or an unavailable NVIDIA GPU produces an `unqualified` qualification and the blocking `host.platform.n1x_unqualified` finding. Unreadable marker or PCI evidence produces an `unknown` qualification and the blocking `host.platform.n1x_inconclusive` finding. -Neither finding can be waived by managed-vLLM intent. +Neither finding can be waived by onboarding intent. Bounded evidence appears under `host.platform.identity` with `product`, `nvidiaPlatform`, `n1xCandidate`, `n1xFastOsMarker`, and `n1xPciGpu` fields. ## Interpret NVIDIA GPU Integration diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index 5e7412a4e30..e2edd2e04b3 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -2040,7 +2040,7 @@ Ollama can serve local chat and some simple tool surfaces, but agent loops with | Long system prompts or sender metadata | Risky | Yes | | Multi-turn tool dispatch | Risky | Yes | -On hosts other than N1x, [set up vLLM](../inference/local-inference/set-up-vllm) with automatic tool choice and the tool-call parser that matches the model family for persistent agent use. After the parser-aware server is ready, re-run onboarding. Select the **Local vLLM** entry marked **running (suggested)** for a server detected on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`; for another address, select **Other OpenAI-compatible endpoint**. On generic hosts, the Local vLLM entry includes an experimental label; on DGX Spark or DGX Station, it does not. On N1x, stop any server that occupies `${NEMOCLAW_VLLM_PORT:-8000}`, then use only the Deferred managed-vLLM preview; existing and compatible servers are not admitted. +[Set up vLLM](../inference/local-inference/set-up-vllm) with automatic tool choice and the tool-call parser that matches the model family for persistent agent use. After the parser-aware server is ready, re-run onboarding. Select the **Local vLLM** entry for a server detected on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`; for another address, select **Other OpenAI-compatible endpoint**. On generic hosts, the Local vLLM entry includes an experimental label; on DGX Spark or DGX Station, it does not. On N1x, decline or disable Express, or set `NEMOCLAW_PROVIDER=vllm`, to enter standard onboarding; this route remains unvalidated on N1x. Do not rely on direct edits to `openclaw.json` for a persistent provider change. NemoClaw-managed rebuilds can overwrite those edits, while onboarding keeps the sandbox image, OpenShell route, and host-managed credentials aligned. diff --git a/docs/resources/starter-prompt.md b/docs/resources/starter-prompt.md index e0c0c9c4adb..5de05610ffa 100644 --- a/docs/resources/starter-prompt.md +++ b/docs/resources/starter-prompt.md @@ -89,17 +89,17 @@ Do not load a platform asset for any other computer. ## Runtime and Provider Selection If the Windows WSL platform asset applies, follow that asset's provider selection and skip the native-N1x branch below. -If the readiness check confirms native N1x (not Windows WSL), do not show the generic provider menu. -Offer only the Deferred managed-vLLM preview with `NEMOCLAW_PROVIDER=install-vllm`. +If the readiness check confirms native N1x (not Windows WSL), offer the Deferred managed-vLLM preview before the generic provider menu. +If the user accepts, set `NEMOCLAW_PROVIDER=install-vllm`. Explain that N1x remains outside the supported-platform set pending complete physical NemoClaw Express E2E validation, accepting this path is explicit preview intent, and the preview uses one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. -Do not offer or reuse an existing vLLM server on N1x. -If the configured vLLM port, `${NEMOCLAW_VLLM_PORT:-8000}`, is occupied, stop and ask the user to stop that server before trying the Deferred preview again. -If the user declines the preview, stop before installation. +If the user declines, continue to the provider question below and explain that each provider retains its existing requirements. +Do not offer local NVIDIA NIM on native N1x. +If the user selects managed vLLM and the configured port, `${NEMOCLAW_VLLM_PORT:-8000}`, is occupied, stop and ask the user to stop that server before trying the Deferred preview again. -For a computer other than native N1x, if no platform asset applies or its offered install path is declined, ask: "Which inference runtime or provider would you like?" +If no platform asset applies or its offered install path is declined, ask: "Which inference runtime or provider would you like?" Choices: -1. Existing vLLM, only when the computer is not N1x and a ready server is detected on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. +1. Existing vLLM, when a ready server is detected on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`; native N1x requires explicit standard-onboarding intent. 2. Managed vLLM, optimized local inference with a large download. 3. Local Ollama, only when the selected agent and platform support it. 4. NVIDIA Endpoints, which requires an NVIDIA API key. @@ -191,8 +191,8 @@ Use this provider mapping for non-interactive setup: - OpenAI-compatible: `NEMOCLAW_PROVIDER=custom`, endpoint, model, `COMPATIBLE_API_KEY`. - Anthropic-compatible: `NEMOCLAW_PROVIDER=anthropicCompatible`, endpoint, model, `COMPATIBLE_ANTHROPIC_API_KEY`. - Ollama: `NEMOCLAW_PROVIDER=ollama`, optional `NEMOCLAW_MODEL`. -- Existing vLLM: `NEMOCLAW_PROVIDER=vllm`; unavailable on native N1x. -- Managed vLLM: `NEMOCLAW_PROVIDER=install-vllm`; this is the only admitted native-N1x provider and requires explicit Deferred preview intent there. Qualifying N1x WSL hosts instead follow the Windows WSL asset's managed llama.cpp path. Use an approved optional model override only when the selected platform supports it. +- Existing vLLM: `NEMOCLAW_PROVIDER=vllm`; on native N1x, this value supplies explicit standard-onboarding intent, but the route remains unvalidated. +- Managed vLLM: `NEMOCLAW_PROVIDER=install-vllm`; on native N1x, this value supplies explicit Deferred preview intent. Qualifying N1x WSL hosts instead follow the Windows WSL asset's managed llama.cpp path. Use an approved optional model override only when the selected platform supports it. Do not offer Hermes Provider for OpenClaw or Deep Agents. diff --git a/docs/security/best-practices.mdx b/docs/security/best-practices.mdx index 6bf2cb90e9b..c99295dc16b 100644 --- a/docs/security/best-practices.mdx +++ b/docs/security/best-practices.mdx @@ -331,19 +331,20 @@ Review the data policies of any cloud provider you use. The `NEMOCLAW_EXPERIMENTAL=1` environment variable gates local NVIDIA NIM on eligible hosts other than native N1x FASTOS and generic Linux managed vLLM install/start. Managed vLLM install/start is Docker-backed and requires Docker on `PATH`; attaching an already-running local vLLM server is separate. DGX Spark and DGX Station managed vLLM entries appear by default when Docker is available. -Native N1x FASTOS omits local NVIDIA NIM and offers only the Deferred managed-vLLM preview. +Native N1x FASTOS omits local NVIDIA NIM and offers the Deferred managed-vLLM preview or explicitly selected standard onboarding. Native N1x FASTOS remains outside the supported-platform set because its physical NemoClaw Express E2E validation is incomplete. -After NemoClaw qualifies the native N1x identity, you must provide explicit managed-vLLM preview intent before onboarding can use this path. +After NemoClaw qualifies the native N1x identity, you must provide explicit Express or standard-onboarding intent. A qualifying Windows WSL N1x host uses managed llama.cpp through Windows Express. The installer uses local Docker Desktop, Arm64, the Windows N1x product identity, and at least 48,000 MiB of GPU memory as preliminary selection conditions. Onboarding also requires the default local Docker context, at least 48,000 MiB of Docker memory, driver version `580.65.06` or later, Docker storage and runtime readiness, NVIDIA GPU integration, and a successful Docker Desktop GPU passthrough proof. Onboarding stops managed selection when any required readiness check fails. Other Windows WSL hosts retain Windows-host or WSL-local Ollama. -On hosts other than native N1x FASTOS, an already-running vLLM server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}` also appears in the menu without a flag because selecting it is an explicit user action. +An already-running vLLM server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}` appears in standard onboarding because selecting it is an explicit user action. +Native N1x requires you to decline or disable Express or set `NEMOCLAW_PROVIDER=vllm` first. | Aspect | Detail | |---|---| -| Default | Local NVIDIA NIM and generic Linux managed vLLM install/start are hidden. When Docker is available, DGX Spark and DGX Station managed vLLM entries are offered by default. On hosts other than native N1x FASTOS, already-running vLLM on `localhost:${NEMOCLAW_VLLM_PORT:-8000}` is offered when detected. Qualifying Windows WSL N1x Express runs use managed llama.cpp. | +| Default | Local NVIDIA NIM and generic Linux managed vLLM install/start are hidden. When Docker is available, DGX Spark and DGX Station managed vLLM entries are offered by default. Standard onboarding offers already-running vLLM on `localhost:${NEMOCLAW_VLLM_PORT:-8000}` when detected; native N1x requires explicit standard-onboarding intent. Qualifying Windows WSL N1x Express runs use managed llama.cpp. | | What you can change | On eligible hosts other than native N1x FASTOS, set `NEMOCLAW_EXPERIMENTAL=1` before onboarding to surface Local NIM and, when Docker is available on `PATH`, generic Linux managed vLLM. To request only the Docker-backed managed vLLM path non-interactively, set `NEMOCLAW_PROVIDER=install-vllm`. | | Risk if selected | NemoClaw has not fully validated these providers. NIM requires a NIM-capable GPU. The managed vLLM path requires Docker, pulls a container image, and starts it on a supported NVIDIA GPU host. Misconfiguration can cause failed inference or unexpected behavior. | | Recommendation | Use experimental providers only for evaluation. Do not rely on them for always-on assistants. | diff --git a/scripts/install.sh b/scripts/install.sh index 7b5ba14f419..0ab576a774c 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -3683,7 +3683,10 @@ run_installer_host_preflight() { try { const { assessHost, planHostAdvisories } = require(preflightPath); const { createHostReadinessReport } = require(hostReadinessPath); - const { evaluateOnboardReadinessAdmission } = require(onboardAdmissionPath); + const { + evaluateOnboardReadinessAdmission, + hasExplicitDeferredN1xOnboardingIntent, + } = require(onboardAdmissionPath); const { loadGatewayManagementDeclaration } = require(gatewayManagementPath); const { configuredRuntimeProviderOwnsHostReadiness } = require(gatewayRuntimePath); const host = assessHost(); @@ -3718,6 +3721,7 @@ run_installer_host_preflight() { // authoritative onboarding gate apply supported storage remediation, // but only when the gateway declaration confirms NemoClaw ownership. allowStorageRemediation, + allowDeferredN1x: hasExplicitDeferredN1xOnboardingIntent(process.env), }); const infoLines = []; const actionLines = []; @@ -6053,9 +6057,6 @@ maybe_offer_express_install() { # On a detected Express platform but a skip condition applies — explain why so # the user understands they could have gotten express otherwise. if [ "${NEMOCLAW_NO_EXPRESS:-}" = "1" ]; then - if [ "$platform" = "N1x" ] && [ "${NEMOCLAW_PROVIDER:-}" != "install-vllm" ]; then - error "N1x onboarding currently requires explicit Deferred managed-vLLM preview intent. Remove NEMOCLAW_NO_EXPRESS=1 and accept the preview, or set NEMOCLAW_PROVIDER=install-vllm." - fi if [ "$platform" = "DGX Station" ]; then station_dual_pair_resume_pending \ && error "A dual-DGX Station pair resume is pending; finish exact pair revalidation before disabling Station setup." @@ -6065,9 +6066,6 @@ maybe_offer_express_install() { return 0 fi if [ -n "${NEMOCLAW_PROVIDER:-}" ]; then - if [ "$platform" = "N1x" ] && [ "$NEMOCLAW_PROVIDER" != "install-vllm" ]; then - error "N1x onboarding currently accepts only the Deferred managed-vLLM preview. Set NEMOCLAW_PROVIDER=install-vllm or use another host for provider ${NEMOCLAW_PROVIDER}." - fi if [ "$platform" = "DGX Station" ] && [ "$NEMOCLAW_PROVIDER" = "install-vllm" ]; then # An explicit managed-vLLM provider selects the same Station host/pair # preparation boundary without forcing the rest of the express policy. @@ -6160,7 +6158,7 @@ maybe_offer_express_install() { ;; *) if [ "$platform" = "N1x" ]; then - error "N1x onboarding currently requires the Deferred managed-vLLM preview. Re-run the installer and accept the preview, or set NEMOCLAW_PROVIDER=install-vllm." + export NEMOCLAW_NO_EXPRESS=1 fi info "Skipping express install. Continuing with interactive flow." ;; diff --git a/src/lib/inference/serving/resolver.ts b/src/lib/inference/serving/resolver.ts index f380de29db9..5123754bfd0 100644 --- a/src/lib/inference/serving/resolver.ts +++ b/src/lib/inference/serving/resolver.ts @@ -122,7 +122,7 @@ function hasAdmittedReadinessException( explicitlyOptedOutGpuPassthrough: false, allowUnsupportedRuntime: false, allowStorageRemediation: remediableStorage, - allowDeferredN1xManagedVllm, + allowDeferredN1x: allowDeferredN1xManagedVllm, }); if (!admission.admitted || admission.waivedFindingIds.length === 0) return false; const waivedFindingIds = new Set(admission.waivedFindingIds); diff --git a/src/lib/onboard/fatal-runtime-preflight.test.ts b/src/lib/onboard/fatal-runtime-preflight.test.ts index 4c0651bce19..374c9ce87b9 100644 --- a/src/lib/onboard/fatal-runtime-preflight.test.ts +++ b/src/lib/onboard/fatal-runtime-preflight.test.ts @@ -152,7 +152,7 @@ afterEach(() => { }); describe("report-backed runtime readiness (#7411)", () => { - it("requires explicit managed-vLLM intent and lets rebuild reject ambient intent (#9292)", () => { + it("admits explicit N1x onboarding intent and lets rebuild reject ambient intent (#11041)", () => { const readiness: SystemReadinessReport = { schemaVersion: "1.1.0", status: "incompatible", @@ -206,7 +206,7 @@ describe("report-backed runtime readiness (#7411)", () => { }), ).toBe(readiness); - vi.stubEnv("NEMOCLAW_PROVIDER", "install-vllm"); + vi.stubEnv("NEMOCLAW_PROVIDER", "ollama"); expect( assertOnboardSystemReadiness(readiness, hostWithRuntime("docker"), { explicitlyOptedOutGpuPassthrough: false, @@ -214,6 +214,16 @@ describe("report-backed runtime readiness (#7411)", () => { }), ).toBe(readiness); + vi.stubEnv("NEMOCLAW_PROVIDER", ""); + vi.stubEnv("NEMOCLAW_NO_EXPRESS", "1"); + expect( + assertOnboardSystemReadiness(readiness, hostWithRuntime("docker"), { + explicitlyOptedOutGpuPassthrough: false, + exitProcess: exit as never, + }), + ).toBe(readiness); + + vi.stubEnv("NEMOCLAW_PROVIDER", "ollama"); expect(() => assertOnboardSystemReadiness(readiness, hostWithRuntime("docker"), { explicitlyOptedOutGpuPassthrough: false, diff --git a/src/lib/onboard/fatal-runtime-preflight.ts b/src/lib/onboard/fatal-runtime-preflight.ts index 5609b9ba2f0..3b1c2ef3067 100644 --- a/src/lib/onboard/fatal-runtime-preflight.ts +++ b/src/lib/onboard/fatal-runtime-preflight.ts @@ -21,6 +21,7 @@ import { import { evaluateOnboardGatewayReadinessAdmission, evaluateOnboardReadinessAdmission, + hasExplicitDeferredN1xOnboardingIntent, } from "../readiness/onboard-admission"; import { composeSystemReadinessReport } from "../readiness/system"; import type { SystemReadinessReport } from "../readiness/types"; @@ -46,7 +47,6 @@ import { validateSandboxGpuPreflight, } from "./sandbox-gpu-preflight"; import type { OnboardOptions } from "./types"; -import { MANAGED_VLLM_PROVIDER_KEY } from "./vllm-menu"; export type FatalRuntimePreflightOptions = Pick< OnboardOptions, @@ -178,9 +178,8 @@ export function assertOnboardSystemReadiness( providerOwnsHostReadiness: selectedRuntimeOwnsHostReadiness, allowStorageRemediation: options.allowStorageRemediation === true, allowPortableHostPreparation: options.allowPortableHostPreparation, - allowDeferredN1xManagedVllm: - options.allowDeferredN1xManagedVllm ?? - process.env.NEMOCLAW_PROVIDER === MANAGED_VLLM_PROVIDER_KEY, + allowDeferredN1x: + options.allowDeferredN1xManagedVllm ?? hasExplicitDeferredN1xOnboardingIntent(process.env), }); const advisories = planHostAdvisories(host, { providerOwnsHostReadiness: selectedRuntimeOwnsHostReadiness, diff --git a/src/lib/onboard/vllm-menu.test.ts b/src/lib/onboard/vllm-menu.test.ts index c4595a6a78a..96c5e50743a 100644 --- a/src/lib/onboard/vllm-menu.test.ts +++ b/src/lib/onboard/vllm-menu.test.ts @@ -119,7 +119,7 @@ describe("buildVllmMenuEntries", () => { log: () => {}, }); - // N1x readiness rejects the running-server state without explicit managed + // N1x readiness rejects the running-server state without explicit onboarding // intent. If this lower-level helper sees that state in isolation, it must // not mislabel an operator-managed server as the Deferred managed preview. assert.equal(install[0].label, "Install vLLM (N1x) [Deferred preview]"); diff --git a/src/lib/readiness/onboard-admission.test.ts b/src/lib/readiness/onboard-admission.test.ts index 0892a931b61..398f1cb6f90 100644 --- a/src/lib/readiness/onboard-admission.test.ts +++ b/src/lib/readiness/onboard-admission.test.ts @@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest"; import { evaluateOnboardGatewayReadinessAdmission, evaluateOnboardReadinessAdmission, + hasExplicitDeferredN1xOnboardingIntent, ONBOARD_READINESS_ADMISSION_REASON_IDS, ONBOARD_READINESS_FINDING_IDS, ONBOARD_REQUIRED_CAPABILITY_IDS, @@ -101,6 +102,20 @@ function withCapabilityState( } describe("onboarding readiness admission (#7411)", () => { + it.each([ + ["preview decline", { NEMOCLAW_NO_EXPRESS: "1" }, true], + ["explicit non-Express provider", { NEMOCLAW_PROVIDER: "ollama" }, true], + ["explicit managed provider", { NEMOCLAW_PROVIDER: "install-vllm" }, true], + ["blank provider", { NEMOCLAW_PROVIDER: " " }, false], + ["disabled Express override", { NEMOCLAW_NO_EXPRESS: "0" }, false], + ["no intent", {}, false], + ] as const)( + "recognizes %s as Deferred N1x onboarding intent (#11041)", + (_scenario, env, expected) => { + expect(hasExplicitDeferredN1xOnboardingIntent(env)).toBe(expected); + }, + ); + it("admits known required capabilities without relying on the report status", () => { expect( evaluateOnboardReadinessAdmission(report({ status: "inconclusive" }), DEFAULT_OPTIONS), @@ -385,7 +400,7 @@ describe("onboarding readiness admission (#7411)", () => { }); }); - it("admits only explicit managed-vLLM intent through the Deferred N1x validation gate (#8574)", () => { + it("admits explicit onboarding intent only for a qualified Deferred N1x identity (#11041)", () => { const capabilities = [ ...withCapabilityState( requiredCapabilities(), @@ -405,17 +420,38 @@ describe("onboarding readiness admission (#7411)", () => { expect( evaluateOnboardReadinessAdmission( report({ capabilities, findings: [pending], status: "incompatible" }), - { ...DEFAULT_OPTIONS, allowDeferredN1xManagedVllm: true }, + { ...DEFAULT_OPTIONS, allowDeferredN1x: true }, ), ).toEqual({ admitted: true, waivedFindingIds: [pending.id] }); expect( evaluateOnboardReadinessAdmission(report({ findings: [pending], status: "incompatible" }), { ...DEFAULT_OPTIONS, - allowDeferredN1xManagedVllm: true, + allowDeferredN1x: true, }), ).toMatchObject({ admitted: false, findingIds: [pending.id] }); }); + it.each(["absent", "unknown"] as const)( + "keeps Deferred N1x blocked when its identity capability is %s (#11041)", + (state) => { + const pending = finding(ONBOARD_READINESS_FINDING_IDS.n1xValidationPending); + const capabilities = [ + ...withCapabilityState( + requiredCapabilities(), + ONBOARD_REQUIRED_CAPABILITY_IDS.platformSupported, + "absent", + ), + capability("host.platform.n1x", state), + ]; + expect( + evaluateOnboardReadinessAdmission( + report({ capabilities, findings: [pending], status: "incompatible" }), + { ...DEFAULT_OPTIONS, allowDeferredN1x: true }, + ), + ).toMatchObject({ admitted: false, findingIds: [pending.id] }); + }, + ); + it("fails closed when a required capability is unknown or missing", () => { const capabilities = withCapabilityState( requiredCapabilities().filter( diff --git a/src/lib/readiness/onboard-admission.ts b/src/lib/readiness/onboard-admission.ts index e2ba71e6212..9615e018686 100644 --- a/src/lib/readiness/onboard-admission.ts +++ b/src/lib/readiness/onboard-admission.ts @@ -11,6 +11,12 @@ export const ONBOARD_READINESS_ADMISSION_REASON_IDS = { export type OnboardReadinessAdmissionReasonId = (typeof ONBOARD_READINESS_ADMISSION_REASON_IDS)[keyof typeof ONBOARD_READINESS_ADMISSION_REASON_IDS]; +export function hasExplicitDeferredN1xOnboardingIntent( + env: Readonly>, +): boolean { + return env.NEMOCLAW_NO_EXPRESS === "1" || String(env.NEMOCLAW_PROVIDER ?? "").trim() !== ""; +} + export const ONBOARD_READINESS_FINDING_IDS = { containerToolkitMissing: "host.gpu.container_toolkit_missing", cdiMissing: "host.gpu.cdi_missing", @@ -53,8 +59,8 @@ export interface OnboardReadinessAdmissionOptions { allowStorageRemediation: boolean; /** The explicit portable profile may prepare its rootless runtime before revalidation. */ allowPortableHostPreparation?: boolean; - /** Explicit managed-vLLM intent may exercise the Deferred N1x validation path. */ - allowDeferredN1xManagedVllm?: boolean; + /** Explicit Express or standard-onboarding intent may exercise the Deferred N1x path. */ + allowDeferredN1x?: boolean; } export type OnboardReadinessAdmissionDecision = @@ -144,7 +150,7 @@ function canWaiveFinding( return true; } if ( - options.allowDeferredN1xManagedVllm && + options.allowDeferredN1x && finding.id === ONBOARD_READINESS_FINDING_IDS.n1xValidationPending && capabilityState(capabilities, "host.platform.n1x") === "present" ) { diff --git a/test/helpers/installer-express-prompt-pty-harness.ts b/test/helpers/installer-express-prompt-pty-harness.ts index 3e232eace25..e1d6593495a 100644 --- a/test/helpers/installer-express-prompt-pty-harness.ts +++ b/test/helpers/installer-express-prompt-pty-harness.ts @@ -64,11 +64,11 @@ classify_dgx_station_release() { printf "%s" "\${EXPRESS_RELEASE_STATE:-generic- station_installer_revision() { printf 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; } station_express_resume_generation() { printf '0123456789abcdef0123456789abcdef'; } bash() { - printf "RESULT NON_INTERACTIVE=%s SUDO_MODE=%s PROVIDER=%s MODEL=%s VLLM_MODEL=%s POLICY=%s YES=%s SANDBOX=%s STATION_EXPRESS=%s PROFILE_GATE=%s PROFILE_RUNTIME=%s SPARK_SELECTION=%s\\n" \ + printf "RESULT NON_INTERACTIVE=%s SUDO_MODE=%s PROVIDER=%s MODEL=%s VLLM_MODEL=%s POLICY=%s YES=%s SANDBOX=%s STATION_EXPRESS=%s PROFILE_GATE=%s PROFILE_RUNTIME=%s SPARK_SELECTION=%s NO_EXPRESS=%s\\n" \ "\${NON_INTERACTIVE:-}" "\${NEMOCLAW_NON_INTERACTIVE_SUDO_MODE:-}" "\${NEMOCLAW_PROVIDER:-}" "\${NEMOCLAW_MODEL:-}" \ "\${NEMOCLAW_VLLM_MODEL:-}" "\${NEMOCLAW_POLICY_MODE:-}" "\${NEMOCLAW_YES:-}" "\${NEMOCLAW_SANDBOX_NAME:-}" \ "\${NEMOCLAW_STATION_EXPRESS:-}" "\${NEMOCLAW_ENABLE_LOCAL_MODEL_PROFILE:-}" "\${NEMOCLAW_LOCAL_MODEL_RUNTIME:-}" \ - "\${_SPARK_EXPRESS_INFERENCE_SELECTION:-}" + "\${_SPARK_EXPRESS_INFERENCE_SELECTION:-}" "\${NEMOCLAW_NO_EXPRESS:-}" exit 0 } main "$@" @@ -85,11 +85,11 @@ if [ "\${FORCE_EXPRESS_PROMPT_READ_FAILURE:-}" = "1" ]; then read() { return 1; } fi maybe_offer_express_install -printf "RESULT NON_INTERACTIVE=%s SUDO_MODE=%s PROVIDER=%s MODEL=%s VLLM_MODEL=%s POLICY=%s YES=%s SANDBOX=%s STATION_EXPRESS=%s PROFILE_GATE=%s PROFILE_RUNTIME=%s SPARK_SELECTION=%s\\n" \\ +printf "RESULT NON_INTERACTIVE=%s SUDO_MODE=%s PROVIDER=%s MODEL=%s VLLM_MODEL=%s POLICY=%s YES=%s SANDBOX=%s STATION_EXPRESS=%s PROFILE_GATE=%s PROFILE_RUNTIME=%s SPARK_SELECTION=%s NO_EXPRESS=%s\\n" \\ "\${NON_INTERACTIVE:-}" "\${NEMOCLAW_NON_INTERACTIVE_SUDO_MODE:-}" "\${NEMOCLAW_PROVIDER:-}" "\${NEMOCLAW_MODEL:-}" \\ "\${NEMOCLAW_VLLM_MODEL:-}" "\${NEMOCLAW_POLICY_MODE:-}" "\${NEMOCLAW_YES:-}" "\${NEMOCLAW_SANDBOX_NAME:-}" \\ "\${NEMOCLAW_STATION_EXPRESS:-}" "\${NEMOCLAW_ENABLE_LOCAL_MODEL_PROFILE:-}" "\${NEMOCLAW_LOCAL_MODEL_RUNTIME:-}" \\ - "\${_SPARK_EXPRESS_INFERENCE_SELECTION:-}" + "\${_SPARK_EXPRESS_INFERENCE_SELECTION:-}" "\${NEMOCLAW_NO_EXPRESS:-}" ''' env = dict(os.environ) env["INSTALLER_UNDER_TEST"] = installer diff --git a/test/helpers/installer-readiness-stubs.ts b/test/helpers/installer-readiness-stubs.ts index f352079265f..317a9075944 100644 --- a/test/helpers/installer-readiness-stubs.ts +++ b/test/helpers/installer-readiness-stubs.ts @@ -84,6 +84,8 @@ export function writeInstallerReadinessModuleStubs(readinessDir: string): void { waivedFindingIds: [], }; }; +exports.hasExplicitDeferredN1xOnboardingIntent = (env) => + env.NEMOCLAW_NO_EXPRESS === "1" || String(env.NEMOCLAW_PROVIDER || "").trim() !== ""; `, ); fs.writeFileSync( diff --git a/test/install/install-express-n1x.test.ts b/test/install/install-express-n1x.test.ts index 5231e31a82a..f4962e7a64e 100644 --- a/test/install/install-express-n1x.test.ts +++ b/test/install/install-express-n1x.test.ts @@ -30,29 +30,36 @@ describe("installer N1x Express preview", () => { ); }); - it("stops before onboarding when the Deferred preview is declined (#8574)", () => { + it("continues with ordinary onboarding when the Deferred preview is declined (#11041)", () => { // Model curl | bash: stdin is the script pipe, while the reply reaches the controlling /dev/tty. const result = runExpressPromptWithTty("n\n", "pipe", "N1x"); const output = `${result.stdout}${result.stderr}`; - expect(result.status, output).toBe(1); - expect(output).toMatch(/N1x onboarding currently requires the Deferred managed-vLLM preview/); - expect(output).toMatch(/accept the preview, or set NEMOCLAW_PROVIDER=install-vllm/); - expect(output).not.toMatch(/Continuing with interactive flow/); - expect(output).not.toMatch(/RESULT /); + expect(result.status, output).toBe(0); + expect(output).toMatch(/Skipping express install\. Continuing with interactive flow/); + expect(output).toMatch(/RESULT .*NO_EXPRESS=1/); }); - it("rejects NEMOCLAW_NO_EXPRESS without explicit managed-vLLM intent (#8574)", () => { + it("continues with ordinary onboarding when NEMOCLAW_NO_EXPRESS is set (#11041)", () => { const result = runExpressPromptWithTty("", "pipe", "N1x", { NEMOCLAW_NO_EXPRESS: "1", }); const output = `${result.stdout}${result.stderr}`; - expect(result.status, output).toBe(1); - expect(output).toMatch(/explicit Deferred managed-vLLM preview intent/); - expect(output).toMatch(/Remove NEMOCLAW_NO_EXPRESS=1 and accept the preview/); - expect(output).toMatch(/set NEMOCLAW_PROVIDER=install-vllm/); - expect(output).not.toMatch(/RESULT /); + expect(result.status, output).toBe(0); + expect(output).toMatch(/Skipping express prompt \(NEMOCLAW_NO_EXPRESS=1\)/); + expect(output).toMatch(/RESULT .*NO_EXPRESS=1/); + }); + + it("continues with an explicit non-Express provider on N1x (#11041)", () => { + const result = runExpressPromptWithTty("", "pipe", "N1x", { + NEMOCLAW_PROVIDER: "ollama", + }); + const output = `${result.stdout}${result.stderr}`; + + expect(result.status, output).toBe(0); + expect(output).toMatch(/Skipping express prompt \(NEMOCLAW_PROVIDER=ollama already set\)/); + expect(output).toMatch(/RESULT .*PROVIDER=ollama/); }); it("allows the N1x prompt bypass with explicit managed-vLLM intent (#8574)", () => { diff --git a/test/package-contract/installer-host-preflight.test.ts b/test/package-contract/installer-host-preflight.test.ts index 95e80aee7b3..838423fc170 100644 --- a/test/package-contract/installer-host-preflight.test.ts +++ b/test/package-contract/installer-host-preflight.test.ts @@ -30,6 +30,7 @@ exit 99`, function runInstallerHostAdmissionTest( host: { runtime: string; + isN1x?: boolean; hasNestedOverlayConflict?: boolean; isUnsupportedRuntime?: boolean; additionalFindingIds?: string[]; @@ -42,6 +43,8 @@ function runInstallerHostAdmissionTest( gatewayManagementMode?: string; portableProfileArtifact?: "present" | "missing"; providerResolutionFailure?: string; + provider?: string; + noExpress?: boolean; } = {}, ) { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-installer-host-admission-")); @@ -156,6 +159,11 @@ exports.evaluateOnboardReadinessAdmission = (report, options) => { ]); const findingIds = report.findings .filter((finding) => { + if ( + finding.id === "host.platform.n1x_validation_pending" && + options.allowDeferredN1x && + report.host.isN1x + ) return false; if ( options.providerOwnsHostReadiness && providerOwnedDockerFindings.has(finding.id) @@ -188,6 +196,8 @@ exports.evaluateOnboardReadinessAdmission = (report, options) => { ? { admitted: true, waivedFindingIds: [] } : { admitted: false, reasonIds: [], findingIds, capabilityIds, waivedFindingIds: [] }; }; +exports.hasExplicitDeferredN1xOnboardingIntent = (env) => + env.NEMOCLAW_NO_EXPRESS === "1" || String(env.NEMOCLAW_PROVIDER || "").trim() !== ""; `, ); writeNodeStub(fakeBin); @@ -195,6 +205,8 @@ exports.evaluateOnboardReadinessAdmission = (report, options) => { const { NEMOCLAW_EXPERIMENTAL_PROFILE: _experimentalProfile, NEMOCLAW_GATEWAY_RUNTIME: _gatewayRuntime, + NEMOCLAW_NO_EXPRESS: _noExpress, + NEMOCLAW_PROVIDER: _provider, TEST_GATEWAY_MANAGEMENT_MODE: _gatewayManagementMode, ...inheritedEnv } = process.env; @@ -208,6 +220,8 @@ exports.evaluateOnboardReadinessAdmission = (report, options) => { ? { NEMOCLAW_EXPERIMENTAL_PROFILE: options.experimentalProfile } : {}), ...(options.gatewayRuntime ? { NEMOCLAW_GATEWAY_RUNTIME: options.gatewayRuntime } : {}), + ...(options.noExpress ? { NEMOCLAW_NO_EXPRESS: "1" } : {}), + ...(options.provider ? { NEMOCLAW_PROVIDER: options.provider } : {}), TEST_GATEWAY_MANAGEMENT_MODE: options.gatewayManagementMode ?? "", }; @@ -232,6 +246,35 @@ run_installer_host_preflight } describe("installer host preflight package contract", () => { + it.each([ + ["a declined Express preview", { noExpress: true }], + ["an explicit standard provider", { provider: "ollama" }], + ])("admits qualified Deferred N1x through %s (#11041)", (_scenario, intent) => { + const { output, result } = runInstallerHostAdmissionTest( + { + runtime: "docker", + isN1x: true, + additionalFindingIds: ["host.platform.n1x_validation_pending"], + }, + undefined, + intent, + ); + + expect(result.status, output).toBe(0); + expect(output).not.toMatch(/Host preflight found issues/); + }); + + it("keeps Deferred N1x blocked without explicit onboarding intent (#11041)", () => { + const { output, result } = runInstallerHostAdmissionTest({ + runtime: "docker", + isN1x: true, + additionalFindingIds: ["host.platform.n1x_validation_pending"], + }); + + expect(result.status).toBe(1); + expect(output).toContain("host.platform.n1x_validation_pending"); + }); + it("continues to onboarding when managed storage remediation is available", () => { const { output, result } = runInstallerHostAdmissionTest({ runtime: "docker", From 393f741fc555abf6e8f228400d0bcc596f3627c7 Mon Sep 17 00:00:00 2001 From: San Dang Date: Fri, 4 Sep 2026 16:53:31 +0700 Subject: [PATCH 02/19] fix(onboard): harden N1x standard admission Signed-off-by: San Dang --- .../onboard/fatal-runtime-preflight.test.ts | 27 +++++++- src/lib/onboard/fatal-runtime-preflight.ts | 11 ++-- .../provider-selection-keys.ts | 54 ++++++++++++++++ .../machine/handlers/preflight.test.ts | 61 ++++++++++++++++++- src/lib/onboard/machine/handlers/preflight.ts | 16 ++++- src/lib/onboard/providers.ts | 42 +++---------- src/lib/readiness/onboard-admission.test.ts | 4 ++ src/lib/readiness/onboard-admission.ts | 3 +- .../installer-express-prompt-pty-harness.ts | 53 +++++++++++++++- test/helpers/installer-readiness-stubs.ts | 25 +++++++- test/install/install-express-n1x.test.ts | 26 ++++++++ .../installer-host-preflight.test.ts | 22 ++++++- 12 files changed, 290 insertions(+), 54 deletions(-) create mode 100644 src/lib/onboard/inference-providers/provider-selection-keys.ts diff --git a/src/lib/onboard/fatal-runtime-preflight.test.ts b/src/lib/onboard/fatal-runtime-preflight.test.ts index 374c9ce87b9..cbc5bfcb8eb 100644 --- a/src/lib/onboard/fatal-runtime-preflight.test.ts +++ b/src/lib/onboard/fatal-runtime-preflight.test.ts @@ -201,7 +201,7 @@ describe("report-backed runtime readiness (#7411)", () => { expect( assertOnboardSystemReadiness(readiness, hostWithRuntime("docker"), { explicitlyOptedOutGpuPassthrough: false, - allowDeferredN1xManagedVllm: true, + allowDeferredN1xOnboarding: true, exitProcess: exit as never, }), ).toBe(readiness); @@ -227,7 +227,30 @@ describe("report-backed runtime readiness (#7411)", () => { expect(() => assertOnboardSystemReadiness(readiness, hostWithRuntime("docker"), { explicitlyOptedOutGpuPassthrough: false, - allowDeferredN1xManagedVllm: false, + allowDeferredN1xOnboarding: false, + exitProcess: exit as never, + }), + ).toThrow("exit"); + + vi.stubEnv("NEMOCLAW_NO_EXPRESS", "0"); + vi.stubEnv("NEMOCLAW_PROVIDER", "unknown-provider"); + expect(() => + assertOnboardSystemReadiness(readiness, hostWithRuntime("docker"), { + explicitlyOptedOutGpuPassthrough: false, + exitProcess: exit as never, + }), + ).toThrow("exit"); + vi.stubEnv("NEMOCLAW_PROVIDER", "nim"); + expect(() => + assertOnboardSystemReadiness(readiness, hostWithRuntime("docker"), { + explicitlyOptedOutGpuPassthrough: false, + exitProcess: exit as never, + }), + ).toThrow("exit"); + vi.stubEnv("NEMOCLAW_PROVIDER", "nim-local"); + expect(() => + assertOnboardSystemReadiness(readiness, hostWithRuntime("docker"), { + explicitlyOptedOutGpuPassthrough: false, exitProcess: exit as never, }), ).toThrow("exit"); diff --git a/src/lib/onboard/fatal-runtime-preflight.ts b/src/lib/onboard/fatal-runtime-preflight.ts index 3b1c2ef3067..9093f7f0d0d 100644 --- a/src/lib/onboard/fatal-runtime-preflight.ts +++ b/src/lib/onboard/fatal-runtime-preflight.ts @@ -119,7 +119,8 @@ export interface OnboardHostReadinessOptions { resuming?: boolean; allowStorageRemediation?: boolean; allowPortableHostPreparation?: boolean; - allowDeferredN1xManagedVllm?: boolean; + /** A trusted current or recorded choice may exercise the Deferred N1x path. */ + allowDeferredN1xOnboarding?: boolean; /** Print warning-severity host advisories before returning an admitted report. */ presentAdvisories?: boolean; exitProcess?: (code: number) => never; @@ -179,7 +180,7 @@ export function assertOnboardSystemReadiness( allowStorageRemediation: options.allowStorageRemediation === true, allowPortableHostPreparation: options.allowPortableHostPreparation, allowDeferredN1x: - options.allowDeferredN1xManagedVllm ?? hasExplicitDeferredN1xOnboardingIntent(process.env), + options.allowDeferredN1xOnboarding ?? hasExplicitDeferredN1xOnboardingIntent(process.env), }); const advisories = planHostAdvisories(host, { providerOwnsHostReadiness: selectedRuntimeOwnsHostReadiness, @@ -314,7 +315,7 @@ function collectOnboardHostReadiness( wslDockerDesktopGpuProofPassed: runtimeGpu?.wslDockerDesktopGpuProofPassed, resuming: context.resuming, allowStorageRemediation, - allowDeferredN1xManagedVllm: options.allowDeferredN1xManagedVllm, + allowDeferredN1xOnboarding: options.allowDeferredN1xManagedVllm, // The initial host readiness gate already presented warning advisories. presentAdvisories: false, exitProcess: context.exitProcess, @@ -409,7 +410,7 @@ async function collectAdmittedReadinessPair( host.result.sandboxGpuConfig.mode === "0" || options.optedOutGpuPassthrough === true, resuming: context.resuming, allowStorageRemediation: isManagedGatewayReadiness(gateway), - allowDeferredN1xManagedVllm: options.allowDeferredN1xManagedVllm, + allowDeferredN1xOnboarding: options.allowDeferredN1xManagedVllm, presentAdvisories: false, exitProcess, }); @@ -592,7 +593,7 @@ export function runFatalOnboardRuntimePreflight( explicitlyOptedOutGpuPassthrough, resuming: context.resuming, allowStorageRemediation: context.allowStorageRemediation, - allowDeferredN1xManagedVllm: options.allowDeferredN1xManagedVllm, + allowDeferredN1xOnboarding: options.allowDeferredN1xManagedVllm, exitProcess, observedAt, now, diff --git a/src/lib/onboard/inference-providers/provider-selection-keys.ts b/src/lib/onboard/inference-providers/provider-selection-keys.ts new file mode 100644 index 00000000000..b89045146bc --- /dev/null +++ b/src/lib/onboard/inference-providers/provider-selection-keys.ts @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +export const NON_INTERACTIVE_PROVIDER_ALIASES: Readonly> = { + cloud: "build", + nim: "nim-local", + vllm: "vllm", + "open-router": "openrouter", + openrouterai: "openrouter", + anthropiccompatible: "anthropicCompatible", + hermes: "hermesProvider", + "hermes-provider": "hermesProvider", + hermesprovider: "hermesProvider", + nous: "hermesProvider", + "nous-portal": "hermesProvider", +}; + +export const NON_INTERACTIVE_PROVIDER_KEYS: ReadonlySet = new Set([ + "build", + "openrouter", + "openai", + "anthropic", + "anthropicCompatible", + "gemini", + "hermesProvider", + "ollama", + "llama-cpp", + "install-llama-cpp", + "custom", + "nim-local", + "vllm", + "routed", + "install-vllm", + "install-ollama", + "install-windows-ollama", + "start-windows-ollama", +]); + +export function normalizeNonInteractiveProviderKey( + value: string | null | undefined, +): string | null { + const key = String(value ?? "") + .trim() + .toLowerCase(); + if (!key) return null; + const normalized = NON_INTERACTIVE_PROVIDER_ALIASES[key] ?? key; + return NON_INTERACTIVE_PROVIDER_KEYS.has(normalized) ? normalized : null; +} + +/** True only for a recognized provider route that N1x onboarding permits. */ +export function isN1xOnboardingProviderKey(value: string | null | undefined): boolean { + const normalized = normalizeNonInteractiveProviderKey(value); + return normalized !== null && normalized !== "nim-local"; +} diff --git a/src/lib/onboard/machine/handlers/preflight.test.ts b/src/lib/onboard/machine/handlers/preflight.test.ts index de5cf84b4d0..f8b6d438716 100644 --- a/src/lib/onboard/machine/handlers/preflight.test.ts +++ b/src/lib/onboard/machine/handlers/preflight.test.ts @@ -212,16 +212,73 @@ describe("handlePreflightState", () => { 1, expect.anything(), expect.anything(), - expect.objectContaining({ allowDeferredN1xManagedVllm: true }), + expect.objectContaining({ allowDeferredN1xOnboarding: true }), ); expect(assertOnboardHostReadiness).toHaveBeenNthCalledWith( 2, expect.anything(), expect.anything(), - expect.objectContaining({ allowDeferredN1xManagedVllm: true }), + expect.objectContaining({ allowDeferredN1xOnboarding: true }), ); }); + it("reuses a recorded standard provider for Deferred N1x resume (#11041)", async () => { + const session = createSession({ provider: "ollama-local" }); + session.steps.preflight.status = "complete"; + const assertOnboardHostReadiness = vi.fn(); + const harness = createDeps({ assertOnboardHostReadiness }); + + await handlePreflightState({ ...baseOptions(harness.deps, session), resume: true }); + + expect(assertOnboardHostReadiness).toHaveBeenCalledTimes(2); + expect(assertOnboardHostReadiness).toHaveBeenNthCalledWith( + 1, + expect.anything(), + expect.anything(), + expect.objectContaining({ allowDeferredN1xOnboarding: true }), + ); + expect(assertOnboardHostReadiness).toHaveBeenNthCalledWith( + 2, + expect.anything(), + expect.anything(), + expect.objectContaining({ allowDeferredN1xOnboarding: true }), + ); + }); + + it.each([ + ["an excluded NIM provider", "nim", undefined, false], + ["an excluded NIM provider selection", "nim-local", undefined, false], + ["an excluded NIM inference provider", "nvidia-nim", undefined, false], + ["an explicit rebuild denial", "ollama-local", false, false], + ] as const)( + "does not reuse %s for Deferred N1x resume (#11041)", + async (_case, provider, authority, expected) => { + const session = createSession({ provider }); + session.steps.preflight.status = "complete"; + const assertOnboardHostReadiness = vi.fn(); + const harness = createDeps({ assertOnboardHostReadiness }); + + await handlePreflightState({ + ...baseOptions(harness.deps, session), + resume: true, + ...(authority === undefined ? {} : { allowDeferredN1xManagedVllm: authority }), + }); + + expect(assertOnboardHostReadiness).toHaveBeenNthCalledWith( + 1, + expect.anything(), + expect.anything(), + expect.objectContaining({ allowDeferredN1xOnboarding: expected }), + ); + expect(assertOnboardHostReadiness).toHaveBeenNthCalledWith( + 2, + expect.anything(), + expect.anything(), + expect.objectContaining({ allowDeferredN1xOnboarding: expected }), + ); + }, + ); + it("rejects changed gateway ownership before cached resume probe effects (#7411)", async () => { const session = createSession(); session.steps.preflight.status = "complete"; diff --git a/src/lib/onboard/machine/handlers/preflight.ts b/src/lib/onboard/machine/handlers/preflight.ts index 2b96bd96171..e022de751e4 100644 --- a/src/lib/onboard/machine/handlers/preflight.ts +++ b/src/lib/onboard/machine/handlers/preflight.ts @@ -7,6 +7,8 @@ import { advanceTo, type OnboardStateTransitionResult } from "../result"; export type PreflightSandboxGpuFlag = "enable" | "disable" | null; +const N1X_EXCLUDED_RECORDED_PROVIDERS = new Set(["nim", "nim-local", "nvidia-nim"]); + export interface PreflightSandboxGpuOverrides { flag: PreflightSandboxGpuFlag; device: string | null; @@ -56,7 +58,7 @@ export interface PreflightStateOptions< observedAt?: string; now?: () => Date; wslDockerDesktopGpuProofPassed?: boolean; - allowDeferredN1xManagedVllm?: boolean; + allowDeferredN1xOnboarding?: boolean; resuming: true; presentAdvisories?: boolean; }, @@ -151,6 +153,14 @@ export async function handlePreflightState< : { flag: null, device: null }; const effectiveSandboxGpuFlag = explicitSandboxGpuFlag ?? resumedSandboxGpuOverrides.flag; const effectiveSandboxGpuDevice = sandboxGpuDevice ?? resumedSandboxGpuOverrides.device; + const recordedProviderAllowsDeferredN1x = + typeof session?.provider === "string" && + session.provider.trim() !== "" && + !N1X_EXCLUDED_RECORDED_PROVIDERS.has(session.provider); + // An explicit false is authoritative for rebuilds. Ordinary resume may use + // the provider that this owner-only session already validated and recorded. + const allowDeferredN1xOnboarding = + allowDeferredN1xManagedVllm ?? recordedProviderAllowsDeferredN1x; let gpu: Gpu; if (resumePreflight) { @@ -177,7 +187,7 @@ export async function handlePreflightState< explicitlyOptedOutGpuPassthrough: resumeSandboxGpuConfig.mode === "0", observedAt: hostObservedAt, now, - allowDeferredN1xManagedVllm, + allowDeferredN1xOnboarding, resuming: true, }); // A full detector can run the bounded ARM64 WSL Docker GPU proof. Keep it @@ -200,7 +210,7 @@ export async function handlePreflightState< observedAt: hostObservedAt, now, ...(wslDockerDesktopGpuProofPassed === undefined ? {} : { wslDockerDesktopGpuProofPassed }), - allowDeferredN1xManagedVllm, + allowDeferredN1xOnboarding, resuming: true, presentAdvisories: false, }); diff --git a/src/lib/onboard/providers.ts b/src/lib/onboard/providers.ts index a60d00bc1b2..9be9b5c093b 100644 --- a/src/lib/onboard/providers.ts +++ b/src/lib/onboard/providers.ts @@ -32,6 +32,11 @@ const { MESSAGING_CREDENTIAL_PROVIDER_TYPE, } = require("../messaging/provider-profile"); const { ensureWebSearchProviderProfiles } = require("./brave-provider-profile"); +const { + NON_INTERACTIVE_PROVIDER_ALIASES, + NON_INTERACTIVE_PROVIDER_KEYS, + normalizeNonInteractiveProviderKey, +} = require("./inference-providers/provider-selection-keys"); const MESSAGING_PROVIDER_BINDING_CONFLICT = "NEMOCLAW_MESSAGING_PROVIDER_BINDING_CONFLICT"; const MESSAGING_PROVIDER_MUTATION_FAILURE = "NEMOCLAW_MESSAGING_PROVIDER_MUTATION_FAILURE"; @@ -164,39 +169,6 @@ const PROVIDER_MODEL_ENV = "NEMOCLAW_PROVIDER_MODEL"; // provider/namespace/model convention. This endpoint is staged as a custom // OpenAI-compatible provider, not as the public build.nvidia.com provider. const HOSTED_INFERENCE_MODEL = "nvidia/nvidia/nemotron-3-ultra"; -const NON_INTERACTIVE_PROVIDER_ALIASES = { - cloud: "build", - nim: "nim-local", - vllm: "vllm", - "open-router": "openrouter", - openrouterai: "openrouter", - anthropiccompatible: "anthropicCompatible", - hermes: "hermesProvider", - "hermes-provider": "hermesProvider", - hermesprovider: "hermesProvider", - nous: "hermesProvider", - "nous-portal": "hermesProvider", -}; -const NON_INTERACTIVE_PROVIDER_KEYS = new Set([ - "build", - "openrouter", - "openai", - "anthropic", - "anthropicCompatible", - "gemini", - "hermesProvider", - "ollama", - "llama-cpp", - "install-llama-cpp", - "custom", - "nim-local", - "vllm", - "routed", - "install-vllm", - "install-ollama", - "install-windows-ollama", - "start-windows-ollama", -]); const NON_INTERACTIVE_PROVIDER_VALID_VALUES = "Valid values: build, openrouter, openai, anthropic, anthropicCompatible, gemini, hermes-provider, ollama, llama-cpp, install-llama-cpp, custom, nim-local, vllm, routed, install-vllm, install-ollama, install-windows-ollama, start-windows-ollama"; const PROVIDER_KEY_ROUTE_VALUES = new Set( @@ -384,8 +356,8 @@ function getNonInteractiveProvider(allowHostedInferenceStaging = true) { if (allowHostedInferenceStaging) stageHostedInferenceSourceSecretEnv(); const providerKey = (process.env.NEMOCLAW_PROVIDER || "").trim().toLowerCase(); if (!providerKey) return null; - const normalized = NON_INTERACTIVE_PROVIDER_ALIASES[providerKey] || providerKey; - if (!NON_INTERACTIVE_PROVIDER_KEYS.has(normalized)) { + const normalized = normalizeNonInteractiveProviderKey(providerKey); + if (!normalized) { console.error(` Unsupported NEMOCLAW_PROVIDER: ${providerKey}`); console.error(` ${NON_INTERACTIVE_PROVIDER_VALID_VALUES}`); process.exit(1); diff --git a/src/lib/readiness/onboard-admission.test.ts b/src/lib/readiness/onboard-admission.test.ts index 398f1cb6f90..6d789e874e0 100644 --- a/src/lib/readiness/onboard-admission.test.ts +++ b/src/lib/readiness/onboard-admission.test.ts @@ -105,7 +105,11 @@ describe("onboarding readiness admission (#7411)", () => { it.each([ ["preview decline", { NEMOCLAW_NO_EXPRESS: "1" }, true], ["explicit non-Express provider", { NEMOCLAW_PROVIDER: "ollama" }, true], + ["normalized provider alias", { NEMOCLAW_PROVIDER: " Open-Router " }, true], ["explicit managed provider", { NEMOCLAW_PROVIDER: "install-vllm" }, true], + ["unknown provider", { NEMOCLAW_PROVIDER: "unknown-provider" }, false], + ["N1x-excluded provider", { NEMOCLAW_PROVIDER: "nim-local" }, false], + ["N1x-excluded provider alias", { NEMOCLAW_PROVIDER: "nim" }, false], ["blank provider", { NEMOCLAW_PROVIDER: " " }, false], ["disabled Express override", { NEMOCLAW_NO_EXPRESS: "0" }, false], ["no intent", {}, false], diff --git a/src/lib/readiness/onboard-admission.ts b/src/lib/readiness/onboard-admission.ts index 9615e018686..cfb34e9dcea 100644 --- a/src/lib/readiness/onboard-admission.ts +++ b/src/lib/readiness/onboard-admission.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { ReadinessCapability, ReadinessFinding, SystemReadinessReport } from "./types"; +import { isN1xOnboardingProviderKey } from "../onboard/inference-providers/provider-selection-keys"; export const ONBOARD_READINESS_ADMISSION_REASON_IDS = { blockingFindings: "onboard.readiness.blocking_findings", @@ -14,7 +15,7 @@ export type OnboardReadinessAdmissionReasonId = export function hasExplicitDeferredN1xOnboardingIntent( env: Readonly>, ): boolean { - return env.NEMOCLAW_NO_EXPRESS === "1" || String(env.NEMOCLAW_PROVIDER ?? "").trim() !== ""; + return env.NEMOCLAW_NO_EXPRESS === "1" || isN1xOnboardingProviderKey(env.NEMOCLAW_PROVIDER); } export const ONBOARD_READINESS_FINDING_IDS = { diff --git a/test/helpers/installer-express-prompt-pty-harness.ts b/test/helpers/installer-express-prompt-pty-harness.ts index e1d6593495a..500a0020b02 100644 --- a/test/helpers/installer-express-prompt-pty-harness.ts +++ b/test/helpers/installer-express-prompt-pty-harness.ts @@ -25,7 +25,7 @@ export function runExpressPromptWithTty( stdinMode: "pipe" | "tty", platform = "DGX Spark", extraEnv: Record = {}, - entrypoint: "prompt" | "accepted-station-main" = "prompt", + entrypoint: "prompt" | "accepted-station-main" | "n1x-standard-main" = "prompt", entrypointArgs: string[] = [], harnessFixture?: InstallerExpressPtyFixture, ) { @@ -51,7 +51,56 @@ harness_mode = sys.argv[6] timeout_seconds = float(sys.argv[7]) pid_file = sys.argv[8] entrypoint_args = sys.argv[9:] -if entrypoint == "accepted-station-main": +if entrypoint == "n1x-standard-main": + script = r''' +source "$INSTALLER_UNDER_TEST" >/dev/null +detect_express_platform() { printf "N1x"; } +classify_dgx_station_release() { printf "generic-ubuntu"; } +NON_INTERACTIVE="\${NON_INTERACTIVE:-}" +NEMOCLAW_PROVIDER="\${NEMOCLAW_PROVIDER:-}" +NEMOCLAW_NO_EXPRESS="\${NEMOCLAW_NO_EXPRESS:-}" +maybe_offer_express_install + +NEMOCLAW_SOURCE_ROOT="$PWD/source" +mkdir -p "$NEMOCLAW_SOURCE_ROOT/dist/lib/onboard/experimental" \ + "$NEMOCLAW_SOURCE_ROOT/dist/lib/readiness" +for module in \ + "$NEMOCLAW_SOURCE_ROOT/dist/lib/onboard/preflight.js" \ + "$NEMOCLAW_SOURCE_ROOT/dist/lib/onboard/gateway-management.js" \ + "$NEMOCLAW_SOURCE_ROOT/dist/lib/onboard/experimental/portable-profile.js" \ + "$NEMOCLAW_SOURCE_ROOT/dist/lib/onboard/docker-driver-gateway-env.js" \ + "$NEMOCLAW_SOURCE_ROOT/dist/lib/readiness/host.js" \ + "$NEMOCLAW_SOURCE_ROOT/dist/lib/readiness/onboard-admission.js"; do + : >"$module" +done +repair_installer_nvidia_cdi_spec() { :; } +node() { + if [ "\${1:-}" = "-e" ]; then + printf '__INFO__\nHOST_PREFLIGHT_NO_EXPRESS=%s PROVIDER=%s QUALIFIED_N1X=1\n' \ + "\${NEMOCLAW_NO_EXPRESS:-}" "\${NEMOCLAW_PROVIDER:-}" + if [ "\${NEMOCLAW_NO_EXPRESS:-}" = "1" ] || [ "\${NEMOCLAW_PROVIDER:-}" = "ollama" ]; then + return 0 + fi + printf '__ACTIONS__\nAdmission finding IDs: host.platform.n1x_validation_pending\n' + return 10 + fi + command node "$@" +} +run_installer_host_preflight || exit $? + +fake_cli="$PWD/nemoclaw-under-test" +printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'printf "ONBOARD NO_EXPRESS=%s PROVIDER=%s ARGS=%s\\n" "\${NEMOCLAW_NO_EXPRESS:-}" "\${NEMOCLAW_PROVIDER:-}" "$*"' \ + >"$fake_cli" +chmod +x "$fake_cli" +_CLI_BIN="nemoclaw" +_CLI_PATH="$fake_cli" +show_usage_notice() { :; } +nemoclaw_state_dir() { printf '%s' "$PWD/state"; } +run_onboard +''' +elif entrypoint == "accepted-station-main": script = r''' source "$INSTALLER_UNDER_TEST" >/dev/null detect_express_platform() { printf "$EXPRESS_PLATFORM"; } diff --git a/test/helpers/installer-readiness-stubs.ts b/test/helpers/installer-readiness-stubs.ts index 317a9075944..2a394c79a60 100644 --- a/test/helpers/installer-readiness-stubs.ts +++ b/test/helpers/installer-readiness-stubs.ts @@ -7,6 +7,28 @@ import os from "node:os"; import path from "node:path"; import { INSTALLER_PAYLOAD, TEST_SYSTEM_PATH, writeExecutable } from "./installer-sourced-env"; +export const DEFERRED_N1X_INTENT_STUB = ` +const deferredN1xProviderAliases = new Map([ + ["cloud", "build"], ["nim", "nim-local"], ["open-router", "openrouter"], + ["openrouterai", "openrouter"], ["anthropiccompatible", "anthropicCompatible"], + ["hermes", "hermesProvider"], ["hermes-provider", "hermesProvider"], + ["hermesprovider", "hermesProvider"], ["nous", "hermesProvider"], + ["nous-portal", "hermesProvider"], +]); +const deferredN1xProviderKeys = new Set([ + "build", "openrouter", "openai", "anthropic", "anthropicCompatible", "gemini", + "hermesProvider", "ollama", "llama-cpp", "install-llama-cpp", "custom", "nim-local", + "vllm", "routed", "install-vllm", "install-ollama", "install-windows-ollama", + "start-windows-ollama", +]); +exports.hasExplicitDeferredN1xOnboardingIntent = (env) => { + if (env.NEMOCLAW_NO_EXPRESS === "1") return true; + const raw = String(env.NEMOCLAW_PROVIDER || "").trim().toLowerCase(); + const normalized = deferredN1xProviderAliases.get(raw) || raw; + return deferredN1xProviderKeys.has(normalized) && normalized !== "nim-local"; +}; +`; + /** Fake node that reports v22.19.0. */ export function writeNodeStub(fakeBin: string): void { writeExecutable( @@ -84,8 +106,7 @@ export function writeInstallerReadinessModuleStubs(readinessDir: string): void { waivedFindingIds: [], }; }; -exports.hasExplicitDeferredN1xOnboardingIntent = (env) => - env.NEMOCLAW_NO_EXPRESS === "1" || String(env.NEMOCLAW_PROVIDER || "").trim() !== ""; +${DEFERRED_N1X_INTENT_STUB} `, ); fs.writeFileSync( diff --git a/test/install/install-express-n1x.test.ts b/test/install/install-express-n1x.test.ts index f4962e7a64e..f22f74079e0 100644 --- a/test/install/install-express-n1x.test.ts +++ b/test/install/install-express-n1x.test.ts @@ -40,6 +40,15 @@ describe("installer N1x Express preview", () => { expect(output).toMatch(/RESULT .*NO_EXPRESS=1/); }); + it("carries a declined preview through host admission into the onboarding CLI (#11041)", () => { + const result = runExpressPromptWithTty("n\n", "pipe", "N1x", {}, "n1x-standard-main"); + const output = `${result.stdout}${result.stderr}`; + + expect(result.status, output).toBe(0); + expect(output).toContain("HOST_PREFLIGHT_NO_EXPRESS=1 PROVIDER= QUALIFIED_N1X=1"); + expect(output).toContain("ONBOARD NO_EXPRESS=1 PROVIDER= ARGS=onboard"); + }); + it("continues with ordinary onboarding when NEMOCLAW_NO_EXPRESS is set (#11041)", () => { const result = runExpressPromptWithTty("", "pipe", "N1x", { NEMOCLAW_NO_EXPRESS: "1", @@ -62,6 +71,23 @@ describe("installer N1x Express preview", () => { expect(output).toMatch(/RESULT .*PROVIDER=ollama/); }); + it("carries an explicit Ollama choice through host admission into the onboarding CLI (#11041)", () => { + const result = runExpressPromptWithTty( + "", + "pipe", + "N1x", + { + NEMOCLAW_PROVIDER: "ollama", + }, + "n1x-standard-main", + ); + const output = `${result.stdout}${result.stderr}`; + + expect(result.status, output).toBe(0); + expect(output).toContain("HOST_PREFLIGHT_NO_EXPRESS= PROVIDER=ollama QUALIFIED_N1X=1"); + expect(output).toContain("ONBOARD NO_EXPRESS= PROVIDER=ollama ARGS=onboard"); + }); + it("allows the N1x prompt bypass with explicit managed-vLLM intent (#8574)", () => { const result = runExpressPromptWithTty("", "pipe", "N1x", { NEMOCLAW_NO_EXPRESS: "1", diff --git a/test/package-contract/installer-host-preflight.test.ts b/test/package-contract/installer-host-preflight.test.ts index 838423fc170..1b554c2602c 100644 --- a/test/package-contract/installer-host-preflight.test.ts +++ b/test/package-contract/installer-host-preflight.test.ts @@ -11,6 +11,7 @@ import { TEST_SYSTEM_PATH, writeExecutable, } from "../helpers/installer-sourced-env"; +import { DEFERRED_N1X_INTENT_STUB } from "../helpers/installer-readiness-stubs"; function writeNodeStub(fakeBin: string) { writeExecutable( @@ -196,8 +197,7 @@ exports.evaluateOnboardReadinessAdmission = (report, options) => { ? { admitted: true, waivedFindingIds: [] } : { admitted: false, reasonIds: [], findingIds, capabilityIds, waivedFindingIds: [] }; }; -exports.hasExplicitDeferredN1xOnboardingIntent = (env) => - env.NEMOCLAW_NO_EXPRESS === "1" || String(env.NEMOCLAW_PROVIDER || "").trim() !== ""; +${DEFERRED_N1X_INTENT_STUB} `, ); writeNodeStub(fakeBin); @@ -275,6 +275,24 @@ describe("installer host preflight package contract", () => { expect(output).toContain("host.platform.n1x_validation_pending"); }); + it.each(["unknown-provider", "nim", "nim-local"])( + "keeps Deferred N1x blocked for provider %s (#11041)", + (provider) => { + const { output, result } = runInstallerHostAdmissionTest( + { + runtime: "docker", + isN1x: true, + additionalFindingIds: ["host.platform.n1x_validation_pending"], + }, + undefined, + { provider }, + ); + + expect(result.status).toBe(1); + expect(output).toContain("host.platform.n1x_validation_pending"); + }, + ); + it("continues to onboarding when managed storage remediation is available", () => { const { output, result } = runInstallerHostAdmissionTest({ runtime: "docker", From cb8844e91e66296ba109947d4325a2e16e2e3880 Mon Sep 17 00:00:00 2001 From: San Dang Date: Fri, 4 Sep 2026 17:15:26 +0700 Subject: [PATCH 03/19] fix(onboard): close N1x review gaps Signed-off-by: San Dang --- ci/platform-matrix.json | 2 +- docs/inference/choose-inference-provider.mdx | 2 +- docs/inference/set-up-vllm.mdx | 5 +- docs/reference/platform-support.mdx | 2 +- docs/reference/system-readiness.mdx | 4 +- .../onboard/fatal-runtime-preflight.test.ts | 15 +++ .../provider-selection-keys.ts | 38 ++++++ .../machine/handlers/preflight.test.ts | 14 +-- src/lib/onboard/machine/handlers/preflight.ts | 10 +- src/lib/onboard/provider-recovery.ts | 16 +-- src/lib/readiness/onboard-admission.test.ts | 10 ++ src/lib/readiness/onboard-admission.ts | 4 +- test/helpers/installer-readiness-stubs.ts | 24 +--- .../installer-host-preflight.test.ts | 114 +++++++++++++++--- 14 files changed, 190 insertions(+), 70 deletions(-) diff --git a/ci/platform-matrix.json b/ci/platform-matrix.json index 4500c85eb87..76ad45fefa5 100644 --- a/ci/platform-matrix.json +++ b/ci/platform-matrix.json @@ -74,7 +74,7 @@ "status": "deferred", "ci_tested": false, "prerequisites_notes": "N1x Express remains Deferred until a physical NemoClaw Express E2E test passes. Host identity requires Linux `arm64`, a trusted `/etc/fastos-release` marker with `NAME=\"N1x FASTOS\"`, and NVIDIA vendor ID `10de` on a display-class PCI device. CUDA and Container Device Interface (CDI) readiness are still required.", - "notes": "The accepted Deferred N1x Express preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. It does not activate DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Host identity requires Linux `arm64`, a regular `/etc/fastos-release` file of 1 through 4,096 bytes that is owned by UID 0 and GID 0, is not a symbolic link, is not group- or world-writable, and contains exactly one `NAME=\"N1x FASTOS\"` line. It also requires NVIDIA vendor ID `10de` on a display-class PCI device. Generic DMI values are supplemental and FastOS version is not pinned. Physical CUDA and CDI checks passed on one N1x host, but full NemoClaw Express E2E validation and dedicated CI coverage are pending. `host.platform.supported` remains absent. After identity qualifies, accepting Express, declining or disabling Express, or setting `NEMOCLAW_PROVIDER` can waive only the pending-validation finding. Existing provider requirements still apply, and this waiver does not promote N1x. Do not claim N1x as supported until validation passes and this row is promoted." + "notes": "The accepted Deferred N1x Express preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. It does not activate DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Host identity requires Linux `arm64`, a regular `/etc/fastos-release` file of 1 through 4,096 bytes that is owned by UID 0 and GID 0, is not a symbolic link, is not group- or world-writable, and contains exactly one `NAME=\"N1x FASTOS\"` line. It also requires NVIDIA vendor ID `10de` on a display-class PCI device. Generic DMI values are supplemental and FastOS version is not pinned. Physical CUDA and CDI checks passed on one N1x host, but full NemoClaw Express E2E validation and dedicated CI coverage are pending. `host.platform.supported` remains absent. After identity qualifies, accepting Express can waive only the pending-validation finding. Declining or disabling Express can apply the same waiver when no provider is preselected or the explicit provider is recognized and is not Local NVIDIA NIM. An invalid or Local NVIDIA NIM provider remains blocked. Existing provider requirements still apply, and this waiver does not promote N1x. Do not claim N1x as supported until validation passes and this row is promoted." }, { "name": "NVIDIA RTX (consumer and Pro workstation GPUs)", diff --git a/docs/inference/choose-inference-provider.mdx b/docs/inference/choose-inference-provider.mdx index 6fc7cec405e..d1817dbbbff 100644 --- a/docs/inference/choose-inference-provider.mdx +++ b/docs/inference/choose-inference-provider.mdx @@ -13,7 +13,7 @@ Choose a provider based on where the model runs, which API it exposes, and which NemoClaw validates the provider and model before it creates the sandbox. -On native N1x FASTOS, accept the Deferred managed-vLLM preview or enter standard onboarding by declining or disabling Express or setting `NEMOCLAW_PROVIDER`. +On native N1x FASTOS, accept the Deferred managed-vLLM preview or enter standard onboarding by declining Express, disabling Express without an excluded provider, or setting a recognized `NEMOCLAW_PROVIDER` value other than Local NVIDIA NIM. N1x remains Deferred, each standard provider keeps its existing requirements, and local NVIDIA NIM remains unavailable. On a qualifying Windows WSL N1x host, Windows Express uses managed llama.cpp after Docker Desktop GPU passthrough succeeds. Other Windows WSL hosts retain Windows-host or WSL-local Ollama. diff --git a/docs/inference/set-up-vllm.mdx b/docs/inference/set-up-vllm.mdx index 59c6203c0a6..b7cc66d8cb1 100644 --- a/docs/inference/set-up-vllm.mdx +++ b/docs/inference/set-up-vllm.mdx @@ -473,7 +473,10 @@ Generic DMI values, such as `SKU 1` and `JMJWOA-Generic`, do not establish N1x i N1x Express does not activate DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Accepting the preview prompt sets explicit managed-vLLM intent for the narrow pending-validation readiness exception. -Declining the prompt or setting only `NEMOCLAW_NO_EXPRESS=1` stops the installer before onboarding; neither path opens interactive provider selection. +Declining the prompt continues with ordinary interactive onboarding. +Setting `NEMOCLAW_NO_EXPRESS=1` also enters ordinary onboarding when no provider is preselected or the explicit provider is recognized and is not Local NVIDIA NIM. +An invalid or Local NVIDIA NIM provider remains blocked. +Set a recognized provider other than Local NVIDIA NIM to enter ordinary onboarding directly. Set `NEMOCLAW_PROVIDER=install-vllm` when you need to bypass the prompt with the same explicit preview intent. On a host that still meets the N1x identity checks, `rebuild` reuses the recorded route as explicit preview intent only when all of these conditions hold: diff --git a/docs/reference/platform-support.mdx b/docs/reference/platform-support.mdx index fad650305c4..94fa5ee0e28 100644 --- a/docs/reference/platform-support.mdx +++ b/docs/reference/platform-support.mdx @@ -82,7 +82,7 @@ For install requirements and the shorter setup-oriented platform view, refer to | DGX OS (Station) | Docker | Tested with limitations | P1 | No | The PRD marks this platform as P1. Physical validation on one DGX Station GB300 covers generic Ubuntu 24.04 ARM64, stock DGX OS `7.5.0`, the April 2026 NVIDIA Colossus BaseOS profile, and the June 2026 NVIDIA AI Developer Tools profile. A physical no-OTA DGX OS `7.6.0` host provided the release and hardware profile used for its stable workstation-family classifier and passed read-only eligibility and runtime-command preflight. Full Station Express end-to-end qualification for the accepted no-OTA DGX OS `7.6.x` profile is pending. The profile remains subject to the same physical GB300, driver, ECC, Docker, CDI, and container GPU validation. Clean-host end-to-end validation passed on generic Ubuntu and Colossus BaseOS; stock DGX OS and AI Developer Tools completed Station Express validation. The DGX OS `7.5.0` run used released OpenShell `0.0.85`, local Nemotron Ultra serving, sandbox `cuInit(0)`, and a Hermes write/read file-tool task. A dual-Station configuration has not been validated, and dedicated CI coverage is not available. Direct-GPU policies expose only the exact read-only BDF directory for each discovered display-class PCI device with NVIDIA vendor ID (`0x10de`) and GB300 device ID (`0x31c2` or `0x31c3`) plus required existing topology and module paths; they do not expose `/sys`, the PCI parent subtree, or sysfs write access. During physical validation, reads of `/sys/fs/cgroup/cgroup.controllers` and `/sys/class/net/lo/address` remained denied. For canonical hardware qualification, image requirements, preparation, repair limits, reboot handoff, and the explicit temporary metadata override, see [Prepare DGX Station to Install NemoClaw](../get-started/additional-setup/dgx-station-preparation). | | Linux | Docker | Tested | P0 | Yes | Primary tested path. Ubuntu 24.04 has host-level onboarding validation. A digest-pinned Ubuntu 26.04 userspace lane builds the CLI and runs preflight, installer, and platform contracts on eligible main pushes; Docker-host, AppArmor, Landlock, and live onboarding validation on 26.04 remain pending. Other distros (Ubuntu 22.04, Fedora, Rocky, Alma, NixOS, Arch) may work but are not validated. | | macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | P0 | Yes | Start the container runtime (Colima or Docker Desktop) before installing; NemoClaw verifies the pinned official OpenShell formula and grants formula-scoped trust only around each Homebrew install, inspection, start, or stop operation, requires a legacy or changed formula to be repaired by rerunning the pinned installer, uses the standalone gateway only when Homebrew is absent or both the staged formula and installed keg are absent, requires Homebrew Colima users to install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work, and recommends Xcode Command Line Tools (`xcode-select --install`) for Node native modules. | -| N1x FASTOS | Docker | Deferred | Unset | No | The accepted Deferred N1x Express preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. It does not activate DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Host identity requires Linux `arm64`, a regular `/etc/fastos-release` file of 1 through 4,096 bytes that is owned by UID 0 and GID 0, is not a symbolic link, is not group- or world-writable, and contains exactly one `NAME="N1x FASTOS"` line. It also requires NVIDIA vendor ID `10de` on a display-class PCI device. Generic DMI values are supplemental and FastOS version is not pinned. Physical CUDA and CDI checks passed on one N1x host, but full NemoClaw Express E2E validation and dedicated CI coverage are pending. `host.platform.supported` remains absent. After identity qualifies, accepting Express, declining or disabling Express, or setting `NEMOCLAW_PROVIDER` can waive only the pending-validation finding. Existing provider requirements still apply, and this waiver does not promote N1x. Do not claim N1x as supported until validation passes and this row is promoted. | +| N1x FASTOS | Docker | Deferred | Unset | No | The accepted Deferred N1x Express preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. It does not activate DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Host identity requires Linux `arm64`, a regular `/etc/fastos-release` file of 1 through 4,096 bytes that is owned by UID 0 and GID 0, is not a symbolic link, is not group- or world-writable, and contains exactly one `NAME="N1x FASTOS"` line. It also requires NVIDIA vendor ID `10de` on a display-class PCI device. Generic DMI values are supplemental and FastOS version is not pinned. Physical CUDA and CDI checks passed on one N1x host, but full NemoClaw Express E2E validation and dedicated CI coverage are pending. `host.platform.supported` remains absent. After identity qualifies, accepting Express can waive only the pending-validation finding. Declining or disabling Express can apply the same waiver when no provider is preselected or the explicit provider is recognized and is not Local NVIDIA NIM. An invalid or Local NVIDIA NIM provider remains blocked. Existing provider requirements still apply, and this waiver does not promote N1x. Do not claim N1x as supported until validation passes and this row is promoted. | | NVIDIA RTX (consumer and Pro workstation GPUs) | Docker | Deferred | P1 | No | The PRD marks this platform as P1. Covers RTX consumer cards and RTX Pro workstation cards on Linux hosts that meet the generic-Linux-GPU requirements (NVIDIA Container Toolkit + CDI present). The provider menu emits managed vLLM behind `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm` for this host class today; the end-to-end onboard path on this hardware is not yet validated in CI. | | Windows WSL2 | Docker Desktop (WSL backend) | Tested with limitations | P1 | No | Requires WSL2 with Docker Desktop backend. Qualifying Windows WSL N1x hosts can use the explicit Experimental managed llama.cpp Qwen 3.6 recipe after local-default Docker authority, N1x product identity, ARM64, 48,000 MiB Docker and GPU memory, driver, NVIDIA integration, and Docker Desktop GPU-passthrough checks pass. Other Windows WSL hosts retain Windows-host or WSL-local Ollama. Dedicated N1x WSL CI and physical end-to-end validation are pending. | {/* platform-matrix-full:end */} diff --git a/docs/reference/system-readiness.mdx b/docs/reference/system-readiness.mdx index 64a36befba2..94c9be4238f 100644 --- a/docs/reference/system-readiness.mdx +++ b/docs/reference/system-readiness.mdx @@ -177,7 +177,9 @@ Generic DMI values do not satisfy either requirement. A matching identity produces a `qualified` `host.platform.n1x` qualification, but `host.platform.supported` remains `absent` while N1x is Deferred. The blocking `host.platform.n1x_validation_pending` finding records the remaining physical NemoClaw Express E2E gate. -Accepting Express, declining or disabling Express, or setting `NEMOCLAW_PROVIDER` lets onboarding waive only that finding after `host.platform.n1x` is `present`. +After `host.platform.n1x` is `present`, accepting Express lets onboarding waive only that finding. +Declining or disabling Express can apply the same waiver when no provider is preselected or the explicit provider is recognized and is not Local NVIDIA NIM. +An invalid or Local NVIDIA NIM provider remains blocked. The read-only `host probe` report remains `incompatible` and does not apply that onboarding exception. Invalid marker metadata, invalid or ambiguous contents, the wrong PCI identity, the wrong architecture, or an unavailable NVIDIA GPU produces an `unqualified` qualification and the blocking `host.platform.n1x_unqualified` finding. diff --git a/src/lib/onboard/fatal-runtime-preflight.test.ts b/src/lib/onboard/fatal-runtime-preflight.test.ts index cbc5bfcb8eb..72025f61b19 100644 --- a/src/lib/onboard/fatal-runtime-preflight.test.ts +++ b/src/lib/onboard/fatal-runtime-preflight.test.ts @@ -254,6 +254,21 @@ describe("report-backed runtime readiness (#7411)", () => { exitProcess: exit as never, }), ).toThrow("exit"); + vi.stubEnv("NEMOCLAW_NO_EXPRESS", "1"); + vi.stubEnv("NEMOCLAW_PROVIDER", "nim"); + expect(() => + assertOnboardSystemReadiness(readiness, hostWithRuntime("docker"), { + explicitlyOptedOutGpuPassthrough: false, + exitProcess: exit as never, + }), + ).toThrow("exit"); + vi.stubEnv("NEMOCLAW_PROVIDER", "nim-local"); + expect(() => + assertOnboardSystemReadiness(readiness, hostWithRuntime("docker"), { + explicitlyOptedOutGpuPassthrough: false, + exitProcess: exit as never, + }), + ).toThrow("exit"); }); it("rejects ambiguous gateway ownership before the caller can run effects", () => { diff --git a/src/lib/onboard/inference-providers/provider-selection-keys.ts b/src/lib/onboard/inference-providers/provider-selection-keys.ts index b89045146bc..25547953128 100644 --- a/src/lib/onboard/inference-providers/provider-selection-keys.ts +++ b/src/lib/onboard/inference-providers/provider-selection-keys.ts @@ -36,6 +36,22 @@ export const NON_INTERACTIVE_PROVIDER_KEYS: ReadonlySet = new Set([ "start-windows-ollama", ]); +const PERSISTED_PROVIDER_SELECTION_KEYS: Readonly> = { + "nvidia-router": "routed", + "ollama-local": "ollama", + "nvidia-prod": "build", + // This legacy name identifies NVIDIA Endpoints, not Local NVIDIA NIM. + "nvidia-nim": "build", + "openai-api": "openai", + "openrouter-api": "openrouter", + "anthropic-prod": "anthropic", + "compatible-anthropic-endpoint": "anthropicCompatible", + "gemini-api": "gemini", + "compatible-endpoint": "custom", + "llama-cpp-local": "llama-cpp", + "hermes-provider": "hermesProvider", +}; + export function normalizeNonInteractiveProviderKey( value: string | null | undefined, ): string | null { @@ -52,3 +68,25 @@ export function isN1xOnboardingProviderKey(value: string | null | undefined): bo const normalized = normalizeNonInteractiveProviderKey(value); return normalized !== null && normalized !== "nim-local"; } + +/** Resolve the provider representation persisted in an onboarding session. */ +export function persistedProviderNameToSelectionKey( + value: string | null | undefined, + options: { hasNimContainer?: boolean } = {}, +): string | null { + const name = String(value ?? "").trim(); + if (!name) return null; + // Local NIM and standalone vLLM both persist as vllm-local. Only the + // owner-only NIM container record distinguishes the excluded NIM route. + if (name === "vllm-local") return options.hasNimContainer ? "nim-local" : "vllm"; + return PERSISTED_PROVIDER_SELECTION_KEYS[name] ?? normalizeNonInteractiveProviderKey(name); +} + +/** True only for a recognized persisted provider route that N1x permits. */ +export function isN1xOnboardingRecordedProvider( + value: string | null | undefined, + options: { hasNimContainer?: boolean } = {}, +): boolean { + const selection = persistedProviderNameToSelectionKey(value, options); + return selection !== null && selection !== "nim-local"; +} diff --git a/src/lib/onboard/machine/handlers/preflight.test.ts b/src/lib/onboard/machine/handlers/preflight.test.ts index f8b6d438716..a768ed0929a 100644 --- a/src/lib/onboard/machine/handlers/preflight.test.ts +++ b/src/lib/onboard/machine/handlers/preflight.test.ts @@ -246,14 +246,14 @@ describe("handlePreflightState", () => { }); it.each([ - ["an excluded NIM provider", "nim", undefined, false], - ["an excluded NIM provider selection", "nim-local", undefined, false], - ["an excluded NIM inference provider", "nvidia-nim", undefined, false], - ["an explicit rebuild denial", "ollama-local", false, false], + ["persisted Local NIM", "vllm-local", "nemoclaw-nim", undefined, false], + ["an unknown persisted provider", "obsolete-provider", null, undefined, false], + ["legacy NVIDIA Endpoints", "nvidia-nim", null, undefined, true], + ["an explicit rebuild denial", "ollama-local", null, false, false], ] as const)( - "does not reuse %s for Deferred N1x resume (#11041)", - async (_case, provider, authority, expected) => { - const session = createSession({ provider }); + "classifies %s for Deferred N1x resume (#11041)", + async (_case, provider, nimContainer, authority, expected) => { + const session = createSession({ provider, nimContainer }); session.steps.preflight.status = "complete"; const assertOnboardHostReadiness = vi.fn(); const harness = createDeps({ assertOnboardHostReadiness }); diff --git a/src/lib/onboard/machine/handlers/preflight.ts b/src/lib/onboard/machine/handlers/preflight.ts index e022de751e4..51669940d54 100644 --- a/src/lib/onboard/machine/handlers/preflight.ts +++ b/src/lib/onboard/machine/handlers/preflight.ts @@ -2,13 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import type { Session } from "../../../state/onboard-session"; +import { isN1xOnboardingRecordedProvider } from "../../inference-providers/provider-selection-keys"; import { withPreflightTrace } from "../../tracing"; import { advanceTo, type OnboardStateTransitionResult } from "../result"; export type PreflightSandboxGpuFlag = "enable" | "disable" | null; -const N1X_EXCLUDED_RECORDED_PROVIDERS = new Set(["nim", "nim-local", "nvidia-nim"]); - export interface PreflightSandboxGpuOverrides { flag: PreflightSandboxGpuFlag; device: string | null; @@ -153,10 +152,9 @@ export async function handlePreflightState< : { flag: null, device: null }; const effectiveSandboxGpuFlag = explicitSandboxGpuFlag ?? resumedSandboxGpuOverrides.flag; const effectiveSandboxGpuDevice = sandboxGpuDevice ?? resumedSandboxGpuOverrides.device; - const recordedProviderAllowsDeferredN1x = - typeof session?.provider === "string" && - session.provider.trim() !== "" && - !N1X_EXCLUDED_RECORDED_PROVIDERS.has(session.provider); + const recordedProviderAllowsDeferredN1x = isN1xOnboardingRecordedProvider(session?.provider, { + hasNimContainer: Boolean(session?.nimContainer), + }); // An explicit false is authoritative for rebuilds. Ordinary resume may use // the provider that this owner-only session already validated and recorded. const allowDeferredN1xOnboarding = diff --git a/src/lib/onboard/provider-recovery.ts b/src/lib/onboard/provider-recovery.ts index 65136cb00c0..a08ce84fa66 100644 --- a/src/lib/onboard/provider-recovery.ts +++ b/src/lib/onboard/provider-recovery.ts @@ -10,6 +10,7 @@ import { normalizeInferenceEndpointSource, } from "../inference/selection"; import { getLiveGatewayInference } from "../inference/live"; +import { persistedProviderNameToSelectionKey } from "./inference-providers/provider-selection-keys"; export type RemoteProviderConfigEntryLike = { providerName?: string }; @@ -79,19 +80,8 @@ export function providerNameToOptionKey( opts: { hasNimContainer?: boolean } = {}, ): string | null { if (!name) return null; - if (name === "nvidia-router") return "routed"; - if (name === "ollama-local") return "ollama"; - // Local NIM and standalone vLLM both persist as provider="vllm-local". NIM - // is positively identified by a nimContainer record; the absence of one in - // registry/session recovery reliably means standalone vLLM (the standalone - // path never records a container), so default to "vllm" there. Live-gateway - // recovery doesn't carry container info either, but the caller's - // option-availability check still gates on whether vllm is actually running. - if (name === "vllm-local") return opts.hasNimContainer ? "nim-local" : "vllm"; - // `nvidia-nim` is a legacy alias for cloud NVIDIA Endpoints (see - // setupInference: it routes nvidia-nim through REMOTE_PROVIDER_CONFIG.build), - // not a marker for Local NIM. Local NIM persists as vllm-local + nimContainer. - if (name === "nvidia-nim") return "build"; + const builtIn = persistedProviderNameToSelectionKey(name, opts); + if (builtIn) return builtIn; for (const [key, cfg] of Object.entries(remoteProviderConfig)) { if (cfg.providerName === name) return key; } diff --git a/src/lib/readiness/onboard-admission.test.ts b/src/lib/readiness/onboard-admission.test.ts index 6d789e874e0..00a51c1d896 100644 --- a/src/lib/readiness/onboard-admission.test.ts +++ b/src/lib/readiness/onboard-admission.test.ts @@ -110,6 +110,16 @@ describe("onboarding readiness admission (#7411)", () => { ["unknown provider", { NEMOCLAW_PROVIDER: "unknown-provider" }, false], ["N1x-excluded provider", { NEMOCLAW_PROVIDER: "nim-local" }, false], ["N1x-excluded provider alias", { NEMOCLAW_PROVIDER: "nim" }, false], + [ + "disabled Express with N1x-excluded provider", + { NEMOCLAW_NO_EXPRESS: "1", NEMOCLAW_PROVIDER: "nim-local" }, + false, + ], + [ + "disabled Express with N1x-excluded alias", + { NEMOCLAW_NO_EXPRESS: "1", NEMOCLAW_PROVIDER: "nim" }, + false, + ], ["blank provider", { NEMOCLAW_PROVIDER: " " }, false], ["disabled Express override", { NEMOCLAW_NO_EXPRESS: "0" }, false], ["no intent", {}, false], diff --git a/src/lib/readiness/onboard-admission.ts b/src/lib/readiness/onboard-admission.ts index cfb34e9dcea..18f10a4a0c1 100644 --- a/src/lib/readiness/onboard-admission.ts +++ b/src/lib/readiness/onboard-admission.ts @@ -15,7 +15,9 @@ export type OnboardReadinessAdmissionReasonId = export function hasExplicitDeferredN1xOnboardingIntent( env: Readonly>, ): boolean { - return env.NEMOCLAW_NO_EXPRESS === "1" || isN1xOnboardingProviderKey(env.NEMOCLAW_PROVIDER); + const provider = String(env.NEMOCLAW_PROVIDER ?? "").trim(); + if (provider) return isN1xOnboardingProviderKey(provider); + return env.NEMOCLAW_NO_EXPRESS === "1"; } export const ONBOARD_READINESS_FINDING_IDS = { diff --git a/test/helpers/installer-readiness-stubs.ts b/test/helpers/installer-readiness-stubs.ts index 2a394c79a60..ae82e8a2fe6 100644 --- a/test/helpers/installer-readiness-stubs.ts +++ b/test/helpers/installer-readiness-stubs.ts @@ -7,28 +7,6 @@ import os from "node:os"; import path from "node:path"; import { INSTALLER_PAYLOAD, TEST_SYSTEM_PATH, writeExecutable } from "./installer-sourced-env"; -export const DEFERRED_N1X_INTENT_STUB = ` -const deferredN1xProviderAliases = new Map([ - ["cloud", "build"], ["nim", "nim-local"], ["open-router", "openrouter"], - ["openrouterai", "openrouter"], ["anthropiccompatible", "anthropicCompatible"], - ["hermes", "hermesProvider"], ["hermes-provider", "hermesProvider"], - ["hermesprovider", "hermesProvider"], ["nous", "hermesProvider"], - ["nous-portal", "hermesProvider"], -]); -const deferredN1xProviderKeys = new Set([ - "build", "openrouter", "openai", "anthropic", "anthropicCompatible", "gemini", - "hermesProvider", "ollama", "llama-cpp", "install-llama-cpp", "custom", "nim-local", - "vllm", "routed", "install-vllm", "install-ollama", "install-windows-ollama", - "start-windows-ollama", -]); -exports.hasExplicitDeferredN1xOnboardingIntent = (env) => { - if (env.NEMOCLAW_NO_EXPRESS === "1") return true; - const raw = String(env.NEMOCLAW_PROVIDER || "").trim().toLowerCase(); - const normalized = deferredN1xProviderAliases.get(raw) || raw; - return deferredN1xProviderKeys.has(normalized) && normalized !== "nim-local"; -}; -`; - /** Fake node that reports v22.19.0. */ export function writeNodeStub(fakeBin: string): void { writeExecutable( @@ -106,7 +84,7 @@ export function writeInstallerReadinessModuleStubs(readinessDir: string): void { waivedFindingIds: [], }; }; -${DEFERRED_N1X_INTENT_STUB} +exports.hasExplicitDeferredN1xOnboardingIntent = () => false; `, ); fs.writeFileSync( diff --git a/test/package-contract/installer-host-preflight.test.ts b/test/package-contract/installer-host-preflight.test.ts index 1b554c2602c..9615ff22177 100644 --- a/test/package-contract/installer-host-preflight.test.ts +++ b/test/package-contract/installer-host-preflight.test.ts @@ -11,7 +11,6 @@ import { TEST_SYSTEM_PATH, writeExecutable, } from "../helpers/installer-sourced-env"; -import { DEFERRED_N1X_INTENT_STUB } from "../helpers/installer-readiness-stubs"; function writeNodeStub(fakeBin: string) { writeExecutable( @@ -28,6 +27,17 @@ exit 99`, ); } +function copyCompiledOnboardAdmission(readinessDir: string, onboardDir: string): void { + const compiledAdmission = path.resolve("dist/lib/readiness/onboard-admission.js"); + const compiledProviderKeys = path.resolve( + "dist/lib/onboard/inference-providers/provider-selection-keys.js", + ); + const targetProviderDir = path.join(onboardDir, "inference-providers"); + fs.mkdirSync(targetProviderDir, { recursive: true }); + fs.copyFileSync(compiledAdmission, path.join(readinessDir, "onboard-admission.js")); + fs.copyFileSync(compiledProviderKeys, path.join(targetProviderDir, "provider-selection-keys.js")); +} + function runInstallerHostAdmissionTest( host: { runtime: string; @@ -46,6 +56,7 @@ function runInstallerHostAdmissionTest( providerResolutionFailure?: string; provider?: string; noExpress?: boolean; + useCompiledAdmission?: boolean; } = {}, ) { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-installer-host-admission-")); @@ -133,13 +144,41 @@ exports.configuredRuntimeProviderOwnsHostReadiness = ({ environment = process.en for (const id of host.additionalFindingIds || []) { findings.push({ id, severity: "blocking", summary: "Blocking finding: " + id }); } - return { findings, capabilityIds: host.unknownCapabilityIds || [], host }; + const requiredCapabilities = [ + "host.docker.available", + "host.docker.daemon_reachable", + "host.docker.runtime_supported", + "host.docker.storage_compatible", + "host.gpu.nvidia_available", + "host.gpu.container_toolkit_available", + "host.gpu.cdi_healthy", + "host.platform.supported", + ]; + const unknown = new Set(host.unknownCapabilityIds || []); + const capabilities = requiredCapabilities.map((id) => ({ + id, + state: unknown.has(id) ? "unknown" : id === "host.platform.supported" && host.isN1x ? "absent" : "present", + })); + if (host.isN1x) capabilities.push({ id: "host.platform.n1x", state: "present" }); + for (const id of unknown) { + if (!capabilities.some((capability) => capability.id === id)) { + capabilities.push({ id, state: "unknown" }); + } + } + return { + observations: [], + capabilities, + findings, + capabilityIds: host.unknownCapabilityIds || [], + host, + }; }; `, ); - fs.writeFileSync( - path.join(readinessDir, "onboard-admission.js"), - `const forcedRejection = ${JSON.stringify(forcedRejection ?? null)}; + const writeStubbedOnboardAdmission = () => + fs.writeFileSync( + path.join(readinessDir, "onboard-admission.js"), + `const forcedRejection = ${JSON.stringify(forcedRejection ?? null)}; exports.evaluateOnboardReadinessAdmission = (report, options) => { if (forcedRejection) { return { admitted: false, reasonIds: [], ...forcedRejection, waivedFindingIds: [] }; @@ -197,9 +236,13 @@ exports.evaluateOnboardReadinessAdmission = (report, options) => { ? { admitted: true, waivedFindingIds: [] } : { admitted: false, reasonIds: [], findingIds, capabilityIds, waivedFindingIds: [] }; }; -${DEFERRED_N1X_INTENT_STUB} +exports.hasExplicitDeferredN1xOnboardingIntent = () => false; `, - ); + ); + const installOnboardAdmission = options.useCompiledAdmission + ? () => copyCompiledOnboardAdmission(readinessDir, onboardDir) + : writeStubbedOnboardAdmission; + installOnboardAdmission(); writeNodeStub(fakeBin); const { @@ -247,8 +290,8 @@ run_installer_host_preflight describe("installer host preflight package contract", () => { it.each([ - ["a declined Express preview", { noExpress: true }], - ["an explicit standard provider", { provider: "ollama" }], + ["a declined Express preview", { noExpress: true, useCompiledAdmission: true }], + ["an explicit standard provider", { provider: "ollama", useCompiledAdmission: true }], ])("admits qualified Deferred N1x through %s (#11041)", (_scenario, intent) => { const { output, result } = runInstallerHostAdmissionTest( { @@ -265,11 +308,15 @@ describe("installer host preflight package contract", () => { }); it("keeps Deferred N1x blocked without explicit onboarding intent (#11041)", () => { - const { output, result } = runInstallerHostAdmissionTest({ - runtime: "docker", - isN1x: true, - additionalFindingIds: ["host.platform.n1x_validation_pending"], - }); + const { output, result } = runInstallerHostAdmissionTest( + { + runtime: "docker", + isN1x: true, + additionalFindingIds: ["host.platform.n1x_validation_pending"], + }, + undefined, + { useCompiledAdmission: true }, + ); expect(result.status).toBe(1); expect(output).toContain("host.platform.n1x_validation_pending"); @@ -285,7 +332,25 @@ describe("installer host preflight package contract", () => { additionalFindingIds: ["host.platform.n1x_validation_pending"], }, undefined, - { provider }, + { provider, useCompiledAdmission: true }, + ); + + expect(result.status).toBe(1); + expect(output).toContain("host.platform.n1x_validation_pending"); + }, + ); + + it.each(["nim", "nim-local"])( + "keeps Deferred N1x blocked for provider %s when Express is disabled (#11041)", + (provider) => { + const { output, result } = runInstallerHostAdmissionTest( + { + runtime: "docker", + isN1x: true, + additionalFindingIds: ["host.platform.n1x_validation_pending"], + }, + undefined, + { noExpress: true, provider, useCompiledAdmission: true }, ); expect(result.status).toBe(1); @@ -293,6 +358,25 @@ describe("installer host preflight package contract", () => { }, ); + it("keeps unrelated blocking findings fail-closed for Deferred N1x intent (#11041)", () => { + const { output, result } = runInstallerHostAdmissionTest( + { + runtime: "docker", + isN1x: true, + additionalFindingIds: [ + "host.platform.n1x_validation_pending", + "host.test.additional_blocker", + ], + }, + undefined, + { provider: "ollama", useCompiledAdmission: true }, + ); + + expect(result.status).toBe(1); + expect(output).not.toContain("host.platform.n1x_validation_pending"); + expect(output).toContain("host.test.additional_blocker"); + }); + it("continues to onboarding when managed storage remediation is available", () => { const { output, result } = runInstallerHostAdmissionTest({ runtime: "docker", From f0b6497c632c51628312c88dcd38aeab51f425db Mon Sep 17 00:00:00 2001 From: San Dang Date: Fri, 4 Sep 2026 17:37:28 +0700 Subject: [PATCH 04/19] fix(rebuild): preserve N1x standard provider intent Signed-off-by: San Dang --- .../actions/sandbox/rebuild-gpu-opt-out.ts | 4 +- ...eflight-target-phase-orchestration.test.ts | 18 ++++-- .../sandbox/rebuild-preflight-target-phase.ts | 4 +- .../sandbox/rebuild-target-preflight.ts | 2 +- .../sandbox/rebuild-target-runtime.test.ts | 4 +- .../sandbox/rebuild-target-staging.test.ts | 57 +++++++++++++++++++ .../actions/sandbox/rebuild-target-staging.ts | 41 +++++++++---- .../sandbox/n1x-managed-vllm-rebuild.ts | 11 +++- src/lib/onboard.ts | 2 +- .../authoritative-rebuild-target.test.ts | 10 ++-- .../onboard/authoritative-rebuild-target.ts | 6 +- src/lib/onboard/fatal-runtime-preflight.ts | 8 +-- .../machine/handlers/preflight.test.ts | 4 +- src/lib/onboard/machine/handlers/preflight.ts | 6 +- .../onboard/machine/initial-flow-phases.ts | 4 +- src/lib/onboard/types.ts | 4 +- 16 files changed, 136 insertions(+), 49 deletions(-) diff --git a/src/lib/actions/sandbox/rebuild-gpu-opt-out.ts b/src/lib/actions/sandbox/rebuild-gpu-opt-out.ts index 5a1f5a5a6c2..a5655262151 100644 --- a/src/lib/actions/sandbox/rebuild-gpu-opt-out.ts +++ b/src/lib/actions/sandbox/rebuild-gpu-opt-out.ts @@ -130,8 +130,8 @@ export type RebuildRecreateOnboardOpts = { rebuildRegistryInferenceRoute?: RebuildRouteHandoff; rebuildProviderReconfigure?: RebuildProviderReconfigureHandoff; providerRecoveryReceipt?: ProviderRecoveryReceipt; - /** Recorded managed-vLLM intent admitted only by the N1x readiness exception. */ - allowDeferredN1xManagedVllm?: true; + /** Recorded provider intent admitted only by the Deferred N1x readiness exception. */ + allowDeferredN1xOnboarding?: true; /** Target-scoped authority admitted by the authoritative rebuild preflight. */ rebuildGatewayAuthority?: CheckpointGatewayAuthority; preparedImageRebuild?: PreparedImageRebuildHandoff; diff --git a/src/lib/actions/sandbox/rebuild-preflight-target-phase-orchestration.test.ts b/src/lib/actions/sandbox/rebuild-preflight-target-phase-orchestration.test.ts index 359a6fecf61..6a8e987f7ae 100644 --- a/src/lib/actions/sandbox/rebuild-preflight-target-phase-orchestration.test.ts +++ b/src/lib/actions/sandbox/rebuild-preflight-target-phase-orchestration.test.ts @@ -73,10 +73,12 @@ describe("prepareRebuildTargetPreflights", () => { async function prepareN1xTarget( endpointSource: "onboard" | "inference-set", mcp: { bridges: Record } | null = null, + provider = "vllm-local", + model = "nvidia/Qwen3.6-35B-A3B-NVFP4", ) { const resumeConfig = { - provider: "vllm-local", - model: "nvidia/Qwen3.6-35B-A3B-NVFP4", + provider, + model, preferredInferenceApi: "openai-completions", pinEndpoint: true, endpointUrl: null, @@ -203,15 +205,19 @@ describe("prepareRebuildTargetPreflights", () => { it("passes exact legacy N1x intent into authoritative readiness (#9292)", async () => { const readinessOptions = await prepareN1xTarget("onboard"); - expect(readinessOptions).toEqual( - expect.objectContaining({ allowDeferredN1xManagedVllm: true }), - ); + expect(readinessOptions).toEqual(expect.objectContaining({ allowDeferredN1xOnboarding: true })); + }); + + it("passes recorded Ollama intent into authoritative readiness (#11041)", async () => { + const readinessOptions = await prepareN1xTarget("onboard", null, "ollama-local", "qwen3.5:9b"); + + expect(readinessOptions).toEqual(expect.objectContaining({ allowDeferredN1xOnboarding: true })); }); it("withholds N1x intent for a mismatched endpoint source (#9292)", async () => { const readinessOptions = await prepareN1xTarget("inference-set"); - expect(readinessOptions).not.toHaveProperty("allowDeferredN1xManagedVllm"); + expect(readinessOptions).not.toHaveProperty("allowDeferredN1xOnboarding"); }); it("freezes one MCP runtime target before authoritative readiness (#10514)", async () => { diff --git a/src/lib/actions/sandbox/rebuild-preflight-target-phase.ts b/src/lib/actions/sandbox/rebuild-preflight-target-phase.ts index 496005d3755..5ab1071e1c9 100644 --- a/src/lib/actions/sandbox/rebuild-preflight-target-phase.ts +++ b/src/lib/actions/sandbox/rebuild-preflight-target-phase.ts @@ -62,7 +62,7 @@ import { prepareRebuildTargetConfig, type RebuildTargetConfig, stageRebuildHermesDashboardConfig, - stageRecordedManagedVllmIntent, + stageRecordedDeferredN1xIntent, } from "./rebuild-target-preflight"; /** Upper bound on how long a minted provider-recovery receipt stays valid. */ @@ -267,7 +267,7 @@ export async function prepareRebuildTargetPreflights(args: { recreateOptions.observabilityEnabled = requestedObservabilityEnabled ?? recreateOptions.observabilityEnabled; recreateOptions.observabilityRequestedExplicitly = requestedObservabilityEnabled !== undefined; - stageRecordedManagedVllmIntent(recreateOptions, sandboxEntry, resumeConfig); + stageRecordedDeferredN1xIntent(recreateOptions, sandboxEntry, resumeConfig); if ( !stageRebuildHermesDashboardConfig( rebuildAgent, diff --git a/src/lib/actions/sandbox/rebuild-target-preflight.ts b/src/lib/actions/sandbox/rebuild-target-preflight.ts index c0368f626bd..f899a45a113 100644 --- a/src/lib/actions/sandbox/rebuild-target-preflight.ts +++ b/src/lib/actions/sandbox/rebuild-target-preflight.ts @@ -19,5 +19,5 @@ export { hydrateMessagingConfigForRebuild, prepareRebuildRecreateOptions, stageRebuildHermesDashboardConfig, - stageRecordedManagedVllmIntent, + stageRecordedDeferredN1xIntent, } from "./rebuild-target-staging"; diff --git a/src/lib/actions/sandbox/rebuild-target-runtime.test.ts b/src/lib/actions/sandbox/rebuild-target-runtime.test.ts index 3592792193f..dc9a2d3eeef 100644 --- a/src/lib/actions/sandbox/rebuild-target-runtime.test.ts +++ b/src/lib/actions/sandbox/rebuild-target-runtime.test.ts @@ -207,7 +207,7 @@ describe("authoritative rebuild readiness", () => { mocks.preflightAuthoritativeRebuildTarget.mockResolvedValue(authority); const recreateOptions = { ...RECREATE_OPTIONS, - allowDeferredN1xManagedVllm: true, + allowDeferredN1xOnboarding: true, runtimeSelection, } as RebuildRecreateOnboardOpts; const bail = vi.fn((message: string): never => { @@ -225,7 +225,7 @@ describe("authoritative rebuild readiness", () => { expect(mocks.preflightAuthoritativeRebuildTarget).toHaveBeenCalledWith( expect.objectContaining({ - allowDeferredN1xManagedVllm: true, + allowDeferredN1xOnboarding: true, provider: "vllm-local", model: "test-model", runtimeSelection, diff --git a/src/lib/actions/sandbox/rebuild-target-staging.test.ts b/src/lib/actions/sandbox/rebuild-target-staging.test.ts index fd5eeaed276..068fd1eea66 100644 --- a/src/lib/actions/sandbox/rebuild-target-staging.test.ts +++ b/src/lib/actions/sandbox/rebuild-target-staging.test.ts @@ -17,6 +17,7 @@ import type { RebuildSandboxEntry } from "./rebuild-flow-helpers"; import { hydrateMessagingConfigForRebuild, prepareRebuildRecreateOptions, + stageRecordedDeferredN1xIntent, } from "./rebuild-target-staging"; const SANDBOX_ENTRY = { @@ -158,6 +159,62 @@ describe("prepareRebuildRecreateOptions", () => { }); }); +describe("stageRecordedDeferredN1xIntent", () => { + function stage(args: { + provider: string; + model?: string; + nimContainer?: string | null; + endpointSource?: "onboard" | "inference-set"; + }): { allowDeferredN1xOnboarding?: true } { + const recreateOptions: { allowDeferredN1xOnboarding?: true } = {}; + const model = args.model ?? "test-model"; + stageRecordedDeferredN1xIntent( + recreateOptions, + { + provider: args.provider, + model, + nimContainer: args.nimContainer ?? null, + endpointUrl: null, + endpointSource: args.endpointSource ?? "onboard", + openshellDriver: "docker", + hostLocalInferenceReceipt: null, + }, + { + provider: args.provider, + model, + pinEndpoint: true, + endpointUrl: null, + }, + ); + return recreateOptions; + } + + it.each([ + { name: "Ollama", provider: "ollama-local" }, + { name: "legacy NVIDIA Endpoints", provider: "nvidia-nim" }, + ])("stages a recognized recorded $name provider", ({ provider }) => { + expect(stage({ provider })).toEqual({ allowDeferredN1xOnboarding: true }); + }); + + it.each([ + { + name: "Local NIM", + input: { provider: "vllm-local", nimContainer: "alpha-nim" }, + }, + { name: "unknown provider", input: { provider: "future-provider" } }, + { + name: "malformed N1x Express record", + input: { + provider: "vllm-local", + model: "nvidia/Qwen3.6-35B-A3B-NVFP4", + endpointSource: "inference-set" as const, + }, + }, + ])("does not stage $name", ({ input }) => { + expect(stage(input)).toEqual({}); + }); +}); + describe("hydrateMessagingConfigForRebuild", () => { it("hydrates registry config instead of conflicting session config", () => { const registryPlan = messagingConfigPlan("0"); diff --git a/src/lib/actions/sandbox/rebuild-target-staging.ts b/src/lib/actions/sandbox/rebuild-target-staging.ts index e703e3ec364..005043c25b6 100644 --- a/src/lib/actions/sandbox/rebuild-target-staging.ts +++ b/src/lib/actions/sandbox/rebuild-target-staging.ts @@ -2,8 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import { hydrateMessagingChannelConfig } from "../../messaging-channel-config"; -import { isRecordedN1xManagedVllmRebuildEligible } from "../../domain/sandbox/n1x-managed-vllm-rebuild"; +import { + isN1xManagedVllmProviderModel, + isRecordedN1xManagedVllmRebuildEligible, +} from "../../domain/sandbox/n1x-managed-vllm-rebuild"; import { getStoredMessagingChannelConfig } from "../../onboard/messaging-config"; +import { isN1xOnboardingRecordedProvider } from "../../onboard/inference-providers/provider-selection-keys"; import { createRebuildRouteHandoff, type RegistryInferenceRoute, @@ -86,9 +90,9 @@ export function stageRebuildHermesDashboardConfig( return true; } -/** Stage the recorded N1x decision without moving action state into the domain layer. */ -export function stageRecordedManagedVllmIntent( - recreateOptions: Pick, +/** Stage a validated recorded N1x provider decision for authoritative rebuild readiness. */ +export function stageRecordedDeferredN1xIntent( + recreateOptions: Pick, sandboxEntry: Pick< RebuildSandboxEntry, | "provider" @@ -97,6 +101,7 @@ export function stageRecordedManagedVllmIntent( | "endpointSource" | "openshellDriver" | "hostLocalInferenceReceipt" + | "nimContainer" >, rebuildSelection: { provider: string; @@ -105,14 +110,26 @@ export function stageRecordedManagedVllmIntent( endpointUrl: string | null; }, ): void { - if ( - isRecordedN1xManagedVllmRebuildEligible( - sandboxEntry, - rebuildSelection, - parseHostLocalInferenceReceipt, - ) - ) { - recreateOptions.allowDeferredN1xManagedVllm = true; + const selectionMatchesRecord = + rebuildSelection.provider === sandboxEntry.provider && + rebuildSelection.model === sandboxEntry.model; + const isManagedVllmIdentity = isN1xManagedVllmProviderModel( + sandboxEntry.provider, + sandboxEntry.model, + ); + const recordedStandardProviderIsEligible = + selectionMatchesRecord && + !isManagedVllmIdentity && + isN1xOnboardingRecordedProvider(sandboxEntry.provider, { + hasNimContainer: Boolean(sandboxEntry.nimContainer), + }); + const recordedManagedVllmIsEligible = isRecordedN1xManagedVllmRebuildEligible( + sandboxEntry, + rebuildSelection, + parseHostLocalInferenceReceipt, + ); + if (recordedStandardProviderIsEligible || recordedManagedVllmIsEligible) { + recreateOptions.allowDeferredN1xOnboarding = true; } } diff --git a/src/lib/domain/sandbox/n1x-managed-vllm-rebuild.ts b/src/lib/domain/sandbox/n1x-managed-vllm-rebuild.ts index a3821aec1dc..5a06fbe99a0 100644 --- a/src/lib/domain/sandbox/n1x-managed-vllm-rebuild.ts +++ b/src/lib/domain/sandbox/n1x-managed-vllm-rebuild.ts @@ -6,6 +6,14 @@ import { VLLM_PORT } from "../../core/vllm-port"; const N1X_EXPRESS_PROVIDER = "vllm-local"; const N1X_EXPRESS_MODEL = "nvidia/Qwen3.6-35B-A3B-NVFP4"; +/** True only for the provider/model identity reserved by N1x Express. */ +export function isN1xManagedVllmProviderModel( + provider: string | null | undefined, + model: string | null | undefined, +): boolean { + return provider === N1X_EXPRESS_PROVIDER && model === N1X_EXPRESS_MODEL; +} + export interface RecordedN1xManagedVllmRoute { provider?: string | null; model?: string | null; @@ -42,8 +50,7 @@ export function isRecordedN1xManagedVllmRebuildEligible( const recordedEndpointUsesCanonicalLocalRoute = sandboxEntry.endpointUrl === null || sandboxEntry.endpointUrl === canonicalEndpointUrl; if ( - sandboxEntry.provider !== N1X_EXPRESS_PROVIDER || - sandboxEntry.model !== N1X_EXPRESS_MODEL || + !isN1xManagedVllmProviderModel(sandboxEntry.provider, sandboxEntry.model) || !recordedEndpointUsesCanonicalLocalRoute || sandboxEntry.endpointSource !== "onboard" || sandboxEntry.openshellDriver !== "docker" || diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 44fb3caf150..a0830b89f6d 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -2908,7 +2908,7 @@ async function runOnboard(opts: OnboardOptions = {}): Promise { sandboxGpuDevice: opts.sandboxGpuDevice ?? null, gpuRequested: opts.gpu === true, noGpu: opts.noGpu === true, - allowDeferredN1xManagedVllm: opts.allowDeferredN1xManagedVllm, + allowDeferredN1xOnboarding: opts.allowDeferredN1xOnboarding, env: process.env, recordedGpuPassthroughBeforePreflight, commitSelectedAgentTransition: selectedAgentTransition.commit, diff --git a/src/lib/onboard/authoritative-rebuild-target.test.ts b/src/lib/onboard/authoritative-rebuild-target.test.ts index 6ba1a34de50..e156de33210 100644 --- a/src/lib/onboard/authoritative-rebuild-target.test.ts +++ b/src/lib/onboard/authoritative-rebuild-target.test.ts @@ -48,7 +48,7 @@ describe("authoritative rebuild sandbox flow options", () => { }); describe("authoritative rebuild runtime preflight options", () => { - it("carries only target GPU state and recorded N1x preview intent (#9292)", () => { + it("carries only target GPU state and recorded N1x provider intent (#11041)", () => { const options = { authoritativeResumeConfig: true, sandboxName: "alpha", @@ -60,22 +60,22 @@ describe("authoritative rebuild runtime preflight options", () => { sandboxGpu: "enable", sandboxGpuDevice: "nvidia.com/gpu=all", noGpu: false, - allowDeferredN1xManagedVllm: true, + allowDeferredN1xOnboarding: true, } satisfies AuthoritativeRebuildPreflightOptions; expect(authoritativeRebuildRuntimePreflightOptions(options)).toEqual({ sandboxGpu: "enable", sandboxGpuDevice: "nvidia.com/gpu=all", noGpu: false, - allowDeferredN1xManagedVllm: true, + allowDeferredN1xOnboarding: true, }); - const { allowDeferredN1xManagedVllm: _recordedIntent, ...withoutRecordedIntent } = options; + const { allowDeferredN1xOnboarding: _recordedIntent, ...withoutRecordedIntent } = options; expect(authoritativeRebuildRuntimePreflightOptions(withoutRecordedIntent)).toEqual({ sandboxGpu: "enable", sandboxGpuDevice: "nvidia.com/gpu=all", noGpu: false, - allowDeferredN1xManagedVllm: false, + allowDeferredN1xOnboarding: false, }); }); }); diff --git a/src/lib/onboard/authoritative-rebuild-target.ts b/src/lib/onboard/authoritative-rebuild-target.ts index aeda139f451..e1550567fc1 100644 --- a/src/lib/onboard/authoritative-rebuild-target.ts +++ b/src/lib/onboard/authoritative-rebuild-target.ts @@ -84,7 +84,7 @@ export type AuthoritativeRebuildPreflightOptions = Pick< | "sandboxGpuDevice" | "noGpu" | "controlUiPort" - | "allowDeferredN1xManagedVllm" + | "allowDeferredN1xOnboarding" | "runtimeSelection" > & { authoritativeResumeConfig: true; @@ -101,13 +101,13 @@ export type AuthoritativeRebuildPreflightOptions = Pick< export function authoritativeRebuildRuntimePreflightOptions( opts: AuthoritativeRebuildPreflightOptions, ): Pick & { - allowDeferredN1xManagedVllm: boolean; + allowDeferredN1xOnboarding: boolean; } { return { sandboxGpu: opts.sandboxGpu, sandboxGpuDevice: opts.sandboxGpuDevice, noGpu: opts.noGpu, - allowDeferredN1xManagedVllm: opts.allowDeferredN1xManagedVllm === true, + allowDeferredN1xOnboarding: opts.allowDeferredN1xOnboarding === true, }; } diff --git a/src/lib/onboard/fatal-runtime-preflight.ts b/src/lib/onboard/fatal-runtime-preflight.ts index 9093f7f0d0d..ac6e7beecb4 100644 --- a/src/lib/onboard/fatal-runtime-preflight.ts +++ b/src/lib/onboard/fatal-runtime-preflight.ts @@ -53,7 +53,7 @@ export type FatalRuntimePreflightOptions = Pick< "sandboxGpu" | "sandboxGpuDevice" | "gpu" | "noGpu" > & { /** Explicit false prevents ambient provider intent from crossing a rebuild boundary. */ - allowDeferredN1xManagedVllm?: boolean; + allowDeferredN1xOnboarding?: boolean; optedOutGpuPassthrough?: boolean; }; @@ -315,7 +315,7 @@ function collectOnboardHostReadiness( wslDockerDesktopGpuProofPassed: runtimeGpu?.wslDockerDesktopGpuProofPassed, resuming: context.resuming, allowStorageRemediation, - allowDeferredN1xOnboarding: options.allowDeferredN1xManagedVllm, + allowDeferredN1xOnboarding: options.allowDeferredN1xOnboarding, // The initial host readiness gate already presented warning advisories. presentAdvisories: false, exitProcess: context.exitProcess, @@ -410,7 +410,7 @@ async function collectAdmittedReadinessPair( host.result.sandboxGpuConfig.mode === "0" || options.optedOutGpuPassthrough === true, resuming: context.resuming, allowStorageRemediation: isManagedGatewayReadiness(gateway), - allowDeferredN1xOnboarding: options.allowDeferredN1xManagedVllm, + allowDeferredN1xOnboarding: options.allowDeferredN1xOnboarding, presentAdvisories: false, exitProcess, }); @@ -593,7 +593,7 @@ export function runFatalOnboardRuntimePreflight( explicitlyOptedOutGpuPassthrough, resuming: context.resuming, allowStorageRemediation: context.allowStorageRemediation, - allowDeferredN1xOnboarding: options.allowDeferredN1xManagedVllm, + allowDeferredN1xOnboarding: options.allowDeferredN1xOnboarding, exitProcess, observedAt, now, diff --git a/src/lib/onboard/machine/handlers/preflight.test.ts b/src/lib/onboard/machine/handlers/preflight.test.ts index a768ed0929a..68a710badd8 100644 --- a/src/lib/onboard/machine/handlers/preflight.test.ts +++ b/src/lib/onboard/machine/handlers/preflight.test.ts @@ -204,7 +204,7 @@ describe("handlePreflightState", () => { await handlePreflightState({ ...baseOptions(harness.deps, session), resume: true, - allowDeferredN1xManagedVllm: true, + allowDeferredN1xOnboarding: true, }); expect(assertOnboardHostReadiness).toHaveBeenCalledTimes(2); @@ -261,7 +261,7 @@ describe("handlePreflightState", () => { await handlePreflightState({ ...baseOptions(harness.deps, session), resume: true, - ...(authority === undefined ? {} : { allowDeferredN1xManagedVllm: authority }), + ...(authority === undefined ? {} : { allowDeferredN1xOnboarding: authority }), }); expect(assertOnboardHostReadiness).toHaveBeenNthCalledWith( diff --git a/src/lib/onboard/machine/handlers/preflight.ts b/src/lib/onboard/machine/handlers/preflight.ts index 51669940d54..877d5c61472 100644 --- a/src/lib/onboard/machine/handlers/preflight.ts +++ b/src/lib/onboard/machine/handlers/preflight.ts @@ -35,7 +35,7 @@ export interface PreflightStateOptions< sandboxGpuDevice?: string | null; gpuRequested: boolean; noGpu: boolean; - allowDeferredN1xManagedVllm?: boolean; + allowDeferredN1xOnboarding?: boolean; env: NodeJS.ProcessEnv; deps: { getSandbox(name: string): SandboxEntry | null; @@ -131,7 +131,7 @@ export async function handlePreflightState< sandboxGpuDevice, gpuRequested, noGpu, - allowDeferredN1xManagedVllm, + allowDeferredN1xOnboarding: explicitDeferredN1xOnboarding, env, deps, }: PreflightStateOptions): Promise< @@ -158,7 +158,7 @@ export async function handlePreflightState< // An explicit false is authoritative for rebuilds. Ordinary resume may use // the provider that this owner-only session already validated and recorded. const allowDeferredN1xOnboarding = - allowDeferredN1xManagedVllm ?? recordedProviderAllowsDeferredN1x; + explicitDeferredN1xOnboarding ?? recordedProviderAllowsDeferredN1x; let gpu: Gpu; if (resumePreflight) { diff --git a/src/lib/onboard/machine/initial-flow-phases.ts b/src/lib/onboard/machine/initial-flow-phases.ts index 769b47a0f81..d07d71a0529 100644 --- a/src/lib/onboard/machine/initial-flow-phases.ts +++ b/src/lib/onboard/machine/initial-flow-phases.ts @@ -53,7 +53,7 @@ export interface InitialOnboardFlowPhaseOptions< sandboxGpuDevice?: string | null; gpuRequested: boolean; noGpu: boolean; - allowDeferredN1xManagedVllm?: boolean; + allowDeferredN1xOnboarding?: boolean; env: NodeJS.ProcessEnv; platform?: NodeJS.Platform; recordedGpuPassthroughBeforePreflight: boolean; @@ -145,7 +145,7 @@ export function createInitialOnboardFlowPhases< sandboxGpuDevice: options.sandboxGpuDevice ?? null, gpuRequested: options.gpuRequested, noGpu: options.noGpu, - allowDeferredN1xManagedVllm: options.allowDeferredN1xManagedVllm, + allowDeferredN1xOnboarding: options.allowDeferredN1xOnboarding, env: options.env, deps: { ...options.preflightDeps, diff --git a/src/lib/onboard/types.ts b/src/lib/onboard/types.ts index c8e8b63deb3..8607471d4a9 100644 --- a/src/lib/onboard/types.ts +++ b/src/lib/onboard/types.ts @@ -156,8 +156,8 @@ export type OnboardOptions = { rebuildProviderReconfigure?: import("./rebuild-route-handoff").RebuildProviderReconfigureHandoff; /** Internal one-shot authority to recover the recorded provider during a locked rebuild resume. */ providerRecoveryReceipt?: import("./rebuild-route-handoff").ProviderRecoveryReceipt; - /** Internal rebuild handoff for a recorded managed-vLLM N1x preview selection. */ - allowDeferredN1xManagedVllm?: true; + /** Internal rebuild handoff for a recorded provider admitted by Deferred N1x readiness. */ + allowDeferredN1xOnboarding?: true; /** Internal one-shot handoff for the exact image context validated before rebuild deletion. */ preparedImageRebuild?: import("./prepared-dcode-rebuild").PreparedImageRebuildHandoff; /** Internal immutable managed-image/profile handoff validated before rebuild deletion. */ From b074586ecf72b75df7cebcceb3c3d33fa31d0151 Mon Sep 17 00:00:00 2001 From: San Dang Date: Fri, 4 Sep 2026 18:39:06 +0700 Subject: [PATCH 05/19] fix(onboard): limit N1x fallback to accepted scope Reverts f0b6497c6 so #11041 remains limited to fresh onboarding and its accepted validation scope. Signed-off-by: San Dang --- .../actions/sandbox/rebuild-gpu-opt-out.ts | 4 +- ...eflight-target-phase-orchestration.test.ts | 18 ++---- .../sandbox/rebuild-preflight-target-phase.ts | 4 +- .../sandbox/rebuild-target-preflight.ts | 2 +- .../sandbox/rebuild-target-runtime.test.ts | 4 +- .../sandbox/rebuild-target-staging.test.ts | 57 ------------------- .../actions/sandbox/rebuild-target-staging.ts | 41 ++++--------- .../sandbox/n1x-managed-vllm-rebuild.ts | 11 +--- src/lib/onboard.ts | 2 +- .../authoritative-rebuild-target.test.ts | 10 ++-- .../onboard/authoritative-rebuild-target.ts | 6 +- src/lib/onboard/fatal-runtime-preflight.ts | 8 +-- .../machine/handlers/preflight.test.ts | 4 +- src/lib/onboard/machine/handlers/preflight.ts | 6 +- .../onboard/machine/initial-flow-phases.ts | 4 +- src/lib/onboard/types.ts | 4 +- 16 files changed, 49 insertions(+), 136 deletions(-) diff --git a/src/lib/actions/sandbox/rebuild-gpu-opt-out.ts b/src/lib/actions/sandbox/rebuild-gpu-opt-out.ts index a5655262151..5a1f5a5a6c2 100644 --- a/src/lib/actions/sandbox/rebuild-gpu-opt-out.ts +++ b/src/lib/actions/sandbox/rebuild-gpu-opt-out.ts @@ -130,8 +130,8 @@ export type RebuildRecreateOnboardOpts = { rebuildRegistryInferenceRoute?: RebuildRouteHandoff; rebuildProviderReconfigure?: RebuildProviderReconfigureHandoff; providerRecoveryReceipt?: ProviderRecoveryReceipt; - /** Recorded provider intent admitted only by the Deferred N1x readiness exception. */ - allowDeferredN1xOnboarding?: true; + /** Recorded managed-vLLM intent admitted only by the N1x readiness exception. */ + allowDeferredN1xManagedVllm?: true; /** Target-scoped authority admitted by the authoritative rebuild preflight. */ rebuildGatewayAuthority?: CheckpointGatewayAuthority; preparedImageRebuild?: PreparedImageRebuildHandoff; diff --git a/src/lib/actions/sandbox/rebuild-preflight-target-phase-orchestration.test.ts b/src/lib/actions/sandbox/rebuild-preflight-target-phase-orchestration.test.ts index 6a8e987f7ae..359a6fecf61 100644 --- a/src/lib/actions/sandbox/rebuild-preflight-target-phase-orchestration.test.ts +++ b/src/lib/actions/sandbox/rebuild-preflight-target-phase-orchestration.test.ts @@ -73,12 +73,10 @@ describe("prepareRebuildTargetPreflights", () => { async function prepareN1xTarget( endpointSource: "onboard" | "inference-set", mcp: { bridges: Record } | null = null, - provider = "vllm-local", - model = "nvidia/Qwen3.6-35B-A3B-NVFP4", ) { const resumeConfig = { - provider, - model, + provider: "vllm-local", + model: "nvidia/Qwen3.6-35B-A3B-NVFP4", preferredInferenceApi: "openai-completions", pinEndpoint: true, endpointUrl: null, @@ -205,19 +203,15 @@ describe("prepareRebuildTargetPreflights", () => { it("passes exact legacy N1x intent into authoritative readiness (#9292)", async () => { const readinessOptions = await prepareN1xTarget("onboard"); - expect(readinessOptions).toEqual(expect.objectContaining({ allowDeferredN1xOnboarding: true })); - }); - - it("passes recorded Ollama intent into authoritative readiness (#11041)", async () => { - const readinessOptions = await prepareN1xTarget("onboard", null, "ollama-local", "qwen3.5:9b"); - - expect(readinessOptions).toEqual(expect.objectContaining({ allowDeferredN1xOnboarding: true })); + expect(readinessOptions).toEqual( + expect.objectContaining({ allowDeferredN1xManagedVllm: true }), + ); }); it("withholds N1x intent for a mismatched endpoint source (#9292)", async () => { const readinessOptions = await prepareN1xTarget("inference-set"); - expect(readinessOptions).not.toHaveProperty("allowDeferredN1xOnboarding"); + expect(readinessOptions).not.toHaveProperty("allowDeferredN1xManagedVllm"); }); it("freezes one MCP runtime target before authoritative readiness (#10514)", async () => { diff --git a/src/lib/actions/sandbox/rebuild-preflight-target-phase.ts b/src/lib/actions/sandbox/rebuild-preflight-target-phase.ts index 5ab1071e1c9..496005d3755 100644 --- a/src/lib/actions/sandbox/rebuild-preflight-target-phase.ts +++ b/src/lib/actions/sandbox/rebuild-preflight-target-phase.ts @@ -62,7 +62,7 @@ import { prepareRebuildTargetConfig, type RebuildTargetConfig, stageRebuildHermesDashboardConfig, - stageRecordedDeferredN1xIntent, + stageRecordedManagedVllmIntent, } from "./rebuild-target-preflight"; /** Upper bound on how long a minted provider-recovery receipt stays valid. */ @@ -267,7 +267,7 @@ export async function prepareRebuildTargetPreflights(args: { recreateOptions.observabilityEnabled = requestedObservabilityEnabled ?? recreateOptions.observabilityEnabled; recreateOptions.observabilityRequestedExplicitly = requestedObservabilityEnabled !== undefined; - stageRecordedDeferredN1xIntent(recreateOptions, sandboxEntry, resumeConfig); + stageRecordedManagedVllmIntent(recreateOptions, sandboxEntry, resumeConfig); if ( !stageRebuildHermesDashboardConfig( rebuildAgent, diff --git a/src/lib/actions/sandbox/rebuild-target-preflight.ts b/src/lib/actions/sandbox/rebuild-target-preflight.ts index f899a45a113..c0368f626bd 100644 --- a/src/lib/actions/sandbox/rebuild-target-preflight.ts +++ b/src/lib/actions/sandbox/rebuild-target-preflight.ts @@ -19,5 +19,5 @@ export { hydrateMessagingConfigForRebuild, prepareRebuildRecreateOptions, stageRebuildHermesDashboardConfig, - stageRecordedDeferredN1xIntent, + stageRecordedManagedVllmIntent, } from "./rebuild-target-staging"; diff --git a/src/lib/actions/sandbox/rebuild-target-runtime.test.ts b/src/lib/actions/sandbox/rebuild-target-runtime.test.ts index dc9a2d3eeef..3592792193f 100644 --- a/src/lib/actions/sandbox/rebuild-target-runtime.test.ts +++ b/src/lib/actions/sandbox/rebuild-target-runtime.test.ts @@ -207,7 +207,7 @@ describe("authoritative rebuild readiness", () => { mocks.preflightAuthoritativeRebuildTarget.mockResolvedValue(authority); const recreateOptions = { ...RECREATE_OPTIONS, - allowDeferredN1xOnboarding: true, + allowDeferredN1xManagedVllm: true, runtimeSelection, } as RebuildRecreateOnboardOpts; const bail = vi.fn((message: string): never => { @@ -225,7 +225,7 @@ describe("authoritative rebuild readiness", () => { expect(mocks.preflightAuthoritativeRebuildTarget).toHaveBeenCalledWith( expect.objectContaining({ - allowDeferredN1xOnboarding: true, + allowDeferredN1xManagedVllm: true, provider: "vllm-local", model: "test-model", runtimeSelection, diff --git a/src/lib/actions/sandbox/rebuild-target-staging.test.ts b/src/lib/actions/sandbox/rebuild-target-staging.test.ts index 068fd1eea66..fd5eeaed276 100644 --- a/src/lib/actions/sandbox/rebuild-target-staging.test.ts +++ b/src/lib/actions/sandbox/rebuild-target-staging.test.ts @@ -17,7 +17,6 @@ import type { RebuildSandboxEntry } from "./rebuild-flow-helpers"; import { hydrateMessagingConfigForRebuild, prepareRebuildRecreateOptions, - stageRecordedDeferredN1xIntent, } from "./rebuild-target-staging"; const SANDBOX_ENTRY = { @@ -159,62 +158,6 @@ describe("prepareRebuildRecreateOptions", () => { }); }); -describe("stageRecordedDeferredN1xIntent", () => { - function stage(args: { - provider: string; - model?: string; - nimContainer?: string | null; - endpointSource?: "onboard" | "inference-set"; - }): { allowDeferredN1xOnboarding?: true } { - const recreateOptions: { allowDeferredN1xOnboarding?: true } = {}; - const model = args.model ?? "test-model"; - stageRecordedDeferredN1xIntent( - recreateOptions, - { - provider: args.provider, - model, - nimContainer: args.nimContainer ?? null, - endpointUrl: null, - endpointSource: args.endpointSource ?? "onboard", - openshellDriver: "docker", - hostLocalInferenceReceipt: null, - }, - { - provider: args.provider, - model, - pinEndpoint: true, - endpointUrl: null, - }, - ); - return recreateOptions; - } - - it.each([ - { name: "Ollama", provider: "ollama-local" }, - { name: "legacy NVIDIA Endpoints", provider: "nvidia-nim" }, - ])("stages a recognized recorded $name provider", ({ provider }) => { - expect(stage({ provider })).toEqual({ allowDeferredN1xOnboarding: true }); - }); - - it.each([ - { - name: "Local NIM", - input: { provider: "vllm-local", nimContainer: "alpha-nim" }, - }, - { name: "unknown provider", input: { provider: "future-provider" } }, - { - name: "malformed N1x Express record", - input: { - provider: "vllm-local", - model: "nvidia/Qwen3.6-35B-A3B-NVFP4", - endpointSource: "inference-set" as const, - }, - }, - ])("does not stage $name", ({ input }) => { - expect(stage(input)).toEqual({}); - }); -}); - describe("hydrateMessagingConfigForRebuild", () => { it("hydrates registry config instead of conflicting session config", () => { const registryPlan = messagingConfigPlan("0"); diff --git a/src/lib/actions/sandbox/rebuild-target-staging.ts b/src/lib/actions/sandbox/rebuild-target-staging.ts index 005043c25b6..e703e3ec364 100644 --- a/src/lib/actions/sandbox/rebuild-target-staging.ts +++ b/src/lib/actions/sandbox/rebuild-target-staging.ts @@ -2,12 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import { hydrateMessagingChannelConfig } from "../../messaging-channel-config"; -import { - isN1xManagedVllmProviderModel, - isRecordedN1xManagedVllmRebuildEligible, -} from "../../domain/sandbox/n1x-managed-vllm-rebuild"; +import { isRecordedN1xManagedVllmRebuildEligible } from "../../domain/sandbox/n1x-managed-vllm-rebuild"; import { getStoredMessagingChannelConfig } from "../../onboard/messaging-config"; -import { isN1xOnboardingRecordedProvider } from "../../onboard/inference-providers/provider-selection-keys"; import { createRebuildRouteHandoff, type RegistryInferenceRoute, @@ -90,9 +86,9 @@ export function stageRebuildHermesDashboardConfig( return true; } -/** Stage a validated recorded N1x provider decision for authoritative rebuild readiness. */ -export function stageRecordedDeferredN1xIntent( - recreateOptions: Pick, +/** Stage the recorded N1x decision without moving action state into the domain layer. */ +export function stageRecordedManagedVllmIntent( + recreateOptions: Pick, sandboxEntry: Pick< RebuildSandboxEntry, | "provider" @@ -101,7 +97,6 @@ export function stageRecordedDeferredN1xIntent( | "endpointSource" | "openshellDriver" | "hostLocalInferenceReceipt" - | "nimContainer" >, rebuildSelection: { provider: string; @@ -110,26 +105,14 @@ export function stageRecordedDeferredN1xIntent( endpointUrl: string | null; }, ): void { - const selectionMatchesRecord = - rebuildSelection.provider === sandboxEntry.provider && - rebuildSelection.model === sandboxEntry.model; - const isManagedVllmIdentity = isN1xManagedVllmProviderModel( - sandboxEntry.provider, - sandboxEntry.model, - ); - const recordedStandardProviderIsEligible = - selectionMatchesRecord && - !isManagedVllmIdentity && - isN1xOnboardingRecordedProvider(sandboxEntry.provider, { - hasNimContainer: Boolean(sandboxEntry.nimContainer), - }); - const recordedManagedVllmIsEligible = isRecordedN1xManagedVllmRebuildEligible( - sandboxEntry, - rebuildSelection, - parseHostLocalInferenceReceipt, - ); - if (recordedStandardProviderIsEligible || recordedManagedVllmIsEligible) { - recreateOptions.allowDeferredN1xOnboarding = true; + if ( + isRecordedN1xManagedVllmRebuildEligible( + sandboxEntry, + rebuildSelection, + parseHostLocalInferenceReceipt, + ) + ) { + recreateOptions.allowDeferredN1xManagedVllm = true; } } diff --git a/src/lib/domain/sandbox/n1x-managed-vllm-rebuild.ts b/src/lib/domain/sandbox/n1x-managed-vllm-rebuild.ts index 5a06fbe99a0..a3821aec1dc 100644 --- a/src/lib/domain/sandbox/n1x-managed-vllm-rebuild.ts +++ b/src/lib/domain/sandbox/n1x-managed-vllm-rebuild.ts @@ -6,14 +6,6 @@ import { VLLM_PORT } from "../../core/vllm-port"; const N1X_EXPRESS_PROVIDER = "vllm-local"; const N1X_EXPRESS_MODEL = "nvidia/Qwen3.6-35B-A3B-NVFP4"; -/** True only for the provider/model identity reserved by N1x Express. */ -export function isN1xManagedVllmProviderModel( - provider: string | null | undefined, - model: string | null | undefined, -): boolean { - return provider === N1X_EXPRESS_PROVIDER && model === N1X_EXPRESS_MODEL; -} - export interface RecordedN1xManagedVllmRoute { provider?: string | null; model?: string | null; @@ -50,7 +42,8 @@ export function isRecordedN1xManagedVllmRebuildEligible( const recordedEndpointUsesCanonicalLocalRoute = sandboxEntry.endpointUrl === null || sandboxEntry.endpointUrl === canonicalEndpointUrl; if ( - !isN1xManagedVllmProviderModel(sandboxEntry.provider, sandboxEntry.model) || + sandboxEntry.provider !== N1X_EXPRESS_PROVIDER || + sandboxEntry.model !== N1X_EXPRESS_MODEL || !recordedEndpointUsesCanonicalLocalRoute || sandboxEntry.endpointSource !== "onboard" || sandboxEntry.openshellDriver !== "docker" || diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index a0830b89f6d..44fb3caf150 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -2908,7 +2908,7 @@ async function runOnboard(opts: OnboardOptions = {}): Promise { sandboxGpuDevice: opts.sandboxGpuDevice ?? null, gpuRequested: opts.gpu === true, noGpu: opts.noGpu === true, - allowDeferredN1xOnboarding: opts.allowDeferredN1xOnboarding, + allowDeferredN1xManagedVllm: opts.allowDeferredN1xManagedVllm, env: process.env, recordedGpuPassthroughBeforePreflight, commitSelectedAgentTransition: selectedAgentTransition.commit, diff --git a/src/lib/onboard/authoritative-rebuild-target.test.ts b/src/lib/onboard/authoritative-rebuild-target.test.ts index e156de33210..6ba1a34de50 100644 --- a/src/lib/onboard/authoritative-rebuild-target.test.ts +++ b/src/lib/onboard/authoritative-rebuild-target.test.ts @@ -48,7 +48,7 @@ describe("authoritative rebuild sandbox flow options", () => { }); describe("authoritative rebuild runtime preflight options", () => { - it("carries only target GPU state and recorded N1x provider intent (#11041)", () => { + it("carries only target GPU state and recorded N1x preview intent (#9292)", () => { const options = { authoritativeResumeConfig: true, sandboxName: "alpha", @@ -60,22 +60,22 @@ describe("authoritative rebuild runtime preflight options", () => { sandboxGpu: "enable", sandboxGpuDevice: "nvidia.com/gpu=all", noGpu: false, - allowDeferredN1xOnboarding: true, + allowDeferredN1xManagedVllm: true, } satisfies AuthoritativeRebuildPreflightOptions; expect(authoritativeRebuildRuntimePreflightOptions(options)).toEqual({ sandboxGpu: "enable", sandboxGpuDevice: "nvidia.com/gpu=all", noGpu: false, - allowDeferredN1xOnboarding: true, + allowDeferredN1xManagedVllm: true, }); - const { allowDeferredN1xOnboarding: _recordedIntent, ...withoutRecordedIntent } = options; + const { allowDeferredN1xManagedVllm: _recordedIntent, ...withoutRecordedIntent } = options; expect(authoritativeRebuildRuntimePreflightOptions(withoutRecordedIntent)).toEqual({ sandboxGpu: "enable", sandboxGpuDevice: "nvidia.com/gpu=all", noGpu: false, - allowDeferredN1xOnboarding: false, + allowDeferredN1xManagedVllm: false, }); }); }); diff --git a/src/lib/onboard/authoritative-rebuild-target.ts b/src/lib/onboard/authoritative-rebuild-target.ts index e1550567fc1..aeda139f451 100644 --- a/src/lib/onboard/authoritative-rebuild-target.ts +++ b/src/lib/onboard/authoritative-rebuild-target.ts @@ -84,7 +84,7 @@ export type AuthoritativeRebuildPreflightOptions = Pick< | "sandboxGpuDevice" | "noGpu" | "controlUiPort" - | "allowDeferredN1xOnboarding" + | "allowDeferredN1xManagedVllm" | "runtimeSelection" > & { authoritativeResumeConfig: true; @@ -101,13 +101,13 @@ export type AuthoritativeRebuildPreflightOptions = Pick< export function authoritativeRebuildRuntimePreflightOptions( opts: AuthoritativeRebuildPreflightOptions, ): Pick & { - allowDeferredN1xOnboarding: boolean; + allowDeferredN1xManagedVllm: boolean; } { return { sandboxGpu: opts.sandboxGpu, sandboxGpuDevice: opts.sandboxGpuDevice, noGpu: opts.noGpu, - allowDeferredN1xOnboarding: opts.allowDeferredN1xOnboarding === true, + allowDeferredN1xManagedVllm: opts.allowDeferredN1xManagedVllm === true, }; } diff --git a/src/lib/onboard/fatal-runtime-preflight.ts b/src/lib/onboard/fatal-runtime-preflight.ts index ac6e7beecb4..9093f7f0d0d 100644 --- a/src/lib/onboard/fatal-runtime-preflight.ts +++ b/src/lib/onboard/fatal-runtime-preflight.ts @@ -53,7 +53,7 @@ export type FatalRuntimePreflightOptions = Pick< "sandboxGpu" | "sandboxGpuDevice" | "gpu" | "noGpu" > & { /** Explicit false prevents ambient provider intent from crossing a rebuild boundary. */ - allowDeferredN1xOnboarding?: boolean; + allowDeferredN1xManagedVllm?: boolean; optedOutGpuPassthrough?: boolean; }; @@ -315,7 +315,7 @@ function collectOnboardHostReadiness( wslDockerDesktopGpuProofPassed: runtimeGpu?.wslDockerDesktopGpuProofPassed, resuming: context.resuming, allowStorageRemediation, - allowDeferredN1xOnboarding: options.allowDeferredN1xOnboarding, + allowDeferredN1xOnboarding: options.allowDeferredN1xManagedVllm, // The initial host readiness gate already presented warning advisories. presentAdvisories: false, exitProcess: context.exitProcess, @@ -410,7 +410,7 @@ async function collectAdmittedReadinessPair( host.result.sandboxGpuConfig.mode === "0" || options.optedOutGpuPassthrough === true, resuming: context.resuming, allowStorageRemediation: isManagedGatewayReadiness(gateway), - allowDeferredN1xOnboarding: options.allowDeferredN1xOnboarding, + allowDeferredN1xOnboarding: options.allowDeferredN1xManagedVllm, presentAdvisories: false, exitProcess, }); @@ -593,7 +593,7 @@ export function runFatalOnboardRuntimePreflight( explicitlyOptedOutGpuPassthrough, resuming: context.resuming, allowStorageRemediation: context.allowStorageRemediation, - allowDeferredN1xOnboarding: options.allowDeferredN1xOnboarding, + allowDeferredN1xOnboarding: options.allowDeferredN1xManagedVllm, exitProcess, observedAt, now, diff --git a/src/lib/onboard/machine/handlers/preflight.test.ts b/src/lib/onboard/machine/handlers/preflight.test.ts index 68a710badd8..a768ed0929a 100644 --- a/src/lib/onboard/machine/handlers/preflight.test.ts +++ b/src/lib/onboard/machine/handlers/preflight.test.ts @@ -204,7 +204,7 @@ describe("handlePreflightState", () => { await handlePreflightState({ ...baseOptions(harness.deps, session), resume: true, - allowDeferredN1xOnboarding: true, + allowDeferredN1xManagedVllm: true, }); expect(assertOnboardHostReadiness).toHaveBeenCalledTimes(2); @@ -261,7 +261,7 @@ describe("handlePreflightState", () => { await handlePreflightState({ ...baseOptions(harness.deps, session), resume: true, - ...(authority === undefined ? {} : { allowDeferredN1xOnboarding: authority }), + ...(authority === undefined ? {} : { allowDeferredN1xManagedVllm: authority }), }); expect(assertOnboardHostReadiness).toHaveBeenNthCalledWith( diff --git a/src/lib/onboard/machine/handlers/preflight.ts b/src/lib/onboard/machine/handlers/preflight.ts index 877d5c61472..51669940d54 100644 --- a/src/lib/onboard/machine/handlers/preflight.ts +++ b/src/lib/onboard/machine/handlers/preflight.ts @@ -35,7 +35,7 @@ export interface PreflightStateOptions< sandboxGpuDevice?: string | null; gpuRequested: boolean; noGpu: boolean; - allowDeferredN1xOnboarding?: boolean; + allowDeferredN1xManagedVllm?: boolean; env: NodeJS.ProcessEnv; deps: { getSandbox(name: string): SandboxEntry | null; @@ -131,7 +131,7 @@ export async function handlePreflightState< sandboxGpuDevice, gpuRequested, noGpu, - allowDeferredN1xOnboarding: explicitDeferredN1xOnboarding, + allowDeferredN1xManagedVllm, env, deps, }: PreflightStateOptions): Promise< @@ -158,7 +158,7 @@ export async function handlePreflightState< // An explicit false is authoritative for rebuilds. Ordinary resume may use // the provider that this owner-only session already validated and recorded. const allowDeferredN1xOnboarding = - explicitDeferredN1xOnboarding ?? recordedProviderAllowsDeferredN1x; + allowDeferredN1xManagedVllm ?? recordedProviderAllowsDeferredN1x; let gpu: Gpu; if (resumePreflight) { diff --git a/src/lib/onboard/machine/initial-flow-phases.ts b/src/lib/onboard/machine/initial-flow-phases.ts index d07d71a0529..769b47a0f81 100644 --- a/src/lib/onboard/machine/initial-flow-phases.ts +++ b/src/lib/onboard/machine/initial-flow-phases.ts @@ -53,7 +53,7 @@ export interface InitialOnboardFlowPhaseOptions< sandboxGpuDevice?: string | null; gpuRequested: boolean; noGpu: boolean; - allowDeferredN1xOnboarding?: boolean; + allowDeferredN1xManagedVllm?: boolean; env: NodeJS.ProcessEnv; platform?: NodeJS.Platform; recordedGpuPassthroughBeforePreflight: boolean; @@ -145,7 +145,7 @@ export function createInitialOnboardFlowPhases< sandboxGpuDevice: options.sandboxGpuDevice ?? null, gpuRequested: options.gpuRequested, noGpu: options.noGpu, - allowDeferredN1xOnboarding: options.allowDeferredN1xOnboarding, + allowDeferredN1xManagedVllm: options.allowDeferredN1xManagedVllm, env: options.env, deps: { ...options.preflightDeps, diff --git a/src/lib/onboard/types.ts b/src/lib/onboard/types.ts index 8607471d4a9..c8e8b63deb3 100644 --- a/src/lib/onboard/types.ts +++ b/src/lib/onboard/types.ts @@ -156,8 +156,8 @@ export type OnboardOptions = { rebuildProviderReconfigure?: import("./rebuild-route-handoff").RebuildProviderReconfigureHandoff; /** Internal one-shot authority to recover the recorded provider during a locked rebuild resume. */ providerRecoveryReceipt?: import("./rebuild-route-handoff").ProviderRecoveryReceipt; - /** Internal rebuild handoff for a recorded provider admitted by Deferred N1x readiness. */ - allowDeferredN1xOnboarding?: true; + /** Internal rebuild handoff for a recorded managed-vLLM N1x preview selection. */ + allowDeferredN1xManagedVllm?: true; /** Internal one-shot handoff for the exact image context validated before rebuild deletion. */ preparedImageRebuild?: import("./prepared-dcode-rebuild").PreparedImageRebuildHandoff; /** Internal immutable managed-image/profile handoff validated before rebuild deletion. */ From 0391b03447b92d02fc006749e9d7bf4679286d36 Mon Sep 17 00:00:00 2001 From: San Dang Date: Fri, 4 Sep 2026 18:55:14 +0700 Subject: [PATCH 06/19] fix(onboard): simplify N1x standard fallback Signed-off-by: San Dang --- ci/platform-matrix.json | 8 +- docs/get-started/quickstart-hermes.mdx | 12 +- .../quickstart-langchain-deepagents-code.mdx | 12 +- docs/get-started/quickstart.mdx | 14 +- docs/inference/choose-inference-provider.mdx | 12 +- .../choose-local-inference-server.mdx | 14 +- docs/inference/set-up-vllm.mdx | 19 +-- docs/reference/commands.mdx | 4 +- docs/reference/platform-support.mdx | 6 +- docs/reference/system-readiness.mdx | 6 +- docs/reference/troubleshooting.mdx | 2 +- docs/resources/starter-prompt.md | 18 +- docs/security/best-practices.mdx | 9 +- scripts/install.sh | 12 +- src/lib/inference/serving/resolver.ts | 2 +- .../onboard/fatal-runtime-preflight.test.ts | 47 +----- src/lib/onboard/fatal-runtime-preflight.ts | 17 +- .../provider-selection-keys.ts | 92 ----------- .../machine/handlers/preflight.test.ts | 61 +------ src/lib/onboard/machine/handlers/preflight.ts | 14 +- src/lib/onboard/provider-recovery.ts | 16 +- src/lib/onboard/providers.ts | 42 ++++- src/lib/onboard/vllm-menu.test.ts | 2 +- src/lib/readiness/onboard-admission.test.ts | 56 +------ src/lib/readiness/onboard-admission.ts | 15 +- .../installer-express-prompt-pty-harness.ts | 61 +------ test/helpers/installer-readiness-stubs.ts | 1 - test/install/install-express-n1x.test.ts | 52 +----- .../installer-host-preflight.test.ts | 155 +----------------- 29 files changed, 163 insertions(+), 618 deletions(-) delete mode 100644 src/lib/onboard/inference-providers/provider-selection-keys.ts diff --git a/ci/platform-matrix.json b/ci/platform-matrix.json index 76ad45fefa5..251437d6708 100644 --- a/ci/platform-matrix.json +++ b/ci/platform-matrix.json @@ -1,7 +1,7 @@ { "$comment": "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\nSPDX-License-Identifier: Apache-2.0\n\nSingle source of truth for NemoClaw launch claims and platform support. Covers platforms, inference providers, supported agents, messaging integrations, and deployment paths. Scripts read this to generate README and docs tables. QA/CI update platform/provider rows; the engineering owner reviews other rows. Docs are derived.", "version": "1.1", - "updated": "2026-09-04", + "updated": "2026-08-14", "project_status": { "stage": "alpha", @@ -74,7 +74,7 @@ "status": "deferred", "ci_tested": false, "prerequisites_notes": "N1x Express remains Deferred until a physical NemoClaw Express E2E test passes. Host identity requires Linux `arm64`, a trusted `/etc/fastos-release` marker with `NAME=\"N1x FASTOS\"`, and NVIDIA vendor ID `10de` on a display-class PCI device. CUDA and Container Device Interface (CDI) readiness are still required.", - "notes": "The accepted Deferred N1x Express preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. It does not activate DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Host identity requires Linux `arm64`, a regular `/etc/fastos-release` file of 1 through 4,096 bytes that is owned by UID 0 and GID 0, is not a symbolic link, is not group- or world-writable, and contains exactly one `NAME=\"N1x FASTOS\"` line. It also requires NVIDIA vendor ID `10de` on a display-class PCI device. Generic DMI values are supplemental and FastOS version is not pinned. Physical CUDA and CDI checks passed on one N1x host, but full NemoClaw Express E2E validation and dedicated CI coverage are pending. `host.platform.supported` remains absent. After identity qualifies, accepting Express can waive only the pending-validation finding. Declining or disabling Express can apply the same waiver when no provider is preselected or the explicit provider is recognized and is not Local NVIDIA NIM. An invalid or Local NVIDIA NIM provider remains blocked. Existing provider requirements still apply, and this waiver does not promote N1x. Do not claim N1x as supported until validation passes and this row is promoted." + "notes": "The accepted Deferred N1x Express preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. It does not activate DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Host identity requires Linux `arm64`, a regular `/etc/fastos-release` file of 1 through 4,096 bytes that is owned by UID 0 and GID 0, is not a symbolic link, is not group- or world-writable, and contains exactly one `NAME=\"N1x FASTOS\"` line. It also requires NVIDIA vendor ID `10de` on a display-class PCI device. Generic DMI values are supplemental and FastOS version is not pinned. Physical CUDA and CDI checks passed on one N1x host, but full NemoClaw Express E2E validation and dedicated CI coverage are pending. `host.platform.supported` remains absent; only explicit managed-vLLM preview intent can waive the pending-validation finding. Do not claim N1x as supported until that validation passes and this row is promoted." }, { "name": "NVIDIA RTX (consumer and Pro workstation GPUs)", @@ -157,13 +157,13 @@ "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 healthy CDI spec. Onboarding evaluates the canonical `host.gpu.nvidia_available`, `host.gpu.container_toolkit_available`, and `host.gpu.cdi_healthy` readiness capabilities before gateway, image, or sandbox lifecycle effects. NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag. Onboarding filters the validated image list by usable memory. In non-interactive mode, onboarding selects the first compatible model unless the operator sets `NEMOCLAW_MODEL`. Model selection uses detected free GPU memory. If free memory is unavailable, NemoClaw uses total GPU memory. On DGX Spark, NemoClaw caps usable memory at 50 percent of total memory to match NIM's reported unified-memory limit. The Nemotron 3 Super catalog minimum includes NIM's reported runtime allocation. Local NVIDIA NIM is unavailable on N1x. NemoClaw omits the provider from onboarding and rejects `NEMOCLAW_PROVIDER=nim-local` on N1x. Select another standard provider or use the Deferred managed-vLLM preview on N1x. On Linux arm64 DGX Spark and DGX Station hosts, onboarding warns that some NIM images may not publish a `linux/arm64` manifest; the warning is advisory, and the selected image pull can still fail when the registry has no matching platform manifest. 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 healthy CDI spec. Onboarding evaluates the canonical `host.gpu.nvidia_available`, `host.gpu.container_toolkit_available`, and `host.gpu.cdi_healthy` readiness capabilities before gateway, image, or sandbox lifecycle effects. NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag. Onboarding filters the validated image list by usable memory. In non-interactive mode, onboarding selects the first compatible model unless the operator sets `NEMOCLAW_MODEL`. Model selection uses detected free GPU memory. If free memory is unavailable, NemoClaw uses total GPU memory. On DGX Spark, NemoClaw caps usable memory at 50 percent of total memory to match NIM's reported unified-memory limit. The Nemotron 3 Super catalog minimum includes NIM's reported runtime allocation. Local NVIDIA NIM is unavailable on N1x. NemoClaw omits the provider from onboarding and rejects `NEMOCLAW_PROVIDER=nim-local` on N1x. Use the Deferred managed-vLLM preview on N1x. On Linux arm64 DGX Spark and DGX Station hosts, onboarding warns that some NIM images may not publish a `linux/arm64` manifest; the warning is advisory, and the selected image pull can still fail when the registry has no matching platform manifest. 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)", "status": "caveated", "endpoint_type": "Local OpenAI-compatible", - "notes": "On a qualified N1x host, this route requires explicit standard-onboarding intent and remains unvalidated. On other hosts, no explicit intent is required. It appears in the onboarding menu when NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. The model is whatever the existing server serves." + "notes": "Unavailable on N1x. On other hosts, it appears in the onboarding menu when NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. No flag is required. The model is whatever the existing server serves." }, { "name": "Local vLLM (managed install/start)", diff --git a/docs/get-started/quickstart-hermes.mdx b/docs/get-started/quickstart-hermes.mdx index 2e4440aa6bf..517eed11b51 100644 --- a/docs/get-started/quickstart-hermes.mdx +++ b/docs/get-started/quickstart-hermes.mdx @@ -61,14 +61,14 @@ Review the [Prerequisites](prerequisites) before you begin. Press Enter to use the recommended express install mode for that platform. This mode applies preset settings and runs the remaining onboarding non-interactively. Express install mode preserves the explicit Hermes selection in the install command. - Enter `n` if you want to select a sandbox name, an inference provider, and a model interactively. - On N1x, the selected provider must meet its existing requirements, and local NVIDIA NIM remains unavailable. + On supported non-N1x express platforms, enter `n` if you want to select a sandbox name, an inference provider, and a model interactively. + On N1x, entering `n` stops installation because the Deferred managed-vLLM preview is the only admitted onboarding path. Refer to [Use a container runtime and supported platforms](#use-a-container-runtime-and-supported-platforms) for more information. The install command creates the `my-hermes` sandbox used in the commands that follow. If you accept Express setup, wait for the installer to finish, then continue with **Confirm the Sandbox Is Ready**; Express selects the provider and model non-interactively. - If the installer does not offer Express setup, or if you enter `n` at the Express prompt, choose an inference provider and model, then provide its credential when prompted. + If the installer does not offer Express setup, or if you enter `n` at the Express prompt on a supported non-N1x host, choose an inference provider and model, then provide its credential when prompted. For that interactive path, skip optional web search and messaging setup on a first run, then accept the suggested network policy tier. With the OpenShell Docker driver, stock Hermes onboarding normally uses the release's exact managed-image digest. If registry or catalog availability prevents resolution, stock onboarding stops before sandbox creation and does not build a shipped Dockerfile. @@ -267,9 +267,9 @@ Use these details when your first-run path needs more control. N1x remains outside the supported-platform set until that validation passes and the platform matrix status is promoted. Press Enter at the express install prompt to apply preset settings, switch the remaining onboarding to non-interactive mode, and select the managed local inference path for that platform. On N1x, accept the preview prompt to apply those settings as explicit Deferred preview intent. - Enter `n` to continue with interactive onboarding when you want to select the agent or other settings yourself. - On N1x, setting `NEMOCLAW_NO_EXPRESS=1` also continues with ordinary onboarding. - Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set another provider to enter standard onboarding directly. + On supported non-N1x express platforms, enter `n` to continue with interactive onboarding when you want to select the agent or other settings yourself. + On N1x, declining the preview or setting only `NEMOCLAW_NO_EXPRESS=1` stops installation before onboarding. + Accept the preview, or set `NEMOCLAW_PROVIDER=install-vllm` before installation to provide the required explicit managed-vLLM intent. The first Hermes start can take several minutes while OpenShell pulls the managed image when it is not already cached. The N1x preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. Refer to [Set Up vLLM](../inference/local-inference/set-up-vllm) for managed model profiles and headless setup. diff --git a/docs/get-started/quickstart-langchain-deepagents-code.mdx b/docs/get-started/quickstart-langchain-deepagents-code.mdx index f37a137590e..2ddbfbef6a6 100644 --- a/docs/get-started/quickstart-langchain-deepagents-code.mdx +++ b/docs/get-started/quickstart-langchain-deepagents-code.mdx @@ -57,14 +57,14 @@ If you use the coding-agent prompt in the preceding section, you can skip this p Press Enter to use the recommended express install mode for that platform. This mode applies preset settings and runs the remaining onboarding non-interactively. Express install mode preserves the explicit Deep Agents selection in the install command. - Enter `n` if you want to select a sandbox name, an inference provider, and a model interactively. - On N1x, the selected provider must meet its existing requirements, and local NVIDIA NIM remains unavailable. + On supported non-N1x express platforms, enter `n` if you want to select a sandbox name, an inference provider, and a model interactively. + On N1x, entering `n` stops installation because the Deferred managed-vLLM preview is the only admitted onboarding path. Refer to [Use a container runtime and supported platforms](#use-a-container-runtime-and-supported-platforms) for more information. The install command creates the `my-deepagents` sandbox used in the commands that follow. If you accept Express setup, wait for the installer to finish, then continue with **Confirm the Sandbox Is Ready**; Express selects the provider and model non-interactively. - If the installer does not offer Express setup, or if you enter `n` at the Express prompt, choose an inference provider and model, then provide its credential when prompted. + If the installer does not offer Express setup, or if you enter `n` at the Express prompt on a supported non-N1x host, choose an inference provider and model, then provide its credential when prompted. For that interactive path, accept the suggested network policy tier on a first run. With the OpenShell Docker driver, stock Deep Agents Code onboarding normally uses the release's exact managed-image digest. If registry or catalog availability prevents resolution, stock onboarding stops before sandbox creation and does not build a shipped Dockerfile. @@ -127,9 +127,9 @@ Use these details when you need more control during setup or after the first san N1x remains outside the supported-platform set until that validation passes and the platform matrix status is promoted. Press Enter at the express install prompt to apply preset settings, switch the remaining onboarding to non-interactive mode, and select the managed local inference path for that platform. On N1x, accept the preview prompt to apply those settings as explicit Deferred preview intent. - Enter `n` to continue with interactive onboarding when you want to select the agent or other settings yourself. - On N1x, setting `NEMOCLAW_NO_EXPRESS=1` also continues with ordinary onboarding. - Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set another provider to enter standard onboarding directly. + On supported non-N1x express platforms, enter `n` to continue with interactive onboarding when you want to select the agent or other settings yourself. + On N1x, declining the preview or setting only `NEMOCLAW_NO_EXPRESS=1` stops installation before onboarding. + Accept the preview, or set `NEMOCLAW_PROVIDER=install-vllm` before installation to provide the required explicit managed-vLLM intent. The N1x preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. diff --git a/docs/get-started/quickstart.mdx b/docs/get-started/quickstart.mdx index 292662074a0..1dde40622a4 100644 --- a/docs/get-started/quickstart.mdx +++ b/docs/get-started/quickstart.mdx @@ -61,9 +61,9 @@ Review the [Prerequisites](prerequisites) before you begin. Press Enter to use the recommended preset settings on a qualified platform or to explicitly opt into the Deferred N1x preview. Express install mode completes the remaining onboarding non-interactively. If you accept, wait for the installer to finish, then continue with **Confirm the Sandbox Is Ready**. - On N1x, enter `n` to continue with the interactive onboarding flow described in this step. - N1x remains Deferred, and the provider that you select must meet its existing requirements. - Local NVIDIA NIM remains unavailable on N1x. + On N1x, entering `n` stops installation because the Deferred managed-vLLM preview is the only admitted onboarding path. + Re-run and accept the preview, or set `NEMOCLAW_PROVIDER=install-vllm` before installation. + On the other express platforms, enter `n` to continue with the interactive onboarding flow described in this step. @@ -197,14 +197,14 @@ Use these details when your first-run path needs more control. After you press Enter at the express install prompt, or explicitly accept the Deferred N1x preview, the installer switches the remaining onboarding to non-interactive mode and selects the managed local inference path for that platform. Express install mode uses `my-assistant` unless you set `NEMOCLAW_SANDBOX_NAME`, and it applies the suggested Balanced policy. It can still prompt for `sudo` when host setup needs it. - On any express platform, set `NEMOCLAW_NO_EXPRESS=1` to skip the express prompt, or set `NEMOCLAW_PROVIDER` to choose a provider yourself. + On the other express platforms, set `NEMOCLAW_NO_EXPRESS=1` to skip the express prompt, or set `NEMOCLAW_PROVIDER` to choose a provider yourself. N1x Express selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. N1x detection requires Linux `arm64`, a trusted `/etc/fastos-release` marker with `NAME="N1x FASTOS"`, and NVIDIA vendor ID `10de` on a display-class PCI device. The N1x path does not use DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. - Accepting the N1x prompt supplies the narrow managed-vLLM preview intent required to pass the pending-validation readiness finding. - Declining the N1x prompt or setting `NEMOCLAW_NO_EXPRESS=1` continues with ordinary interactive onboarding. - Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set another provider to enter standard onboarding directly. + Accepting the N1x prompt supplies the narrow preview intent required to pass the pending-validation readiness finding. + Declining the N1x prompt or setting only `NEMOCLAW_NO_EXPRESS=1` stops installation before onboarding; neither path opens interactive provider selection. + Set `NEMOCLAW_PROVIDER=install-vllm` when you need to bypass the N1x preview prompt with the same explicit intent. Refer to [Platform Support](../reference/platform-support) before you rely on this path. Before the Station express confirmation, the installer reports whether the Hugging Face download is authenticated without displaying the token value. diff --git a/docs/inference/choose-inference-provider.mdx b/docs/inference/choose-inference-provider.mdx index d1817dbbbff..8f123ad2836 100644 --- a/docs/inference/choose-inference-provider.mdx +++ b/docs/inference/choose-inference-provider.mdx @@ -13,8 +13,8 @@ Choose a provider based on where the model runs, which API it exposes, and which NemoClaw validates the provider and model before it creates the sandbox. -On native N1x FASTOS, accept the Deferred managed-vLLM preview or enter standard onboarding by declining Express, disabling Express without an excluded provider, or setting a recognized `NEMOCLAW_PROVIDER` value other than Local NVIDIA NIM. -N1x remains Deferred, each standard provider keeps its existing requirements, and local NVIDIA NIM remains unavailable. +On native N1x FASTOS, the Deferred managed-vLLM preview is the only admitted inference route. +Accept the native N1x Express prompt or set `NEMOCLAW_PROVIDER=install-vllm` to provide the required explicit preview intent. On a qualifying Windows WSL N1x host, Windows Express uses managed llama.cpp after Docker Desktop GPU passthrough succeeds. Other Windows WSL hosts retain Windows-host or WSL-local Ollama. @@ -37,8 +37,8 @@ Use this status table to distinguish validated provider integrations from adapte | 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 llama.cpp (already running) | Experimental | Local OpenAI-compatible | Attaches only to an authenticated operator-managed server on loopback port `8081` that passes cooperative native llama.cpp fingerprinting. NemoClaw does not own its server, model, or lifecycle. | | Local llama.cpp (managed DGX Spark) | Experimental | Local OpenAI-compatible | Uses the repository-owned YAML recipe for NVIDIA Nemotron 3 Nano 30B-A3B, exact GGUF verification, and Docker lifecycle on a qualified DGX Spark host. The recipe remains unqualified for agent support. | -| 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 healthy CDI spec. Onboarding evaluates the canonical `host.gpu.nvidia_available`, `host.gpu.container_toolkit_available`, and `host.gpu.cdi_healthy` readiness capabilities before gateway, image, or sandbox lifecycle effects. NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag. Onboarding filters the validated image list by usable memory. In non-interactive mode, onboarding selects the first compatible model unless the operator sets `NEMOCLAW_MODEL`. Model selection uses detected free GPU memory. If free memory is unavailable, NemoClaw uses total GPU memory. On DGX Spark, NemoClaw caps usable memory at 50 percent of total memory to match NIM's reported unified-memory limit. The Nemotron 3 Super catalog minimum includes NIM's reported runtime allocation. Local NVIDIA NIM is unavailable on N1x. NemoClaw omits the provider from onboarding and rejects `NEMOCLAW_PROVIDER=nim-local` on N1x. Select another standard provider or use the Deferred managed-vLLM preview on N1x. On Linux arm64 DGX Spark and DGX Station hosts, onboarding warns that some NIM images may not publish a `linux/arm64` manifest; the warning is advisory, and the selected image pull can still fail when the registry has no matching platform manifest. 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 | On a qualified N1x host, this route requires explicit standard-onboarding intent and remains unvalidated. On other hosts, no explicit intent is required. It appears in the onboarding menu when NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. The model is whatever the existing server serves. | +| 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 healthy CDI spec. Onboarding evaluates the canonical `host.gpu.nvidia_available`, `host.gpu.container_toolkit_available`, and `host.gpu.cdi_healthy` readiness capabilities before gateway, image, or sandbox lifecycle effects. NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag. Onboarding filters the validated image list by usable memory. In non-interactive mode, onboarding selects the first compatible model unless the operator sets `NEMOCLAW_MODEL`. Model selection uses detected free GPU memory. If free memory is unavailable, NemoClaw uses total GPU memory. On DGX Spark, NemoClaw caps usable memory at 50 percent of total memory to match NIM's reported unified-memory limit. The Nemotron 3 Super catalog minimum includes NIM's reported runtime allocation. Local NVIDIA NIM is unavailable on N1x. NemoClaw omits the provider from onboarding and rejects `NEMOCLAW_PROVIDER=nim-local` on N1x. Use the Deferred managed-vLLM preview on N1x. On Linux arm64 DGX Spark and DGX Station hosts, onboarding warns that some NIM images may not publish a `linux/arm64` manifest; the warning is advisory, and the selected image pull can still fail when the registry has no matching platform manifest. 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 | Unavailable on N1x. On other hosts, it appears in the onboarding menu when NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. No flag is required. The model is whatever the existing server serves. | | Local vLLM (managed install/start) | Tested with limitations | Local OpenAI-compatible | Managed install/start requires Docker on `PATH`; attaching an already-running local vLLM server is a separate path. Appears by default on DGX Spark and qualifying DGX Station GB300 hosts. A labeled Deferred preview appears on hosts that match the N1x identity requirements; N1x remains Deferred until a physical NemoClaw Express E2E test passes. DGX Station is Tested with limitations across qualified profiles on one physical DGX Station GB300. Full Station Express end-to-end qualification for the accepted no-OTA DGX OS `7.6.x` profile is pending. Dual-Station configurations are not yet validated, and dedicated CI coverage is not available. For canonical Station qualification and host preparation, see the Additional Setup page for [OpenClaw](/user-guide/openclaw/get-started/additional-setup/dgx-station-preparation), [Hermes](/user-guide/hermes/get-started/additional-setup/dgx-station-preparation), or [Deep Agents](/user-guide/deepagents/get-started/additional-setup/dgx-station-preparation). Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`, NVIDIA Container Toolkit, and CDI. NemoClaw pins runtime images to immutable digests. Station Express defaults to `nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-NVFP4`; `--station-deepseek` selects `deepseek-ai/DeepSeek-V4-Flash`. Direct managed-vLLM defaults are `nvidia/Qwen3.6-35B-A3B-NVFP4` on DGX Spark and the Deferred N1x preview, `deepseek-ai/DeepSeek-V4-Flash` on DGX Station, and `nvidia/NVIDIA-Nemotron-3-Nano-4B-FP8` on generic Linux NVIDIA GPU hosts. Image pulls from `nvcr.io` require NGC registry login. | {/* provider-status:end */} @@ -66,7 +66,7 @@ Local providers run on the host and keep inference traffic on infrastructure tha | Provider | When it appears | Setup guide | |---|---|---| | Local Ollama | Ollama is installed or running on the host. | [Use Ollama](../local-inference/set-up-ollama) | -| Local vLLM already running | NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. Native N1x requires explicit standard-onboarding intent. | [Set Up vLLM](../local-inference/set-up-vllm) | +| Local vLLM already running | On hosts other than native N1x FASTOS, NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. | [Set Up vLLM](../local-inference/set-up-vllm) | | Local vLLM managed install | Docker is available on `PATH`, the host matches a supported GPU profile, and any required opt-in is present. Attaching an already-running local vLLM server is separate. | [Set Up vLLM](../local-inference/set-up-vllm) | | Local llama.cpp already running | You explicitly select an authenticated server on loopback port `8081`. | [Set Up llama.cpp](../local-inference/set-up-llama-cpp) | | Local llama.cpp managed install | A qualified DGX Spark or Windows WSL N1x host satisfies the declarative recipe requirements. | [Set Up llama.cpp](../local-inference/set-up-llama-cpp) | @@ -76,7 +76,7 @@ Local providers run on the host and keep inference traffic on infrastructure tha | Provider | When it appears | Setup guide | |---|---|---| -| Local vLLM already running | NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. Native N1x requires explicit standard-onboarding intent. | [Set Up vLLM](../local-inference/set-up-vllm) | +| Local vLLM already running | On hosts other than native N1x FASTOS, NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. | [Set Up vLLM](../local-inference/set-up-vllm) | | Local vLLM managed install | Docker is available on `PATH`, the host matches a supported GPU profile, and any required opt-in is present. Attaching an already-running local vLLM server is separate. | [Set Up vLLM](../local-inference/set-up-vllm) | | Local llama.cpp already running | You explicitly select an authenticated server on loopback port `8081`. | [Set Up llama.cpp](../local-inference/set-up-llama-cpp) | | Local llama.cpp managed install | A qualified DGX Spark or Windows WSL N1x host satisfies the declarative recipe requirements. | [Set Up llama.cpp](../local-inference/set-up-llama-cpp) | diff --git a/docs/inference/choose-local-inference-server.mdx b/docs/inference/choose-local-inference-server.mdx index b02308402b8..ebd1119a38b 100644 --- a/docs/inference/choose-local-inference-server.mdx +++ b/docs/inference/choose-local-inference-server.mdx @@ -16,8 +16,8 @@ The agent inside the sandbox sends inference traffic to `inference.local`. OpenShell intercepts that traffic and forwards it to the local endpoint configured during onboarding. -On native N1x FASTOS, use the Deferred managed-vLLM preview or enter standard onboarding explicitly. -N1x remains Deferred, each standard provider keeps its existing requirements, and local NVIDIA NIM remains unavailable. +On native N1x FASTOS, use the Deferred managed-vLLM preview. +It is the only admitted local inference route on native N1x FASTOS. A qualifying Windows WSL N1x host instead uses the explicit managed llama.cpp recipe through Windows Express. @@ -27,8 +27,8 @@ A qualifying Windows WSL N1x host instead uses the explicit managed llama.cpp re | Option | When to use it | Availability | Runtime API | |---|---|---|---| | Ollama | You want the default local option and want NemoClaw to install, start, or use Ollama on supported hosts. | Appears when Ollama is installed or running, and the wizard can offer installation on supported hosts. | Ollama through the managed local route. | -| Existing vLLM | You already run vLLM on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. | Appears when NemoClaw detects the server. Native N1x requires explicit standard-onboarding intent, and this route remains unvalidated there. | `/v1/chat/completions`. | -| Managed vLLM | You want NemoClaw to pull an image, download model weights, and manage the server container. | Requires Docker on `PATH`. Appears by default on DGX Spark and DGX Station. Native N1x FASTOS offers this route through a Deferred preview. Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`. Attaching an already-running local vLLM server is separate. | `/v1/chat/completions`. | +| Existing vLLM | You already run vLLM on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. | Unavailable on native N1x FASTOS. On other hosts, it appears when NemoClaw detects the server. | `/v1/chat/completions`. | +| Managed vLLM | You want NemoClaw to pull an image, download model weights, and manage the server container. | Requires Docker on `PATH`. Appears by default on DGX Spark and DGX Station. Native N1x FASTOS offers this route as its only admitted provider through a Deferred preview. Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`. Attaching an already-running local vLLM server is separate. | `/v1/chat/completions`. | | Fixed vLLM profile | You need the fixed catalog-selected vLLM model and serving configuration. | Appears as option 2 in DGX Spark Express. Direct installation can use the dedicated installer flag. The profile does not appear in the provider menu. | `/v1/chat/completions`. | | Existing llama.cpp | You already operate an authenticated llama.cpp server on loopback port `8081`. | Experimental. Always available for explicit selection and attaches only after cooperative fingerprinting succeeds. | `/v1/chat/completions`. | | Managed llama.cpp | You want NemoClaw to acquire a verified GGUF file and manage an authenticated llama.cpp container. | Experimental. Lists compatible profiles on qualified DGX Spark hosts. Qualifying Windows WSL N1x Express hosts use the explicit Qwen 3.6 recipe. | `/v1/chat/completions`. | @@ -38,8 +38,8 @@ A qualifying Windows WSL N1x host instead uses the explicit managed llama.cpp re | Option | When to use it | Availability | Runtime API | |---|---|---|---| -| Existing vLLM | You already run vLLM on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. | Appears when NemoClaw detects the server. Native N1x requires explicit standard-onboarding intent, and this route remains unvalidated there. | `/v1/chat/completions`. | -| Managed vLLM | You want NemoClaw to pull an image, download model weights, and manage the server container. | Requires Docker on `PATH`. Appears by default on DGX Spark and DGX Station. Native N1x FASTOS offers this route through a Deferred preview. Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`. Attaching an already-running local vLLM server is separate. | `/v1/chat/completions`. | +| Existing vLLM | You already run vLLM on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. | Unavailable on native N1x FASTOS. On other hosts, it appears when NemoClaw detects the server. | `/v1/chat/completions`. | +| Managed vLLM | You want NemoClaw to pull an image, download model weights, and manage the server container. | Requires Docker on `PATH`. Appears by default on DGX Spark and DGX Station. Native N1x FASTOS offers this route as its only admitted provider through a Deferred preview. Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`. Attaching an already-running local vLLM server is separate. | `/v1/chat/completions`. | | Fixed vLLM profile | You need the fixed catalog-selected vLLM model and serving configuration. | Appears as option 2 in DGX Spark Express. Direct installation can use the dedicated installer flag. The profile does not appear in the provider menu. | `/v1/chat/completions`. | | Existing llama.cpp | You already operate an authenticated llama.cpp server on loopback port `8081`. | Experimental. Always available for explicit selection and attaches only after cooperative fingerprinting succeeds. | `/v1/chat/completions`. | | Managed llama.cpp | You want NemoClaw to acquire a verified GGUF file and manage an authenticated llama.cpp container. | Experimental. Lists compatible profiles on qualified DGX Spark hosts. Qualifying Windows WSL N1x Express hosts use the explicit Qwen 3.6 recipe. | `/v1/chat/completions`. | @@ -188,7 +188,7 @@ The managed procedure moved to [Set Up llama.cpp](set-up-llama-cpp#install-manag Choose NVIDIA NIM when you want a managed NIM container and your host has a NIM-capable NVIDIA GPU. This path is unavailable on N1x. -Select another standard provider or use the [Deferred managed-vLLM preview](set-up-vllm#use-n1x-express) on N1x. +Use the [Deferred managed-vLLM preview](set-up-vllm#use-n1x-express) on N1x. The path is experimental, requires NGC registry access, and can fail when a selected image does not publish a manifest for the host architecture. Refer to [Set Up NVIDIA NIM](set-up-nvidia-nim). diff --git a/docs/inference/set-up-vllm.mdx b/docs/inference/set-up-vllm.mdx index b7cc66d8cb1..3ee65fd5f78 100644 --- a/docs/inference/set-up-vllm.mdx +++ b/docs/inference/set-up-vllm.mdx @@ -36,8 +36,7 @@ Follow [Set Up vLLM on Two DGX Sparks](set-up-vllm-on-two-dgx-sparks) or [Set Up ## Use an Existing Server -When vLLM is already running on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`, NemoClaw detects it and queries `/v1/models` for the loaded model. -On N1x, decline or disable Express, or set `NEMOCLAW_PROVIDER=vllm`, to supply explicit standard-onboarding intent. +On hosts other than N1x, when vLLM is already running on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`, NemoClaw detects it and queries `/v1/models` for the loaded model. Run onboarding. @@ -47,7 +46,7 @@ $$nemoclaw onboard Select **Local vLLM**. On generic hosts, the entry includes an experimental label. -On N1x, the existing-server route remains unvalidated even after standard-onboarding intent admits it. +On N1x, the readiness gate admits only explicit managed-preview intent, so the user-facing onboarding flow does not offer the existing-server entry. NemoClaw validates the detected endpoint and records the model reported by `/v1/models`. When you request a vLLM serving profile, the detected server must report the profile's served model alias or a safe `root` that matches the profile's declared model. @@ -153,13 +152,12 @@ $$nemoclaw onboard --profile --vllm-gpu-device The single-host managed vLLM runtime is host-global. Destroying a sandbox does not stop it, and the container remains available for other sandboxes. -Later standard onboarding normally reuses a server that is already listening on the configured vLLM port instead of replacing it automatically. +On hosts other than N1x, later onboarding normally reuses a server that is already listening on the configured vLLM port instead of replacing it automatically. When a managed GPU selection accompanies explicit managed-install intent, NemoClaw preserves that intent and reports the running-server conflict. Choose one action: -- To reuse the running server, omit `--vllm-gpu-device` and select **Local vLLM**. - On N1x, decline or disable Express, or set `NEMOCLAW_PROVIDER=vllm`, before onboarding. +- On hosts other than N1x, to reuse the running server, omit `--vllm-gpu-device` and select **Local vLLM**. - To change the GPU or port, inspect the sandbox registry for every gateway-port environment on the host. Run `NEMOCLAW_GATEWAY_PORT= $$nemoclaw list` for port `8080` and for every non-default port represented by a numeric directory under `~/.nemoclaw/gateways/`. Treat every sandbox that reports provider `vllm-local` as a possible consumer because the list output does not distinguish local vLLM from Local NIM. @@ -473,10 +471,7 @@ Generic DMI values, such as `SKU 1` and `JMJWOA-Generic`, do not establish N1x i N1x Express does not activate DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Accepting the preview prompt sets explicit managed-vLLM intent for the narrow pending-validation readiness exception. -Declining the prompt continues with ordinary interactive onboarding. -Setting `NEMOCLAW_NO_EXPRESS=1` also enters ordinary onboarding when no provider is preselected or the explicit provider is recognized and is not Local NVIDIA NIM. -An invalid or Local NVIDIA NIM provider remains blocked. -Set a recognized provider other than Local NVIDIA NIM to enter ordinary onboarding directly. +Declining the prompt or setting only `NEMOCLAW_NO_EXPRESS=1` stops the installer before onboarding; neither path opens interactive provider selection. Set `NEMOCLAW_PROVIDER=install-vllm` when you need to bypass the prompt with the same explicit preview intent. On a host that still meets the N1x identity checks, `rebuild` reuses the recorded route as explicit preview intent only when all of these conditions hold: @@ -550,15 +545,13 @@ If no server is running, it selects the managed install or start entry. If neither local entry is available, it selects NVIDIA Endpoints. On DGX Station and other hosts, an unset provider keeps NVIDIA Endpoints as the automatic default. -Use an already-running server. +On hosts other than N1x, use an already-running server. ```bash NEMOCLAW_PROVIDER=vllm \ $$nemoclaw onboard --non-interactive ``` -On N1x, this explicit provider supplies standard-onboarding intent, but the route remains unvalidated. - Install or start managed vLLM on a supported host, or explicitly enter the Deferred N1x preview. ```bash diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index 5681074af80..ec44c4273e9 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -3599,7 +3599,7 @@ The following variables let you tune onboarding without editing the Dockerfile o | Variable | Format | Effect | | --- | --- | --- | -| `NEMOCLAW_PROVIDER` | provider key (e.g. `build`, `openrouter`, `openai`, `anthropic`, `anthropicCompatible`, `gemini`, `ollama`, `custom`, `vllm`, `nim-local`, `routed`, `hermes-provider`, `llama-cpp`, `install-llama-cpp`, `install-vllm`, `install-ollama`, `install-windows-ollama`, `start-windows-ollama`) | Selects the inference provider during onboarding. The wizard skips the provider menu in both interactive and non-interactive runs when this is set. On a qualified N1x host, `install-vllm` supplies Deferred preview intent, while another provider supplies standard-onboarding intent and retains its existing requirements. Local NVIDIA NIM remains unavailable on N1x. `llama-cpp` selects attachment of an authenticated, operator-managed llama.cpp server on loopback port `8081`. Set `NEMOCLAW_LLAMACPP_LOCAL_TOKEN`; set `NEMOCLAW_MODEL` to the served alias when the server exposes multiple models. `install-llama-cpp` selects the experimental DGX Spark managed path and rejects `NEMOCLAW_MODEL`; use `NEMOCLAW_LLAMACPP_RECIPE` for its declarative selection. If an operator-managed server does not provide consistent native llama.cpp evidence, select `custom`. Aliases: `cloud` → `build`, `open-router` / `openrouterai` → `openrouter`, `nim` → `nim-local`, `hermes` / `nous` / `nous-portal` → `hermes-provider`, `anthropiccompatible` → `anthropicCompatible`. Invalid values fail fast with the list of accepted keys. | +| `NEMOCLAW_PROVIDER` | provider key (e.g. `build`, `openrouter`, `openai`, `anthropic`, `anthropicCompatible`, `gemini`, `ollama`, `custom`, `vllm`, `nim-local`, `routed`, `hermes-provider`, `llama-cpp`, `install-llama-cpp`, `install-vllm`, `install-ollama`, `install-windows-ollama`, `start-windows-ollama`) | Selects the inference provider during onboarding. The wizard skips the provider menu in both interactive and non-interactive runs when this is set. On a qualified N1x host, only `install-vllm` supplies the explicit Deferred preview intent; another provider stops installation before onboarding. `llama-cpp` selects attachment of an authenticated, operator-managed llama.cpp server on loopback port `8081`. Set `NEMOCLAW_LLAMACPP_LOCAL_TOKEN`; set `NEMOCLAW_MODEL` to the served alias when the server exposes multiple models. `install-llama-cpp` selects the experimental DGX Spark managed path and rejects `NEMOCLAW_MODEL`; use `NEMOCLAW_LLAMACPP_RECIPE` for its declarative selection. If an operator-managed server does not provide consistent native llama.cpp evidence, select `custom`. Aliases: `cloud` → `build`, `open-router` / `openrouterai` → `openrouter`, `nim` → `nim-local`, `hermes` / `nous` / `nous-portal` → `hermes-provider`, `anthropiccompatible` → `anthropicCompatible`. Invalid values fail fast with the list of accepted keys. | | `NEMOCLAW_LLAMACPP_RECIPE` | repository-owned managed-inference recipe ID | Selects the managed llama.cpp recipe when `NEMOCLAW_PROVIDER=install-llama-cpp`, including a compatible lower-priority profile. When unset, NemoClaw selects the unique highest-priority compatible automatic profile. An unknown recipe, an ambiguous selection, or a stale or incompatible readiness report fails before image, model, or runtime effects. | | `NEMOCLAW_MODEL` | model ID | Selects an explicit model for a non-interactive onboarding run. NemoClaw preserves it across a detected provider switch, even when it matches the recorded provider's default. When this variable is unset during such a switch, NemoClaw ignores the `NEMOCLAW_PROVIDER_MODEL` compatibility fallback and uses normal provider model selection. | | `NEMOCLAW_TOOL_DISCLOSURE` | `progressive` or `direct` | Selects progressive tool discovery or the prior direct-exposure behavior outside the Portable experimental profile. Defaults to `progressive`; `--tool-disclosure` takes precedence when both are set. Fresh Portable onboarding ignores this variable and defaults to `direct` unless the flag explicitly selects a mode. | @@ -3820,7 +3820,7 @@ The following flags toggle optional behaviors during onboarding. Set them before | `NEMOCLAW_YES` | `1` to enable | Auto-accepts confirmation prompts (`--yes` equivalent) including in helpers like the Ollama proxy auth setup, but does not change managed-vLLM storage-warning handling. Express and other non-interactive setup stop after a verified insufficient-capacity warning, interactive setup still requires an explicit `y` or `yes`, and an inconclusive model-cache check stops non-interactive setup with guidance to rerun interactively. | | `NEMOCLAW_OLLAMA_NO_AUTOSTART` | `1` to enable | Skips the wizard's eager Ollama auto-start during inference-provider selection (equivalent to passing `--no-ollama-autostart`). When set and Ollama is not running on `localhost:11434`, an agent that uses the legacy `16384`-token context floor, currently OpenClaw, prints a warning and selects the default fallback model instead of spawning `ollama serve`. An agent that requires a larger verified runtime context, currently Hermes at `64000` tokens, returns to interactive provider selection or exits when the Ollama provider is pinned or onboarding is non-interactive. The flag covers only the provider-selection step; later setup steps (auth proxy, validation, model warm) still expect a reachable Ollama. On Linux hosts with a systemd Ollama unit, the loopback-override path may still restart the daemon before this gate runs. | | `NEMOCLAW_NON_INTERACTIVE_SUDO_MODE` | `prompt` or empty/unset | When set to `prompt`, allows non-interactive onboarding to use prompt-capable `sudo` for host setup steps that require elevation, which can ask for a password. Empty/unset is the default and uses `sudo -n`, which fails instead of asking for a password. Any other value is rejected. | -| `NEMOCLAW_NO_EXPRESS` | `1` to enable | Installer-only. Skips the express prompt, including the Deferred N1x preview, then continues with normal onboarding. On a qualified N1x host, this value supplies explicit standard-onboarding intent. Combine it with `NEMOCLAW_PROVIDER=install-vllm` only when you intend to bypass the preview prompt with Deferred managed-vLLM intent. | +| `NEMOCLAW_NO_EXPRESS` | `1` to enable | Installer-only. Skips the DGX Spark, DGX Station, and Windows WSL express prompt, then continues with the normal interactive onboarding flow. On N1x, setting this variable alone stops installation before onboarding. Combine it with `NEMOCLAW_PROVIDER=install-vllm` only when you intend to bypass the preview prompt with explicit Deferred managed-vLLM intent. | | `NEMOCLAW_EXPERIMENTAL` | `1` to enable | Surfaces experimental providers and flows in onboarding. | | `NEMOCLAW_IGNORE_RUNTIME_RESOURCES` | `1` to enable | Suppresses the under-provisioned runtime warning during preflight. Use only when you know the sandbox host meets the minimums. | | `NEMOCLAW_DISABLE_OVERLAY_FIX` | `1` to enable | Skips the Docker overlay-fix step during sandbox build. For environments where the fix is incompatible. | diff --git a/docs/reference/platform-support.mdx b/docs/reference/platform-support.mdx index 94fa5ee0e28..e88076608b4 100644 --- a/docs/reference/platform-support.mdx +++ b/docs/reference/platform-support.mdx @@ -82,7 +82,7 @@ For install requirements and the shorter setup-oriented platform view, refer to | DGX OS (Station) | Docker | Tested with limitations | P1 | No | The PRD marks this platform as P1. Physical validation on one DGX Station GB300 covers generic Ubuntu 24.04 ARM64, stock DGX OS `7.5.0`, the April 2026 NVIDIA Colossus BaseOS profile, and the June 2026 NVIDIA AI Developer Tools profile. A physical no-OTA DGX OS `7.6.0` host provided the release and hardware profile used for its stable workstation-family classifier and passed read-only eligibility and runtime-command preflight. Full Station Express end-to-end qualification for the accepted no-OTA DGX OS `7.6.x` profile is pending. The profile remains subject to the same physical GB300, driver, ECC, Docker, CDI, and container GPU validation. Clean-host end-to-end validation passed on generic Ubuntu and Colossus BaseOS; stock DGX OS and AI Developer Tools completed Station Express validation. The DGX OS `7.5.0` run used released OpenShell `0.0.85`, local Nemotron Ultra serving, sandbox `cuInit(0)`, and a Hermes write/read file-tool task. A dual-Station configuration has not been validated, and dedicated CI coverage is not available. Direct-GPU policies expose only the exact read-only BDF directory for each discovered display-class PCI device with NVIDIA vendor ID (`0x10de`) and GB300 device ID (`0x31c2` or `0x31c3`) plus required existing topology and module paths; they do not expose `/sys`, the PCI parent subtree, or sysfs write access. During physical validation, reads of `/sys/fs/cgroup/cgroup.controllers` and `/sys/class/net/lo/address` remained denied. For canonical hardware qualification, image requirements, preparation, repair limits, reboot handoff, and the explicit temporary metadata override, see [Prepare DGX Station to Install NemoClaw](../get-started/additional-setup/dgx-station-preparation). | | Linux | Docker | Tested | P0 | Yes | Primary tested path. Ubuntu 24.04 has host-level onboarding validation. A digest-pinned Ubuntu 26.04 userspace lane builds the CLI and runs preflight, installer, and platform contracts on eligible main pushes; Docker-host, AppArmor, Landlock, and live onboarding validation on 26.04 remain pending. Other distros (Ubuntu 22.04, Fedora, Rocky, Alma, NixOS, Arch) may work but are not validated. | | macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | P0 | Yes | Start the container runtime (Colima or Docker Desktop) before installing; NemoClaw verifies the pinned official OpenShell formula and grants formula-scoped trust only around each Homebrew install, inspection, start, or stop operation, requires a legacy or changed formula to be repaired by rerunning the pinned installer, uses the standalone gateway only when Homebrew is absent or both the staged formula and installed keg are absent, requires Homebrew Colima users to install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work, and recommends Xcode Command Line Tools (`xcode-select --install`) for Node native modules. | -| N1x FASTOS | Docker | Deferred | Unset | No | The accepted Deferred N1x Express preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. It does not activate DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Host identity requires Linux `arm64`, a regular `/etc/fastos-release` file of 1 through 4,096 bytes that is owned by UID 0 and GID 0, is not a symbolic link, is not group- or world-writable, and contains exactly one `NAME="N1x FASTOS"` line. It also requires NVIDIA vendor ID `10de` on a display-class PCI device. Generic DMI values are supplemental and FastOS version is not pinned. Physical CUDA and CDI checks passed on one N1x host, but full NemoClaw Express E2E validation and dedicated CI coverage are pending. `host.platform.supported` remains absent. After identity qualifies, accepting Express can waive only the pending-validation finding. Declining or disabling Express can apply the same waiver when no provider is preselected or the explicit provider is recognized and is not Local NVIDIA NIM. An invalid or Local NVIDIA NIM provider remains blocked. Existing provider requirements still apply, and this waiver does not promote N1x. Do not claim N1x as supported until validation passes and this row is promoted. | +| N1x FASTOS | Docker | Deferred | Unset | No | The accepted Deferred N1x Express preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. It does not activate DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Host identity requires Linux `arm64`, a regular `/etc/fastos-release` file of 1 through 4,096 bytes that is owned by UID 0 and GID 0, is not a symbolic link, is not group- or world-writable, and contains exactly one `NAME="N1x FASTOS"` line. It also requires NVIDIA vendor ID `10de` on a display-class PCI device. Generic DMI values are supplemental and FastOS version is not pinned. Physical CUDA and CDI checks passed on one N1x host, but full NemoClaw Express E2E validation and dedicated CI coverage are pending. `host.platform.supported` remains absent; only explicit managed-vLLM preview intent can waive the pending-validation finding. Do not claim N1x as supported until that validation passes and this row is promoted. | | NVIDIA RTX (consumer and Pro workstation GPUs) | Docker | Deferred | P1 | No | The PRD marks this platform as P1. Covers RTX consumer cards and RTX Pro workstation cards on Linux hosts that meet the generic-Linux-GPU requirements (NVIDIA Container Toolkit + CDI present). The provider menu emits managed vLLM behind `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm` for this host class today; the end-to-end onboard path on this hardware is not yet validated in CI. | | Windows WSL2 | Docker Desktop (WSL backend) | Tested with limitations | P1 | No | Requires WSL2 with Docker Desktop backend. Qualifying Windows WSL N1x hosts can use the explicit Experimental managed llama.cpp Qwen 3.6 recipe after local-default Docker authority, N1x product identity, ARM64, 48,000 MiB Docker and GPU memory, driver, NVIDIA integration, and Docker Desktop GPU-passthrough checks pass. Other Windows WSL hosts retain Windows-host or WSL-local Ollama. Dedicated N1x WSL CI and physical end-to-end validation are pending. | {/* platform-matrix-full:end */} @@ -105,8 +105,8 @@ NemoClaw routes inference through the OpenShell gateway. Each row below is a pro | 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 llama.cpp (already running) | Experimental | Local OpenAI-compatible | Attaches only to an authenticated operator-managed server on loopback port `8081` that passes cooperative native llama.cpp fingerprinting. NemoClaw does not own its server, model, or lifecycle. | | Local llama.cpp (managed DGX Spark) | Experimental | Local OpenAI-compatible | Uses the repository-owned YAML recipe for NVIDIA Nemotron 3 Nano 30B-A3B, exact GGUF verification, and Docker lifecycle on a qualified DGX Spark host. The recipe remains unqualified for agent support. | -| 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 healthy CDI spec. Onboarding evaluates the canonical `host.gpu.nvidia_available`, `host.gpu.container_toolkit_available`, and `host.gpu.cdi_healthy` readiness capabilities before gateway, image, or sandbox lifecycle effects. NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag. Onboarding filters the validated image list by usable memory. In non-interactive mode, onboarding selects the first compatible model unless the operator sets `NEMOCLAW_MODEL`. Model selection uses detected free GPU memory. If free memory is unavailable, NemoClaw uses total GPU memory. On DGX Spark, NemoClaw caps usable memory at 50 percent of total memory to match NIM's reported unified-memory limit. The Nemotron 3 Super catalog minimum includes NIM's reported runtime allocation. Local NVIDIA NIM is unavailable on N1x. NemoClaw omits the provider from onboarding and rejects `NEMOCLAW_PROVIDER=nim-local` on N1x. Select another standard provider or use the Deferred managed-vLLM preview on N1x. On Linux arm64 DGX Spark and DGX Station hosts, onboarding warns that some NIM images may not publish a `linux/arm64` manifest; the warning is advisory, and the selected image pull can still fail when the registry has no matching platform manifest. 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 | On a qualified N1x host, this route requires explicit standard-onboarding intent and remains unvalidated. On other hosts, no explicit intent is required. It appears in the onboarding menu when NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. The model is whatever the existing server serves. | +| 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 healthy CDI spec. Onboarding evaluates the canonical `host.gpu.nvidia_available`, `host.gpu.container_toolkit_available`, and `host.gpu.cdi_healthy` readiness capabilities before gateway, image, or sandbox lifecycle effects. NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag. Onboarding filters the validated image list by usable memory. In non-interactive mode, onboarding selects the first compatible model unless the operator sets `NEMOCLAW_MODEL`. Model selection uses detected free GPU memory. If free memory is unavailable, NemoClaw uses total GPU memory. On DGX Spark, NemoClaw caps usable memory at 50 percent of total memory to match NIM's reported unified-memory limit. The Nemotron 3 Super catalog minimum includes NIM's reported runtime allocation. Local NVIDIA NIM is unavailable on N1x. NemoClaw omits the provider from onboarding and rejects `NEMOCLAW_PROVIDER=nim-local` on N1x. Use the Deferred managed-vLLM preview on N1x. On Linux arm64 DGX Spark and DGX Station hosts, onboarding warns that some NIM images may not publish a `linux/arm64` manifest; the warning is advisory, and the selected image pull can still fail when the registry has no matching platform manifest. 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 | Unavailable on N1x. On other hosts, it appears in the onboarding menu when NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. No flag is required. The model is whatever the existing server serves. | | Local vLLM (managed install/start) | Tested with limitations | Local OpenAI-compatible | Managed install/start requires Docker on `PATH`; attaching an already-running local vLLM server is a separate path. Appears by default on DGX Spark and qualifying DGX Station GB300 hosts. A labeled Deferred preview appears on hosts that match the N1x identity requirements; N1x remains Deferred until a physical NemoClaw Express E2E test passes. DGX Station is Tested with limitations across qualified profiles on one physical DGX Station GB300. Full Station Express end-to-end qualification for the accepted no-OTA DGX OS `7.6.x` profile is pending. Dual-Station configurations are not yet validated, and dedicated CI coverage is not available. For canonical Station qualification and host preparation, see the Additional Setup page for [OpenClaw](/user-guide/openclaw/get-started/additional-setup/dgx-station-preparation), [Hermes](/user-guide/hermes/get-started/additional-setup/dgx-station-preparation), or [Deep Agents](/user-guide/deepagents/get-started/additional-setup/dgx-station-preparation). Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`, NVIDIA Container Toolkit, and CDI. NemoClaw pins runtime images to immutable digests. Station Express defaults to `nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-NVFP4`; `--station-deepseek` selects `deepseek-ai/DeepSeek-V4-Flash`. Direct managed-vLLM defaults are `nvidia/Qwen3.6-35B-A3B-NVFP4` on DGX Spark and the Deferred N1x preview, `deepseek-ai/DeepSeek-V4-Flash` on DGX Station, and `nvidia/NVIDIA-Nemotron-3-Nano-4B-FP8` on generic Linux NVIDIA GPU hosts. Image pulls from `nvcr.io` require NGC registry login. | {/* provider-status-full:end */} diff --git a/docs/reference/system-readiness.mdx b/docs/reference/system-readiness.mdx index 94c9be4238f..d9cc8654bb7 100644 --- a/docs/reference/system-readiness.mdx +++ b/docs/reference/system-readiness.mdx @@ -177,14 +177,12 @@ Generic DMI values do not satisfy either requirement. A matching identity produces a `qualified` `host.platform.n1x` qualification, but `host.platform.supported` remains `absent` while N1x is Deferred. The blocking `host.platform.n1x_validation_pending` finding records the remaining physical NemoClaw Express E2E gate. -After `host.platform.n1x` is `present`, accepting Express lets onboarding waive only that finding. -Declining or disabling Express can apply the same waiver when no provider is preselected or the explicit provider is recognized and is not Local NVIDIA NIM. -An invalid or Local NVIDIA NIM provider remains blocked. +Explicit `NEMOCLAW_PROVIDER=install-vllm` intent, including acceptance of the installer N1x preview prompt, lets onboarding waive only that finding after `host.platform.n1x` is `present`. The read-only `host probe` report remains `incompatible` and does not apply that onboarding exception. Invalid marker metadata, invalid or ambiguous contents, the wrong PCI identity, the wrong architecture, or an unavailable NVIDIA GPU produces an `unqualified` qualification and the blocking `host.platform.n1x_unqualified` finding. Unreadable marker or PCI evidence produces an `unknown` qualification and the blocking `host.platform.n1x_inconclusive` finding. -Neither finding can be waived by onboarding intent. +Neither finding can be waived by managed-vLLM intent. Bounded evidence appears under `host.platform.identity` with `product`, `nvidiaPlatform`, `n1xCandidate`, `n1xFastOsMarker`, and `n1xPciGpu` fields. ## Interpret NVIDIA GPU Integration diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index e2edd2e04b3..5e7412a4e30 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -2040,7 +2040,7 @@ Ollama can serve local chat and some simple tool surfaces, but agent loops with | Long system prompts or sender metadata | Risky | Yes | | Multi-turn tool dispatch | Risky | Yes | -[Set up vLLM](../inference/local-inference/set-up-vllm) with automatic tool choice and the tool-call parser that matches the model family for persistent agent use. After the parser-aware server is ready, re-run onboarding. Select the **Local vLLM** entry for a server detected on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`; for another address, select **Other OpenAI-compatible endpoint**. On generic hosts, the Local vLLM entry includes an experimental label; on DGX Spark or DGX Station, it does not. On N1x, decline or disable Express, or set `NEMOCLAW_PROVIDER=vllm`, to enter standard onboarding; this route remains unvalidated on N1x. +On hosts other than N1x, [set up vLLM](../inference/local-inference/set-up-vllm) with automatic tool choice and the tool-call parser that matches the model family for persistent agent use. After the parser-aware server is ready, re-run onboarding. Select the **Local vLLM** entry marked **running (suggested)** for a server detected on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`; for another address, select **Other OpenAI-compatible endpoint**. On generic hosts, the Local vLLM entry includes an experimental label; on DGX Spark or DGX Station, it does not. On N1x, stop any server that occupies `${NEMOCLAW_VLLM_PORT:-8000}`, then use only the Deferred managed-vLLM preview; existing and compatible servers are not admitted. Do not rely on direct edits to `openclaw.json` for a persistent provider change. NemoClaw-managed rebuilds can overwrite those edits, while onboarding keeps the sandbox image, OpenShell route, and host-managed credentials aligned. diff --git a/docs/resources/starter-prompt.md b/docs/resources/starter-prompt.md index 5de05610ffa..e0c0c9c4adb 100644 --- a/docs/resources/starter-prompt.md +++ b/docs/resources/starter-prompt.md @@ -89,17 +89,17 @@ Do not load a platform asset for any other computer. ## Runtime and Provider Selection If the Windows WSL platform asset applies, follow that asset's provider selection and skip the native-N1x branch below. -If the readiness check confirms native N1x (not Windows WSL), offer the Deferred managed-vLLM preview before the generic provider menu. -If the user accepts, set `NEMOCLAW_PROVIDER=install-vllm`. +If the readiness check confirms native N1x (not Windows WSL), do not show the generic provider menu. +Offer only the Deferred managed-vLLM preview with `NEMOCLAW_PROVIDER=install-vllm`. Explain that N1x remains outside the supported-platform set pending complete physical NemoClaw Express E2E validation, accepting this path is explicit preview intent, and the preview uses one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. -If the user declines, continue to the provider question below and explain that each provider retains its existing requirements. -Do not offer local NVIDIA NIM on native N1x. -If the user selects managed vLLM and the configured port, `${NEMOCLAW_VLLM_PORT:-8000}`, is occupied, stop and ask the user to stop that server before trying the Deferred preview again. +Do not offer or reuse an existing vLLM server on N1x. +If the configured vLLM port, `${NEMOCLAW_VLLM_PORT:-8000}`, is occupied, stop and ask the user to stop that server before trying the Deferred preview again. +If the user declines the preview, stop before installation. -If no platform asset applies or its offered install path is declined, ask: "Which inference runtime or provider would you like?" +For a computer other than native N1x, if no platform asset applies or its offered install path is declined, ask: "Which inference runtime or provider would you like?" Choices: -1. Existing vLLM, when a ready server is detected on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`; native N1x requires explicit standard-onboarding intent. +1. Existing vLLM, only when the computer is not N1x and a ready server is detected on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. 2. Managed vLLM, optimized local inference with a large download. 3. Local Ollama, only when the selected agent and platform support it. 4. NVIDIA Endpoints, which requires an NVIDIA API key. @@ -191,8 +191,8 @@ Use this provider mapping for non-interactive setup: - OpenAI-compatible: `NEMOCLAW_PROVIDER=custom`, endpoint, model, `COMPATIBLE_API_KEY`. - Anthropic-compatible: `NEMOCLAW_PROVIDER=anthropicCompatible`, endpoint, model, `COMPATIBLE_ANTHROPIC_API_KEY`. - Ollama: `NEMOCLAW_PROVIDER=ollama`, optional `NEMOCLAW_MODEL`. -- Existing vLLM: `NEMOCLAW_PROVIDER=vllm`; on native N1x, this value supplies explicit standard-onboarding intent, but the route remains unvalidated. -- Managed vLLM: `NEMOCLAW_PROVIDER=install-vllm`; on native N1x, this value supplies explicit Deferred preview intent. Qualifying N1x WSL hosts instead follow the Windows WSL asset's managed llama.cpp path. Use an approved optional model override only when the selected platform supports it. +- Existing vLLM: `NEMOCLAW_PROVIDER=vllm`; unavailable on native N1x. +- Managed vLLM: `NEMOCLAW_PROVIDER=install-vllm`; this is the only admitted native-N1x provider and requires explicit Deferred preview intent there. Qualifying N1x WSL hosts instead follow the Windows WSL asset's managed llama.cpp path. Use an approved optional model override only when the selected platform supports it. Do not offer Hermes Provider for OpenClaw or Deep Agents. diff --git a/docs/security/best-practices.mdx b/docs/security/best-practices.mdx index c99295dc16b..6bf2cb90e9b 100644 --- a/docs/security/best-practices.mdx +++ b/docs/security/best-practices.mdx @@ -331,20 +331,19 @@ Review the data policies of any cloud provider you use. The `NEMOCLAW_EXPERIMENTAL=1` environment variable gates local NVIDIA NIM on eligible hosts other than native N1x FASTOS and generic Linux managed vLLM install/start. Managed vLLM install/start is Docker-backed and requires Docker on `PATH`; attaching an already-running local vLLM server is separate. DGX Spark and DGX Station managed vLLM entries appear by default when Docker is available. -Native N1x FASTOS omits local NVIDIA NIM and offers the Deferred managed-vLLM preview or explicitly selected standard onboarding. +Native N1x FASTOS omits local NVIDIA NIM and offers only the Deferred managed-vLLM preview. Native N1x FASTOS remains outside the supported-platform set because its physical NemoClaw Express E2E validation is incomplete. -After NemoClaw qualifies the native N1x identity, you must provide explicit Express or standard-onboarding intent. +After NemoClaw qualifies the native N1x identity, you must provide explicit managed-vLLM preview intent before onboarding can use this path. A qualifying Windows WSL N1x host uses managed llama.cpp through Windows Express. The installer uses local Docker Desktop, Arm64, the Windows N1x product identity, and at least 48,000 MiB of GPU memory as preliminary selection conditions. Onboarding also requires the default local Docker context, at least 48,000 MiB of Docker memory, driver version `580.65.06` or later, Docker storage and runtime readiness, NVIDIA GPU integration, and a successful Docker Desktop GPU passthrough proof. Onboarding stops managed selection when any required readiness check fails. Other Windows WSL hosts retain Windows-host or WSL-local Ollama. -An already-running vLLM server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}` appears in standard onboarding because selecting it is an explicit user action. -Native N1x requires you to decline or disable Express or set `NEMOCLAW_PROVIDER=vllm` first. +On hosts other than native N1x FASTOS, an already-running vLLM server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}` also appears in the menu without a flag because selecting it is an explicit user action. | Aspect | Detail | |---|---| -| Default | Local NVIDIA NIM and generic Linux managed vLLM install/start are hidden. When Docker is available, DGX Spark and DGX Station managed vLLM entries are offered by default. Standard onboarding offers already-running vLLM on `localhost:${NEMOCLAW_VLLM_PORT:-8000}` when detected; native N1x requires explicit standard-onboarding intent. Qualifying Windows WSL N1x Express runs use managed llama.cpp. | +| Default | Local NVIDIA NIM and generic Linux managed vLLM install/start are hidden. When Docker is available, DGX Spark and DGX Station managed vLLM entries are offered by default. On hosts other than native N1x FASTOS, already-running vLLM on `localhost:${NEMOCLAW_VLLM_PORT:-8000}` is offered when detected. Qualifying Windows WSL N1x Express runs use managed llama.cpp. | | What you can change | On eligible hosts other than native N1x FASTOS, set `NEMOCLAW_EXPERIMENTAL=1` before onboarding to surface Local NIM and, when Docker is available on `PATH`, generic Linux managed vLLM. To request only the Docker-backed managed vLLM path non-interactively, set `NEMOCLAW_PROVIDER=install-vllm`. | | Risk if selected | NemoClaw has not fully validated these providers. NIM requires a NIM-capable GPU. The managed vLLM path requires Docker, pulls a container image, and starts it on a supported NVIDIA GPU host. Misconfiguration can cause failed inference or unexpected behavior. | | Recommendation | Use experimental providers only for evaluation. Do not rely on them for always-on assistants. | diff --git a/scripts/install.sh b/scripts/install.sh index 0ab576a774c..566361503fd 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -3683,10 +3683,7 @@ run_installer_host_preflight() { try { const { assessHost, planHostAdvisories } = require(preflightPath); const { createHostReadinessReport } = require(hostReadinessPath); - const { - evaluateOnboardReadinessAdmission, - hasExplicitDeferredN1xOnboardingIntent, - } = require(onboardAdmissionPath); + const { evaluateOnboardReadinessAdmission } = require(onboardAdmissionPath); const { loadGatewayManagementDeclaration } = require(gatewayManagementPath); const { configuredRuntimeProviderOwnsHostReadiness } = require(gatewayRuntimePath); const host = assessHost(); @@ -3721,7 +3718,9 @@ run_installer_host_preflight() { // authoritative onboarding gate apply supported storage remediation, // but only when the gateway declaration confirms NemoClaw ownership. allowStorageRemediation, - allowDeferredN1x: hasExplicitDeferredN1xOnboardingIntent(process.env), + allowDeferredN1xManagedVllm: + process.env.NEMOCLAW_PROVIDER === "install-vllm" || + process.env.NEMOCLAW_NO_EXPRESS === "1", }); const infoLines = []; const actionLines = []; @@ -6066,6 +6065,9 @@ maybe_offer_express_install() { return 0 fi if [ -n "${NEMOCLAW_PROVIDER:-}" ]; then + if [ "$platform" = "N1x" ] && [ "$NEMOCLAW_PROVIDER" != "install-vllm" ]; then + export NEMOCLAW_NO_EXPRESS=1 + fi if [ "$platform" = "DGX Station" ] && [ "$NEMOCLAW_PROVIDER" = "install-vllm" ]; then # An explicit managed-vLLM provider selects the same Station host/pair # preparation boundary without forcing the rest of the express policy. diff --git a/src/lib/inference/serving/resolver.ts b/src/lib/inference/serving/resolver.ts index 5123754bfd0..f380de29db9 100644 --- a/src/lib/inference/serving/resolver.ts +++ b/src/lib/inference/serving/resolver.ts @@ -122,7 +122,7 @@ function hasAdmittedReadinessException( explicitlyOptedOutGpuPassthrough: false, allowUnsupportedRuntime: false, allowStorageRemediation: remediableStorage, - allowDeferredN1x: allowDeferredN1xManagedVllm, + allowDeferredN1xManagedVllm, }); if (!admission.admitted || admission.waivedFindingIds.length === 0) return false; const waivedFindingIds = new Set(admission.waivedFindingIds); diff --git a/src/lib/onboard/fatal-runtime-preflight.test.ts b/src/lib/onboard/fatal-runtime-preflight.test.ts index 72025f61b19..1cc8974a8ac 100644 --- a/src/lib/onboard/fatal-runtime-preflight.test.ts +++ b/src/lib/onboard/fatal-runtime-preflight.test.ts @@ -152,7 +152,7 @@ afterEach(() => { }); describe("report-backed runtime readiness (#7411)", () => { - it("admits explicit N1x onboarding intent and lets rebuild reject ambient intent (#11041)", () => { + it("requires explicit N1x intent and lets rebuild reject ambient intent (#9292)", () => { const readiness: SystemReadinessReport = { schemaVersion: "1.1.0", status: "incompatible", @@ -201,12 +201,12 @@ describe("report-backed runtime readiness (#7411)", () => { expect( assertOnboardSystemReadiness(readiness, hostWithRuntime("docker"), { explicitlyOptedOutGpuPassthrough: false, - allowDeferredN1xOnboarding: true, + allowDeferredN1xManagedVllm: true, exitProcess: exit as never, }), ).toBe(readiness); - vi.stubEnv("NEMOCLAW_PROVIDER", "ollama"); + vi.stubEnv("NEMOCLAW_PROVIDER", "install-vllm"); expect( assertOnboardSystemReadiness(readiness, hostWithRuntime("docker"), { explicitlyOptedOutGpuPassthrough: false, @@ -223,49 +223,10 @@ describe("report-backed runtime readiness (#7411)", () => { }), ).toBe(readiness); - vi.stubEnv("NEMOCLAW_PROVIDER", "ollama"); - expect(() => - assertOnboardSystemReadiness(readiness, hostWithRuntime("docker"), { - explicitlyOptedOutGpuPassthrough: false, - allowDeferredN1xOnboarding: false, - exitProcess: exit as never, - }), - ).toThrow("exit"); - - vi.stubEnv("NEMOCLAW_NO_EXPRESS", "0"); - vi.stubEnv("NEMOCLAW_PROVIDER", "unknown-provider"); - expect(() => - assertOnboardSystemReadiness(readiness, hostWithRuntime("docker"), { - explicitlyOptedOutGpuPassthrough: false, - exitProcess: exit as never, - }), - ).toThrow("exit"); - vi.stubEnv("NEMOCLAW_PROVIDER", "nim"); - expect(() => - assertOnboardSystemReadiness(readiness, hostWithRuntime("docker"), { - explicitlyOptedOutGpuPassthrough: false, - exitProcess: exit as never, - }), - ).toThrow("exit"); - vi.stubEnv("NEMOCLAW_PROVIDER", "nim-local"); - expect(() => - assertOnboardSystemReadiness(readiness, hostWithRuntime("docker"), { - explicitlyOptedOutGpuPassthrough: false, - exitProcess: exit as never, - }), - ).toThrow("exit"); - vi.stubEnv("NEMOCLAW_NO_EXPRESS", "1"); - vi.stubEnv("NEMOCLAW_PROVIDER", "nim"); - expect(() => - assertOnboardSystemReadiness(readiness, hostWithRuntime("docker"), { - explicitlyOptedOutGpuPassthrough: false, - exitProcess: exit as never, - }), - ).toThrow("exit"); - vi.stubEnv("NEMOCLAW_PROVIDER", "nim-local"); expect(() => assertOnboardSystemReadiness(readiness, hostWithRuntime("docker"), { explicitlyOptedOutGpuPassthrough: false, + allowDeferredN1xManagedVllm: false, exitProcess: exit as never, }), ).toThrow("exit"); diff --git a/src/lib/onboard/fatal-runtime-preflight.ts b/src/lib/onboard/fatal-runtime-preflight.ts index 9093f7f0d0d..fbe2ada7f58 100644 --- a/src/lib/onboard/fatal-runtime-preflight.ts +++ b/src/lib/onboard/fatal-runtime-preflight.ts @@ -21,7 +21,6 @@ import { import { evaluateOnboardGatewayReadinessAdmission, evaluateOnboardReadinessAdmission, - hasExplicitDeferredN1xOnboardingIntent, } from "../readiness/onboard-admission"; import { composeSystemReadinessReport } from "../readiness/system"; import type { SystemReadinessReport } from "../readiness/types"; @@ -47,6 +46,7 @@ import { validateSandboxGpuPreflight, } from "./sandbox-gpu-preflight"; import type { OnboardOptions } from "./types"; +import { MANAGED_VLLM_PROVIDER_KEY } from "./vllm-menu"; export type FatalRuntimePreflightOptions = Pick< OnboardOptions, @@ -119,8 +119,7 @@ export interface OnboardHostReadinessOptions { resuming?: boolean; allowStorageRemediation?: boolean; allowPortableHostPreparation?: boolean; - /** A trusted current or recorded choice may exercise the Deferred N1x path. */ - allowDeferredN1xOnboarding?: boolean; + allowDeferredN1xManagedVllm?: boolean; /** Print warning-severity host advisories before returning an admitted report. */ presentAdvisories?: boolean; exitProcess?: (code: number) => never; @@ -179,8 +178,10 @@ export function assertOnboardSystemReadiness( providerOwnsHostReadiness: selectedRuntimeOwnsHostReadiness, allowStorageRemediation: options.allowStorageRemediation === true, allowPortableHostPreparation: options.allowPortableHostPreparation, - allowDeferredN1x: - options.allowDeferredN1xOnboarding ?? hasExplicitDeferredN1xOnboardingIntent(process.env), + allowDeferredN1xManagedVllm: + options.allowDeferredN1xManagedVllm ?? + (process.env.NEMOCLAW_PROVIDER === MANAGED_VLLM_PROVIDER_KEY || + process.env.NEMOCLAW_NO_EXPRESS === "1"), }); const advisories = planHostAdvisories(host, { providerOwnsHostReadiness: selectedRuntimeOwnsHostReadiness, @@ -315,7 +316,7 @@ function collectOnboardHostReadiness( wslDockerDesktopGpuProofPassed: runtimeGpu?.wslDockerDesktopGpuProofPassed, resuming: context.resuming, allowStorageRemediation, - allowDeferredN1xOnboarding: options.allowDeferredN1xManagedVllm, + allowDeferredN1xManagedVllm: options.allowDeferredN1xManagedVllm, // The initial host readiness gate already presented warning advisories. presentAdvisories: false, exitProcess: context.exitProcess, @@ -410,7 +411,7 @@ async function collectAdmittedReadinessPair( host.result.sandboxGpuConfig.mode === "0" || options.optedOutGpuPassthrough === true, resuming: context.resuming, allowStorageRemediation: isManagedGatewayReadiness(gateway), - allowDeferredN1xOnboarding: options.allowDeferredN1xManagedVllm, + allowDeferredN1xManagedVllm: options.allowDeferredN1xManagedVllm, presentAdvisories: false, exitProcess, }); @@ -593,7 +594,7 @@ export function runFatalOnboardRuntimePreflight( explicitlyOptedOutGpuPassthrough, resuming: context.resuming, allowStorageRemediation: context.allowStorageRemediation, - allowDeferredN1xOnboarding: options.allowDeferredN1xManagedVllm, + allowDeferredN1xManagedVllm: options.allowDeferredN1xManagedVllm, exitProcess, observedAt, now, diff --git a/src/lib/onboard/inference-providers/provider-selection-keys.ts b/src/lib/onboard/inference-providers/provider-selection-keys.ts deleted file mode 100644 index 25547953128..00000000000 --- a/src/lib/onboard/inference-providers/provider-selection-keys.ts +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -export const NON_INTERACTIVE_PROVIDER_ALIASES: Readonly> = { - cloud: "build", - nim: "nim-local", - vllm: "vllm", - "open-router": "openrouter", - openrouterai: "openrouter", - anthropiccompatible: "anthropicCompatible", - hermes: "hermesProvider", - "hermes-provider": "hermesProvider", - hermesprovider: "hermesProvider", - nous: "hermesProvider", - "nous-portal": "hermesProvider", -}; - -export const NON_INTERACTIVE_PROVIDER_KEYS: ReadonlySet = new Set([ - "build", - "openrouter", - "openai", - "anthropic", - "anthropicCompatible", - "gemini", - "hermesProvider", - "ollama", - "llama-cpp", - "install-llama-cpp", - "custom", - "nim-local", - "vllm", - "routed", - "install-vllm", - "install-ollama", - "install-windows-ollama", - "start-windows-ollama", -]); - -const PERSISTED_PROVIDER_SELECTION_KEYS: Readonly> = { - "nvidia-router": "routed", - "ollama-local": "ollama", - "nvidia-prod": "build", - // This legacy name identifies NVIDIA Endpoints, not Local NVIDIA NIM. - "nvidia-nim": "build", - "openai-api": "openai", - "openrouter-api": "openrouter", - "anthropic-prod": "anthropic", - "compatible-anthropic-endpoint": "anthropicCompatible", - "gemini-api": "gemini", - "compatible-endpoint": "custom", - "llama-cpp-local": "llama-cpp", - "hermes-provider": "hermesProvider", -}; - -export function normalizeNonInteractiveProviderKey( - value: string | null | undefined, -): string | null { - const key = String(value ?? "") - .trim() - .toLowerCase(); - if (!key) return null; - const normalized = NON_INTERACTIVE_PROVIDER_ALIASES[key] ?? key; - return NON_INTERACTIVE_PROVIDER_KEYS.has(normalized) ? normalized : null; -} - -/** True only for a recognized provider route that N1x onboarding permits. */ -export function isN1xOnboardingProviderKey(value: string | null | undefined): boolean { - const normalized = normalizeNonInteractiveProviderKey(value); - return normalized !== null && normalized !== "nim-local"; -} - -/** Resolve the provider representation persisted in an onboarding session. */ -export function persistedProviderNameToSelectionKey( - value: string | null | undefined, - options: { hasNimContainer?: boolean } = {}, -): string | null { - const name = String(value ?? "").trim(); - if (!name) return null; - // Local NIM and standalone vLLM both persist as vllm-local. Only the - // owner-only NIM container record distinguishes the excluded NIM route. - if (name === "vllm-local") return options.hasNimContainer ? "nim-local" : "vllm"; - return PERSISTED_PROVIDER_SELECTION_KEYS[name] ?? normalizeNonInteractiveProviderKey(name); -} - -/** True only for a recognized persisted provider route that N1x permits. */ -export function isN1xOnboardingRecordedProvider( - value: string | null | undefined, - options: { hasNimContainer?: boolean } = {}, -): boolean { - const selection = persistedProviderNameToSelectionKey(value, options); - return selection !== null && selection !== "nim-local"; -} diff --git a/src/lib/onboard/machine/handlers/preflight.test.ts b/src/lib/onboard/machine/handlers/preflight.test.ts index a768ed0929a..de5cf84b4d0 100644 --- a/src/lib/onboard/machine/handlers/preflight.test.ts +++ b/src/lib/onboard/machine/handlers/preflight.test.ts @@ -212,73 +212,16 @@ describe("handlePreflightState", () => { 1, expect.anything(), expect.anything(), - expect.objectContaining({ allowDeferredN1xOnboarding: true }), + expect.objectContaining({ allowDeferredN1xManagedVllm: true }), ); expect(assertOnboardHostReadiness).toHaveBeenNthCalledWith( 2, expect.anything(), expect.anything(), - expect.objectContaining({ allowDeferredN1xOnboarding: true }), + expect.objectContaining({ allowDeferredN1xManagedVllm: true }), ); }); - it("reuses a recorded standard provider for Deferred N1x resume (#11041)", async () => { - const session = createSession({ provider: "ollama-local" }); - session.steps.preflight.status = "complete"; - const assertOnboardHostReadiness = vi.fn(); - const harness = createDeps({ assertOnboardHostReadiness }); - - await handlePreflightState({ ...baseOptions(harness.deps, session), resume: true }); - - expect(assertOnboardHostReadiness).toHaveBeenCalledTimes(2); - expect(assertOnboardHostReadiness).toHaveBeenNthCalledWith( - 1, - expect.anything(), - expect.anything(), - expect.objectContaining({ allowDeferredN1xOnboarding: true }), - ); - expect(assertOnboardHostReadiness).toHaveBeenNthCalledWith( - 2, - expect.anything(), - expect.anything(), - expect.objectContaining({ allowDeferredN1xOnboarding: true }), - ); - }); - - it.each([ - ["persisted Local NIM", "vllm-local", "nemoclaw-nim", undefined, false], - ["an unknown persisted provider", "obsolete-provider", null, undefined, false], - ["legacy NVIDIA Endpoints", "nvidia-nim", null, undefined, true], - ["an explicit rebuild denial", "ollama-local", null, false, false], - ] as const)( - "classifies %s for Deferred N1x resume (#11041)", - async (_case, provider, nimContainer, authority, expected) => { - const session = createSession({ provider, nimContainer }); - session.steps.preflight.status = "complete"; - const assertOnboardHostReadiness = vi.fn(); - const harness = createDeps({ assertOnboardHostReadiness }); - - await handlePreflightState({ - ...baseOptions(harness.deps, session), - resume: true, - ...(authority === undefined ? {} : { allowDeferredN1xManagedVllm: authority }), - }); - - expect(assertOnboardHostReadiness).toHaveBeenNthCalledWith( - 1, - expect.anything(), - expect.anything(), - expect.objectContaining({ allowDeferredN1xOnboarding: expected }), - ); - expect(assertOnboardHostReadiness).toHaveBeenNthCalledWith( - 2, - expect.anything(), - expect.anything(), - expect.objectContaining({ allowDeferredN1xOnboarding: expected }), - ); - }, - ); - it("rejects changed gateway ownership before cached resume probe effects (#7411)", async () => { const session = createSession(); session.steps.preflight.status = "complete"; diff --git a/src/lib/onboard/machine/handlers/preflight.ts b/src/lib/onboard/machine/handlers/preflight.ts index 51669940d54..2b96bd96171 100644 --- a/src/lib/onboard/machine/handlers/preflight.ts +++ b/src/lib/onboard/machine/handlers/preflight.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 import type { Session } from "../../../state/onboard-session"; -import { isN1xOnboardingRecordedProvider } from "../../inference-providers/provider-selection-keys"; import { withPreflightTrace } from "../../tracing"; import { advanceTo, type OnboardStateTransitionResult } from "../result"; @@ -57,7 +56,7 @@ export interface PreflightStateOptions< observedAt?: string; now?: () => Date; wslDockerDesktopGpuProofPassed?: boolean; - allowDeferredN1xOnboarding?: boolean; + allowDeferredN1xManagedVllm?: boolean; resuming: true; presentAdvisories?: boolean; }, @@ -152,13 +151,6 @@ export async function handlePreflightState< : { flag: null, device: null }; const effectiveSandboxGpuFlag = explicitSandboxGpuFlag ?? resumedSandboxGpuOverrides.flag; const effectiveSandboxGpuDevice = sandboxGpuDevice ?? resumedSandboxGpuOverrides.device; - const recordedProviderAllowsDeferredN1x = isN1xOnboardingRecordedProvider(session?.provider, { - hasNimContainer: Boolean(session?.nimContainer), - }); - // An explicit false is authoritative for rebuilds. Ordinary resume may use - // the provider that this owner-only session already validated and recorded. - const allowDeferredN1xOnboarding = - allowDeferredN1xManagedVllm ?? recordedProviderAllowsDeferredN1x; let gpu: Gpu; if (resumePreflight) { @@ -185,7 +177,7 @@ export async function handlePreflightState< explicitlyOptedOutGpuPassthrough: resumeSandboxGpuConfig.mode === "0", observedAt: hostObservedAt, now, - allowDeferredN1xOnboarding, + allowDeferredN1xManagedVllm, resuming: true, }); // A full detector can run the bounded ARM64 WSL Docker GPU proof. Keep it @@ -208,7 +200,7 @@ export async function handlePreflightState< observedAt: hostObservedAt, now, ...(wslDockerDesktopGpuProofPassed === undefined ? {} : { wslDockerDesktopGpuProofPassed }), - allowDeferredN1xOnboarding, + allowDeferredN1xManagedVllm, resuming: true, presentAdvisories: false, }); diff --git a/src/lib/onboard/provider-recovery.ts b/src/lib/onboard/provider-recovery.ts index a08ce84fa66..65136cb00c0 100644 --- a/src/lib/onboard/provider-recovery.ts +++ b/src/lib/onboard/provider-recovery.ts @@ -10,7 +10,6 @@ import { normalizeInferenceEndpointSource, } from "../inference/selection"; import { getLiveGatewayInference } from "../inference/live"; -import { persistedProviderNameToSelectionKey } from "./inference-providers/provider-selection-keys"; export type RemoteProviderConfigEntryLike = { providerName?: string }; @@ -80,8 +79,19 @@ export function providerNameToOptionKey( opts: { hasNimContainer?: boolean } = {}, ): string | null { if (!name) return null; - const builtIn = persistedProviderNameToSelectionKey(name, opts); - if (builtIn) return builtIn; + if (name === "nvidia-router") return "routed"; + if (name === "ollama-local") return "ollama"; + // Local NIM and standalone vLLM both persist as provider="vllm-local". NIM + // is positively identified by a nimContainer record; the absence of one in + // registry/session recovery reliably means standalone vLLM (the standalone + // path never records a container), so default to "vllm" there. Live-gateway + // recovery doesn't carry container info either, but the caller's + // option-availability check still gates on whether vllm is actually running. + if (name === "vllm-local") return opts.hasNimContainer ? "nim-local" : "vllm"; + // `nvidia-nim` is a legacy alias for cloud NVIDIA Endpoints (see + // setupInference: it routes nvidia-nim through REMOTE_PROVIDER_CONFIG.build), + // not a marker for Local NIM. Local NIM persists as vllm-local + nimContainer. + if (name === "nvidia-nim") return "build"; for (const [key, cfg] of Object.entries(remoteProviderConfig)) { if (cfg.providerName === name) return key; } diff --git a/src/lib/onboard/providers.ts b/src/lib/onboard/providers.ts index 9be9b5c093b..a60d00bc1b2 100644 --- a/src/lib/onboard/providers.ts +++ b/src/lib/onboard/providers.ts @@ -32,11 +32,6 @@ const { MESSAGING_CREDENTIAL_PROVIDER_TYPE, } = require("../messaging/provider-profile"); const { ensureWebSearchProviderProfiles } = require("./brave-provider-profile"); -const { - NON_INTERACTIVE_PROVIDER_ALIASES, - NON_INTERACTIVE_PROVIDER_KEYS, - normalizeNonInteractiveProviderKey, -} = require("./inference-providers/provider-selection-keys"); const MESSAGING_PROVIDER_BINDING_CONFLICT = "NEMOCLAW_MESSAGING_PROVIDER_BINDING_CONFLICT"; const MESSAGING_PROVIDER_MUTATION_FAILURE = "NEMOCLAW_MESSAGING_PROVIDER_MUTATION_FAILURE"; @@ -169,6 +164,39 @@ const PROVIDER_MODEL_ENV = "NEMOCLAW_PROVIDER_MODEL"; // provider/namespace/model convention. This endpoint is staged as a custom // OpenAI-compatible provider, not as the public build.nvidia.com provider. const HOSTED_INFERENCE_MODEL = "nvidia/nvidia/nemotron-3-ultra"; +const NON_INTERACTIVE_PROVIDER_ALIASES = { + cloud: "build", + nim: "nim-local", + vllm: "vllm", + "open-router": "openrouter", + openrouterai: "openrouter", + anthropiccompatible: "anthropicCompatible", + hermes: "hermesProvider", + "hermes-provider": "hermesProvider", + hermesprovider: "hermesProvider", + nous: "hermesProvider", + "nous-portal": "hermesProvider", +}; +const NON_INTERACTIVE_PROVIDER_KEYS = new Set([ + "build", + "openrouter", + "openai", + "anthropic", + "anthropicCompatible", + "gemini", + "hermesProvider", + "ollama", + "llama-cpp", + "install-llama-cpp", + "custom", + "nim-local", + "vllm", + "routed", + "install-vllm", + "install-ollama", + "install-windows-ollama", + "start-windows-ollama", +]); const NON_INTERACTIVE_PROVIDER_VALID_VALUES = "Valid values: build, openrouter, openai, anthropic, anthropicCompatible, gemini, hermes-provider, ollama, llama-cpp, install-llama-cpp, custom, nim-local, vllm, routed, install-vllm, install-ollama, install-windows-ollama, start-windows-ollama"; const PROVIDER_KEY_ROUTE_VALUES = new Set( @@ -356,8 +384,8 @@ function getNonInteractiveProvider(allowHostedInferenceStaging = true) { if (allowHostedInferenceStaging) stageHostedInferenceSourceSecretEnv(); const providerKey = (process.env.NEMOCLAW_PROVIDER || "").trim().toLowerCase(); if (!providerKey) return null; - const normalized = normalizeNonInteractiveProviderKey(providerKey); - if (!normalized) { + const normalized = NON_INTERACTIVE_PROVIDER_ALIASES[providerKey] || providerKey; + if (!NON_INTERACTIVE_PROVIDER_KEYS.has(normalized)) { console.error(` Unsupported NEMOCLAW_PROVIDER: ${providerKey}`); console.error(` ${NON_INTERACTIVE_PROVIDER_VALID_VALUES}`); process.exit(1); diff --git a/src/lib/onboard/vllm-menu.test.ts b/src/lib/onboard/vllm-menu.test.ts index 96c5e50743a..c4595a6a78a 100644 --- a/src/lib/onboard/vllm-menu.test.ts +++ b/src/lib/onboard/vllm-menu.test.ts @@ -119,7 +119,7 @@ describe("buildVllmMenuEntries", () => { log: () => {}, }); - // N1x readiness rejects the running-server state without explicit onboarding + // N1x readiness rejects the running-server state without explicit managed // intent. If this lower-level helper sees that state in isolation, it must // not mislabel an operator-managed server as the Deferred managed preview. assert.equal(install[0].label, "Install vLLM (N1x) [Deferred preview]"); diff --git a/src/lib/readiness/onboard-admission.test.ts b/src/lib/readiness/onboard-admission.test.ts index 00a51c1d896..0892a931b61 100644 --- a/src/lib/readiness/onboard-admission.test.ts +++ b/src/lib/readiness/onboard-admission.test.ts @@ -5,7 +5,6 @@ import { describe, expect, it } from "vitest"; import { evaluateOnboardGatewayReadinessAdmission, evaluateOnboardReadinessAdmission, - hasExplicitDeferredN1xOnboardingIntent, ONBOARD_READINESS_ADMISSION_REASON_IDS, ONBOARD_READINESS_FINDING_IDS, ONBOARD_REQUIRED_CAPABILITY_IDS, @@ -102,34 +101,6 @@ function withCapabilityState( } describe("onboarding readiness admission (#7411)", () => { - it.each([ - ["preview decline", { NEMOCLAW_NO_EXPRESS: "1" }, true], - ["explicit non-Express provider", { NEMOCLAW_PROVIDER: "ollama" }, true], - ["normalized provider alias", { NEMOCLAW_PROVIDER: " Open-Router " }, true], - ["explicit managed provider", { NEMOCLAW_PROVIDER: "install-vllm" }, true], - ["unknown provider", { NEMOCLAW_PROVIDER: "unknown-provider" }, false], - ["N1x-excluded provider", { NEMOCLAW_PROVIDER: "nim-local" }, false], - ["N1x-excluded provider alias", { NEMOCLAW_PROVIDER: "nim" }, false], - [ - "disabled Express with N1x-excluded provider", - { NEMOCLAW_NO_EXPRESS: "1", NEMOCLAW_PROVIDER: "nim-local" }, - false, - ], - [ - "disabled Express with N1x-excluded alias", - { NEMOCLAW_NO_EXPRESS: "1", NEMOCLAW_PROVIDER: "nim" }, - false, - ], - ["blank provider", { NEMOCLAW_PROVIDER: " " }, false], - ["disabled Express override", { NEMOCLAW_NO_EXPRESS: "0" }, false], - ["no intent", {}, false], - ] as const)( - "recognizes %s as Deferred N1x onboarding intent (#11041)", - (_scenario, env, expected) => { - expect(hasExplicitDeferredN1xOnboardingIntent(env)).toBe(expected); - }, - ); - it("admits known required capabilities without relying on the report status", () => { expect( evaluateOnboardReadinessAdmission(report({ status: "inconclusive" }), DEFAULT_OPTIONS), @@ -414,7 +385,7 @@ describe("onboarding readiness admission (#7411)", () => { }); }); - it("admits explicit onboarding intent only for a qualified Deferred N1x identity (#11041)", () => { + it("admits only explicit managed-vLLM intent through the Deferred N1x validation gate (#8574)", () => { const capabilities = [ ...withCapabilityState( requiredCapabilities(), @@ -434,38 +405,17 @@ describe("onboarding readiness admission (#7411)", () => { expect( evaluateOnboardReadinessAdmission( report({ capabilities, findings: [pending], status: "incompatible" }), - { ...DEFAULT_OPTIONS, allowDeferredN1x: true }, + { ...DEFAULT_OPTIONS, allowDeferredN1xManagedVllm: true }, ), ).toEqual({ admitted: true, waivedFindingIds: [pending.id] }); expect( evaluateOnboardReadinessAdmission(report({ findings: [pending], status: "incompatible" }), { ...DEFAULT_OPTIONS, - allowDeferredN1x: true, + allowDeferredN1xManagedVllm: true, }), ).toMatchObject({ admitted: false, findingIds: [pending.id] }); }); - it.each(["absent", "unknown"] as const)( - "keeps Deferred N1x blocked when its identity capability is %s (#11041)", - (state) => { - const pending = finding(ONBOARD_READINESS_FINDING_IDS.n1xValidationPending); - const capabilities = [ - ...withCapabilityState( - requiredCapabilities(), - ONBOARD_REQUIRED_CAPABILITY_IDS.platformSupported, - "absent", - ), - capability("host.platform.n1x", state), - ]; - expect( - evaluateOnboardReadinessAdmission( - report({ capabilities, findings: [pending], status: "incompatible" }), - { ...DEFAULT_OPTIONS, allowDeferredN1x: true }, - ), - ).toMatchObject({ admitted: false, findingIds: [pending.id] }); - }, - ); - it("fails closed when a required capability is unknown or missing", () => { const capabilities = withCapabilityState( requiredCapabilities().filter( diff --git a/src/lib/readiness/onboard-admission.ts b/src/lib/readiness/onboard-admission.ts index 18f10a4a0c1..e2ba71e6212 100644 --- a/src/lib/readiness/onboard-admission.ts +++ b/src/lib/readiness/onboard-admission.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 import type { ReadinessCapability, ReadinessFinding, SystemReadinessReport } from "./types"; -import { isN1xOnboardingProviderKey } from "../onboard/inference-providers/provider-selection-keys"; export const ONBOARD_READINESS_ADMISSION_REASON_IDS = { blockingFindings: "onboard.readiness.blocking_findings", @@ -12,14 +11,6 @@ export const ONBOARD_READINESS_ADMISSION_REASON_IDS = { export type OnboardReadinessAdmissionReasonId = (typeof ONBOARD_READINESS_ADMISSION_REASON_IDS)[keyof typeof ONBOARD_READINESS_ADMISSION_REASON_IDS]; -export function hasExplicitDeferredN1xOnboardingIntent( - env: Readonly>, -): boolean { - const provider = String(env.NEMOCLAW_PROVIDER ?? "").trim(); - if (provider) return isN1xOnboardingProviderKey(provider); - return env.NEMOCLAW_NO_EXPRESS === "1"; -} - export const ONBOARD_READINESS_FINDING_IDS = { containerToolkitMissing: "host.gpu.container_toolkit_missing", cdiMissing: "host.gpu.cdi_missing", @@ -62,8 +53,8 @@ export interface OnboardReadinessAdmissionOptions { allowStorageRemediation: boolean; /** The explicit portable profile may prepare its rootless runtime before revalidation. */ allowPortableHostPreparation?: boolean; - /** Explicit Express or standard-onboarding intent may exercise the Deferred N1x path. */ - allowDeferredN1x?: boolean; + /** Explicit managed-vLLM intent may exercise the Deferred N1x validation path. */ + allowDeferredN1xManagedVllm?: boolean; } export type OnboardReadinessAdmissionDecision = @@ -153,7 +144,7 @@ function canWaiveFinding( return true; } if ( - options.allowDeferredN1x && + options.allowDeferredN1xManagedVllm && finding.id === ONBOARD_READINESS_FINDING_IDS.n1xValidationPending && capabilityState(capabilities, "host.platform.n1x") === "present" ) { diff --git a/test/helpers/installer-express-prompt-pty-harness.ts b/test/helpers/installer-express-prompt-pty-harness.ts index 500a0020b02..3e232eace25 100644 --- a/test/helpers/installer-express-prompt-pty-harness.ts +++ b/test/helpers/installer-express-prompt-pty-harness.ts @@ -25,7 +25,7 @@ export function runExpressPromptWithTty( stdinMode: "pipe" | "tty", platform = "DGX Spark", extraEnv: Record = {}, - entrypoint: "prompt" | "accepted-station-main" | "n1x-standard-main" = "prompt", + entrypoint: "prompt" | "accepted-station-main" = "prompt", entrypointArgs: string[] = [], harnessFixture?: InstallerExpressPtyFixture, ) { @@ -51,56 +51,7 @@ harness_mode = sys.argv[6] timeout_seconds = float(sys.argv[7]) pid_file = sys.argv[8] entrypoint_args = sys.argv[9:] -if entrypoint == "n1x-standard-main": - script = r''' -source "$INSTALLER_UNDER_TEST" >/dev/null -detect_express_platform() { printf "N1x"; } -classify_dgx_station_release() { printf "generic-ubuntu"; } -NON_INTERACTIVE="\${NON_INTERACTIVE:-}" -NEMOCLAW_PROVIDER="\${NEMOCLAW_PROVIDER:-}" -NEMOCLAW_NO_EXPRESS="\${NEMOCLAW_NO_EXPRESS:-}" -maybe_offer_express_install - -NEMOCLAW_SOURCE_ROOT="$PWD/source" -mkdir -p "$NEMOCLAW_SOURCE_ROOT/dist/lib/onboard/experimental" \ - "$NEMOCLAW_SOURCE_ROOT/dist/lib/readiness" -for module in \ - "$NEMOCLAW_SOURCE_ROOT/dist/lib/onboard/preflight.js" \ - "$NEMOCLAW_SOURCE_ROOT/dist/lib/onboard/gateway-management.js" \ - "$NEMOCLAW_SOURCE_ROOT/dist/lib/onboard/experimental/portable-profile.js" \ - "$NEMOCLAW_SOURCE_ROOT/dist/lib/onboard/docker-driver-gateway-env.js" \ - "$NEMOCLAW_SOURCE_ROOT/dist/lib/readiness/host.js" \ - "$NEMOCLAW_SOURCE_ROOT/dist/lib/readiness/onboard-admission.js"; do - : >"$module" -done -repair_installer_nvidia_cdi_spec() { :; } -node() { - if [ "\${1:-}" = "-e" ]; then - printf '__INFO__\nHOST_PREFLIGHT_NO_EXPRESS=%s PROVIDER=%s QUALIFIED_N1X=1\n' \ - "\${NEMOCLAW_NO_EXPRESS:-}" "\${NEMOCLAW_PROVIDER:-}" - if [ "\${NEMOCLAW_NO_EXPRESS:-}" = "1" ] || [ "\${NEMOCLAW_PROVIDER:-}" = "ollama" ]; then - return 0 - fi - printf '__ACTIONS__\nAdmission finding IDs: host.platform.n1x_validation_pending\n' - return 10 - fi - command node "$@" -} -run_installer_host_preflight || exit $? - -fake_cli="$PWD/nemoclaw-under-test" -printf '%s\n' \ - '#!/usr/bin/env bash' \ - 'printf "ONBOARD NO_EXPRESS=%s PROVIDER=%s ARGS=%s\\n" "\${NEMOCLAW_NO_EXPRESS:-}" "\${NEMOCLAW_PROVIDER:-}" "$*"' \ - >"$fake_cli" -chmod +x "$fake_cli" -_CLI_BIN="nemoclaw" -_CLI_PATH="$fake_cli" -show_usage_notice() { :; } -nemoclaw_state_dir() { printf '%s' "$PWD/state"; } -run_onboard -''' -elif entrypoint == "accepted-station-main": +if entrypoint == "accepted-station-main": script = r''' source "$INSTALLER_UNDER_TEST" >/dev/null detect_express_platform() { printf "$EXPRESS_PLATFORM"; } @@ -113,11 +64,11 @@ classify_dgx_station_release() { printf "%s" "\${EXPRESS_RELEASE_STATE:-generic- station_installer_revision() { printf 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; } station_express_resume_generation() { printf '0123456789abcdef0123456789abcdef'; } bash() { - printf "RESULT NON_INTERACTIVE=%s SUDO_MODE=%s PROVIDER=%s MODEL=%s VLLM_MODEL=%s POLICY=%s YES=%s SANDBOX=%s STATION_EXPRESS=%s PROFILE_GATE=%s PROFILE_RUNTIME=%s SPARK_SELECTION=%s NO_EXPRESS=%s\\n" \ + printf "RESULT NON_INTERACTIVE=%s SUDO_MODE=%s PROVIDER=%s MODEL=%s VLLM_MODEL=%s POLICY=%s YES=%s SANDBOX=%s STATION_EXPRESS=%s PROFILE_GATE=%s PROFILE_RUNTIME=%s SPARK_SELECTION=%s\\n" \ "\${NON_INTERACTIVE:-}" "\${NEMOCLAW_NON_INTERACTIVE_SUDO_MODE:-}" "\${NEMOCLAW_PROVIDER:-}" "\${NEMOCLAW_MODEL:-}" \ "\${NEMOCLAW_VLLM_MODEL:-}" "\${NEMOCLAW_POLICY_MODE:-}" "\${NEMOCLAW_YES:-}" "\${NEMOCLAW_SANDBOX_NAME:-}" \ "\${NEMOCLAW_STATION_EXPRESS:-}" "\${NEMOCLAW_ENABLE_LOCAL_MODEL_PROFILE:-}" "\${NEMOCLAW_LOCAL_MODEL_RUNTIME:-}" \ - "\${_SPARK_EXPRESS_INFERENCE_SELECTION:-}" "\${NEMOCLAW_NO_EXPRESS:-}" + "\${_SPARK_EXPRESS_INFERENCE_SELECTION:-}" exit 0 } main "$@" @@ -134,11 +85,11 @@ if [ "\${FORCE_EXPRESS_PROMPT_READ_FAILURE:-}" = "1" ]; then read() { return 1; } fi maybe_offer_express_install -printf "RESULT NON_INTERACTIVE=%s SUDO_MODE=%s PROVIDER=%s MODEL=%s VLLM_MODEL=%s POLICY=%s YES=%s SANDBOX=%s STATION_EXPRESS=%s PROFILE_GATE=%s PROFILE_RUNTIME=%s SPARK_SELECTION=%s NO_EXPRESS=%s\\n" \\ +printf "RESULT NON_INTERACTIVE=%s SUDO_MODE=%s PROVIDER=%s MODEL=%s VLLM_MODEL=%s POLICY=%s YES=%s SANDBOX=%s STATION_EXPRESS=%s PROFILE_GATE=%s PROFILE_RUNTIME=%s SPARK_SELECTION=%s\\n" \\ "\${NON_INTERACTIVE:-}" "\${NEMOCLAW_NON_INTERACTIVE_SUDO_MODE:-}" "\${NEMOCLAW_PROVIDER:-}" "\${NEMOCLAW_MODEL:-}" \\ "\${NEMOCLAW_VLLM_MODEL:-}" "\${NEMOCLAW_POLICY_MODE:-}" "\${NEMOCLAW_YES:-}" "\${NEMOCLAW_SANDBOX_NAME:-}" \\ "\${NEMOCLAW_STATION_EXPRESS:-}" "\${NEMOCLAW_ENABLE_LOCAL_MODEL_PROFILE:-}" "\${NEMOCLAW_LOCAL_MODEL_RUNTIME:-}" \\ - "\${_SPARK_EXPRESS_INFERENCE_SELECTION:-}" "\${NEMOCLAW_NO_EXPRESS:-}" + "\${_SPARK_EXPRESS_INFERENCE_SELECTION:-}" ''' env = dict(os.environ) env["INSTALLER_UNDER_TEST"] = installer diff --git a/test/helpers/installer-readiness-stubs.ts b/test/helpers/installer-readiness-stubs.ts index ae82e8a2fe6..f352079265f 100644 --- a/test/helpers/installer-readiness-stubs.ts +++ b/test/helpers/installer-readiness-stubs.ts @@ -84,7 +84,6 @@ export function writeInstallerReadinessModuleStubs(readinessDir: string): void { waivedFindingIds: [], }; }; -exports.hasExplicitDeferredN1xOnboardingIntent = () => false; `, ); fs.writeFileSync( diff --git a/test/install/install-express-n1x.test.ts b/test/install/install-express-n1x.test.ts index f22f74079e0..e4c1e29d05c 100644 --- a/test/install/install-express-n1x.test.ts +++ b/test/install/install-express-n1x.test.ts @@ -37,55 +37,19 @@ describe("installer N1x Express preview", () => { expect(result.status, output).toBe(0); expect(output).toMatch(/Skipping express install\. Continuing with interactive flow/); - expect(output).toMatch(/RESULT .*NO_EXPRESS=1/); + expect(output).toMatch(/RESULT /); }); - it("carries a declined preview through host admission into the onboarding CLI (#11041)", () => { - const result = runExpressPromptWithTty("n\n", "pipe", "N1x", {}, "n1x-standard-main"); - const output = `${result.stdout}${result.stderr}`; - - expect(result.status, output).toBe(0); - expect(output).toContain("HOST_PREFLIGHT_NO_EXPRESS=1 PROVIDER= QUALIFIED_N1X=1"); - expect(output).toContain("ONBOARD NO_EXPRESS=1 PROVIDER= ARGS=onboard"); - }); - - it("continues with ordinary onboarding when NEMOCLAW_NO_EXPRESS is set (#11041)", () => { - const result = runExpressPromptWithTty("", "pipe", "N1x", { - NEMOCLAW_NO_EXPRESS: "1", - }); - const output = `${result.stdout}${result.stderr}`; - - expect(result.status, output).toBe(0); - expect(output).toMatch(/Skipping express prompt \(NEMOCLAW_NO_EXPRESS=1\)/); - expect(output).toMatch(/RESULT .*NO_EXPRESS=1/); - }); - - it("continues with an explicit non-Express provider on N1x (#11041)", () => { - const result = runExpressPromptWithTty("", "pipe", "N1x", { - NEMOCLAW_PROVIDER: "ollama", - }); - const output = `${result.stdout}${result.stderr}`; - - expect(result.status, output).toBe(0); - expect(output).toMatch(/Skipping express prompt \(NEMOCLAW_PROVIDER=ollama already set\)/); - expect(output).toMatch(/RESULT .*PROVIDER=ollama/); - }); - - it("carries an explicit Ollama choice through host admission into the onboarding CLI (#11041)", () => { - const result = runExpressPromptWithTty( - "", - "pipe", - "N1x", - { - NEMOCLAW_PROVIDER: "ollama", - }, - "n1x-standard-main", - ); + it.each([ + ["NEMOCLAW_NO_EXPRESS", { NEMOCLAW_NO_EXPRESS: "1" }], + ["an explicit provider", { NEMOCLAW_PROVIDER: "ollama" }], + ])("continues with ordinary onboarding for %s (#11041)", (_scenario, env) => { + const result = runExpressPromptWithTty("", "pipe", "N1x", env); const output = `${result.stdout}${result.stderr}`; expect(result.status, output).toBe(0); - expect(output).toContain("HOST_PREFLIGHT_NO_EXPRESS= PROVIDER=ollama QUALIFIED_N1X=1"); - expect(output).toContain("ONBOARD NO_EXPRESS= PROVIDER=ollama ARGS=onboard"); + expect(output).toMatch(/Skipping express prompt/); + expect(output).toMatch(/RESULT /); }); it("allows the N1x prompt bypass with explicit managed-vLLM intent (#8574)", () => { diff --git a/test/package-contract/installer-host-preflight.test.ts b/test/package-contract/installer-host-preflight.test.ts index 9615ff22177..95e80aee7b3 100644 --- a/test/package-contract/installer-host-preflight.test.ts +++ b/test/package-contract/installer-host-preflight.test.ts @@ -27,21 +27,9 @@ exit 99`, ); } -function copyCompiledOnboardAdmission(readinessDir: string, onboardDir: string): void { - const compiledAdmission = path.resolve("dist/lib/readiness/onboard-admission.js"); - const compiledProviderKeys = path.resolve( - "dist/lib/onboard/inference-providers/provider-selection-keys.js", - ); - const targetProviderDir = path.join(onboardDir, "inference-providers"); - fs.mkdirSync(targetProviderDir, { recursive: true }); - fs.copyFileSync(compiledAdmission, path.join(readinessDir, "onboard-admission.js")); - fs.copyFileSync(compiledProviderKeys, path.join(targetProviderDir, "provider-selection-keys.js")); -} - function runInstallerHostAdmissionTest( host: { runtime: string; - isN1x?: boolean; hasNestedOverlayConflict?: boolean; isUnsupportedRuntime?: boolean; additionalFindingIds?: string[]; @@ -54,9 +42,6 @@ function runInstallerHostAdmissionTest( gatewayManagementMode?: string; portableProfileArtifact?: "present" | "missing"; providerResolutionFailure?: string; - provider?: string; - noExpress?: boolean; - useCompiledAdmission?: boolean; } = {}, ) { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-installer-host-admission-")); @@ -144,41 +129,13 @@ exports.configuredRuntimeProviderOwnsHostReadiness = ({ environment = process.en for (const id of host.additionalFindingIds || []) { findings.push({ id, severity: "blocking", summary: "Blocking finding: " + id }); } - const requiredCapabilities = [ - "host.docker.available", - "host.docker.daemon_reachable", - "host.docker.runtime_supported", - "host.docker.storage_compatible", - "host.gpu.nvidia_available", - "host.gpu.container_toolkit_available", - "host.gpu.cdi_healthy", - "host.platform.supported", - ]; - const unknown = new Set(host.unknownCapabilityIds || []); - const capabilities = requiredCapabilities.map((id) => ({ - id, - state: unknown.has(id) ? "unknown" : id === "host.platform.supported" && host.isN1x ? "absent" : "present", - })); - if (host.isN1x) capabilities.push({ id: "host.platform.n1x", state: "present" }); - for (const id of unknown) { - if (!capabilities.some((capability) => capability.id === id)) { - capabilities.push({ id, state: "unknown" }); - } - } - return { - observations: [], - capabilities, - findings, - capabilityIds: host.unknownCapabilityIds || [], - host, - }; + return { findings, capabilityIds: host.unknownCapabilityIds || [], host }; }; `, ); - const writeStubbedOnboardAdmission = () => - fs.writeFileSync( - path.join(readinessDir, "onboard-admission.js"), - `const forcedRejection = ${JSON.stringify(forcedRejection ?? null)}; + fs.writeFileSync( + path.join(readinessDir, "onboard-admission.js"), + `const forcedRejection = ${JSON.stringify(forcedRejection ?? null)}; exports.evaluateOnboardReadinessAdmission = (report, options) => { if (forcedRejection) { return { admitted: false, reasonIds: [], ...forcedRejection, waivedFindingIds: [] }; @@ -199,11 +156,6 @@ exports.evaluateOnboardReadinessAdmission = (report, options) => { ]); const findingIds = report.findings .filter((finding) => { - if ( - finding.id === "host.platform.n1x_validation_pending" && - options.allowDeferredN1x && - report.host.isN1x - ) return false; if ( options.providerOwnsHostReadiness && providerOwnedDockerFindings.has(finding.id) @@ -236,20 +188,13 @@ exports.evaluateOnboardReadinessAdmission = (report, options) => { ? { admitted: true, waivedFindingIds: [] } : { admitted: false, reasonIds: [], findingIds, capabilityIds, waivedFindingIds: [] }; }; -exports.hasExplicitDeferredN1xOnboardingIntent = () => false; `, - ); - const installOnboardAdmission = options.useCompiledAdmission - ? () => copyCompiledOnboardAdmission(readinessDir, onboardDir) - : writeStubbedOnboardAdmission; - installOnboardAdmission(); + ); writeNodeStub(fakeBin); const { NEMOCLAW_EXPERIMENTAL_PROFILE: _experimentalProfile, NEMOCLAW_GATEWAY_RUNTIME: _gatewayRuntime, - NEMOCLAW_NO_EXPRESS: _noExpress, - NEMOCLAW_PROVIDER: _provider, TEST_GATEWAY_MANAGEMENT_MODE: _gatewayManagementMode, ...inheritedEnv } = process.env; @@ -263,8 +208,6 @@ exports.hasExplicitDeferredN1xOnboardingIntent = () => false; ? { NEMOCLAW_EXPERIMENTAL_PROFILE: options.experimentalProfile } : {}), ...(options.gatewayRuntime ? { NEMOCLAW_GATEWAY_RUNTIME: options.gatewayRuntime } : {}), - ...(options.noExpress ? { NEMOCLAW_NO_EXPRESS: "1" } : {}), - ...(options.provider ? { NEMOCLAW_PROVIDER: options.provider } : {}), TEST_GATEWAY_MANAGEMENT_MODE: options.gatewayManagementMode ?? "", }; @@ -289,94 +232,6 @@ run_installer_host_preflight } describe("installer host preflight package contract", () => { - it.each([ - ["a declined Express preview", { noExpress: true, useCompiledAdmission: true }], - ["an explicit standard provider", { provider: "ollama", useCompiledAdmission: true }], - ])("admits qualified Deferred N1x through %s (#11041)", (_scenario, intent) => { - const { output, result } = runInstallerHostAdmissionTest( - { - runtime: "docker", - isN1x: true, - additionalFindingIds: ["host.platform.n1x_validation_pending"], - }, - undefined, - intent, - ); - - expect(result.status, output).toBe(0); - expect(output).not.toMatch(/Host preflight found issues/); - }); - - it("keeps Deferred N1x blocked without explicit onboarding intent (#11041)", () => { - const { output, result } = runInstallerHostAdmissionTest( - { - runtime: "docker", - isN1x: true, - additionalFindingIds: ["host.platform.n1x_validation_pending"], - }, - undefined, - { useCompiledAdmission: true }, - ); - - expect(result.status).toBe(1); - expect(output).toContain("host.platform.n1x_validation_pending"); - }); - - it.each(["unknown-provider", "nim", "nim-local"])( - "keeps Deferred N1x blocked for provider %s (#11041)", - (provider) => { - const { output, result } = runInstallerHostAdmissionTest( - { - runtime: "docker", - isN1x: true, - additionalFindingIds: ["host.platform.n1x_validation_pending"], - }, - undefined, - { provider, useCompiledAdmission: true }, - ); - - expect(result.status).toBe(1); - expect(output).toContain("host.platform.n1x_validation_pending"); - }, - ); - - it.each(["nim", "nim-local"])( - "keeps Deferred N1x blocked for provider %s when Express is disabled (#11041)", - (provider) => { - const { output, result } = runInstallerHostAdmissionTest( - { - runtime: "docker", - isN1x: true, - additionalFindingIds: ["host.platform.n1x_validation_pending"], - }, - undefined, - { noExpress: true, provider, useCompiledAdmission: true }, - ); - - expect(result.status).toBe(1); - expect(output).toContain("host.platform.n1x_validation_pending"); - }, - ); - - it("keeps unrelated blocking findings fail-closed for Deferred N1x intent (#11041)", () => { - const { output, result } = runInstallerHostAdmissionTest( - { - runtime: "docker", - isN1x: true, - additionalFindingIds: [ - "host.platform.n1x_validation_pending", - "host.test.additional_blocker", - ], - }, - undefined, - { provider: "ollama", useCompiledAdmission: true }, - ); - - expect(result.status).toBe(1); - expect(output).not.toContain("host.platform.n1x_validation_pending"); - expect(output).toContain("host.test.additional_blocker"); - }); - it("continues to onboarding when managed storage remediation is available", () => { const { output, result } = runInstallerHostAdmissionTest({ runtime: "docker", From a8403020373257cb0514683a55c702329f63783a Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 4 Sep 2026 11:36:57 -0700 Subject: [PATCH 07/19] fix(onboard): close N1x standard fallback review gaps Signed-off-by: Aaron Erickson --- ci/platform-matrix.json | 8 +-- docs/get-started/quickstart-hermes.mdx | 12 ++--- .../quickstart-langchain-deepagents-code.mdx | 12 ++--- docs/get-started/quickstart.mdx | 14 ++--- docs/inference/choose-inference-provider.mdx | 12 ++--- .../choose-local-inference-server.mdx | 14 ++--- docs/inference/set-up-vllm.mdx | 19 ++++--- docs/reference/commands.mdx | 4 +- docs/reference/platform-support.mdx | 6 +-- docs/reference/system-readiness.mdx | 6 ++- docs/reference/troubleshooting.mdx | 2 +- docs/resources/starter-prompt.md | 18 +++---- docs/security/best-practices.mdx | 9 ++-- scripts/install.sh | 9 ++-- src/lib/onboard/fatal-runtime-preflight.ts | 6 +-- src/lib/readiness/onboard-admission.test.ts | 30 +++++++---- src/lib/readiness/onboard-admission.ts | 9 +++- .../installer-express-prompt-pty-harness.ts | 8 +-- test/helpers/installer-readiness-stubs.ts | 2 + test/install/install-express-n1x.test.ts | 51 ++++++++++++++----- .../installer-host-preflight.test.ts | 47 +++++++++++++++++ 21 files changed, 198 insertions(+), 100 deletions(-) diff --git a/ci/platform-matrix.json b/ci/platform-matrix.json index 251437d6708..76ad45fefa5 100644 --- a/ci/platform-matrix.json +++ b/ci/platform-matrix.json @@ -1,7 +1,7 @@ { "$comment": "SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\nSPDX-License-Identifier: Apache-2.0\n\nSingle source of truth for NemoClaw launch claims and platform support. Covers platforms, inference providers, supported agents, messaging integrations, and deployment paths. Scripts read this to generate README and docs tables. QA/CI update platform/provider rows; the engineering owner reviews other rows. Docs are derived.", "version": "1.1", - "updated": "2026-08-14", + "updated": "2026-09-04", "project_status": { "stage": "alpha", @@ -74,7 +74,7 @@ "status": "deferred", "ci_tested": false, "prerequisites_notes": "N1x Express remains Deferred until a physical NemoClaw Express E2E test passes. Host identity requires Linux `arm64`, a trusted `/etc/fastos-release` marker with `NAME=\"N1x FASTOS\"`, and NVIDIA vendor ID `10de` on a display-class PCI device. CUDA and Container Device Interface (CDI) readiness are still required.", - "notes": "The accepted Deferred N1x Express preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. It does not activate DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Host identity requires Linux `arm64`, a regular `/etc/fastos-release` file of 1 through 4,096 bytes that is owned by UID 0 and GID 0, is not a symbolic link, is not group- or world-writable, and contains exactly one `NAME=\"N1x FASTOS\"` line. It also requires NVIDIA vendor ID `10de` on a display-class PCI device. Generic DMI values are supplemental and FastOS version is not pinned. Physical CUDA and CDI checks passed on one N1x host, but full NemoClaw Express E2E validation and dedicated CI coverage are pending. `host.platform.supported` remains absent; only explicit managed-vLLM preview intent can waive the pending-validation finding. Do not claim N1x as supported until that validation passes and this row is promoted." + "notes": "The accepted Deferred N1x Express preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. It does not activate DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Host identity requires Linux `arm64`, a regular `/etc/fastos-release` file of 1 through 4,096 bytes that is owned by UID 0 and GID 0, is not a symbolic link, is not group- or world-writable, and contains exactly one `NAME=\"N1x FASTOS\"` line. It also requires NVIDIA vendor ID `10de` on a display-class PCI device. Generic DMI values are supplemental and FastOS version is not pinned. Physical CUDA and CDI checks passed on one N1x host, but full NemoClaw Express E2E validation and dedicated CI coverage are pending. `host.platform.supported` remains absent. After identity qualifies, accepting Express can waive only the pending-validation finding. Declining or disabling Express can apply the same waiver when no provider is preselected or the explicit provider is recognized and is not Local NVIDIA NIM. An invalid or Local NVIDIA NIM provider remains blocked. Existing provider requirements still apply, and this waiver does not promote N1x. Do not claim N1x as supported until validation passes and this row is promoted." }, { "name": "NVIDIA RTX (consumer and Pro workstation GPUs)", @@ -157,13 +157,13 @@ "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 healthy CDI spec. Onboarding evaluates the canonical `host.gpu.nvidia_available`, `host.gpu.container_toolkit_available`, and `host.gpu.cdi_healthy` readiness capabilities before gateway, image, or sandbox lifecycle effects. NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag. Onboarding filters the validated image list by usable memory. In non-interactive mode, onboarding selects the first compatible model unless the operator sets `NEMOCLAW_MODEL`. Model selection uses detected free GPU memory. If free memory is unavailable, NemoClaw uses total GPU memory. On DGX Spark, NemoClaw caps usable memory at 50 percent of total memory to match NIM's reported unified-memory limit. The Nemotron 3 Super catalog minimum includes NIM's reported runtime allocation. Local NVIDIA NIM is unavailable on N1x. NemoClaw omits the provider from onboarding and rejects `NEMOCLAW_PROVIDER=nim-local` on N1x. Use the Deferred managed-vLLM preview on N1x. On Linux arm64 DGX Spark and DGX Station hosts, onboarding warns that some NIM images may not publish a `linux/arm64` manifest; the warning is advisory, and the selected image pull can still fail when the registry has no matching platform manifest. 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 healthy CDI spec. Onboarding evaluates the canonical `host.gpu.nvidia_available`, `host.gpu.container_toolkit_available`, and `host.gpu.cdi_healthy` readiness capabilities before gateway, image, or sandbox lifecycle effects. NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag. Onboarding filters the validated image list by usable memory. In non-interactive mode, onboarding selects the first compatible model unless the operator sets `NEMOCLAW_MODEL`. Model selection uses detected free GPU memory. If free memory is unavailable, NemoClaw uses total GPU memory. On DGX Spark, NemoClaw caps usable memory at 50 percent of total memory to match NIM's reported unified-memory limit. The Nemotron 3 Super catalog minimum includes NIM's reported runtime allocation. Local NVIDIA NIM is unavailable on N1x. NemoClaw omits the provider from onboarding and rejects `NEMOCLAW_PROVIDER=nim-local` on N1x. Select another standard provider or use the Deferred managed-vLLM preview on N1x. On Linux arm64 DGX Spark and DGX Station hosts, onboarding warns that some NIM images may not publish a `linux/arm64` manifest; the warning is advisory, and the selected image pull can still fail when the registry has no matching platform manifest. 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)", "status": "caveated", "endpoint_type": "Local OpenAI-compatible", - "notes": "Unavailable on N1x. On other hosts, it appears in the onboarding menu when NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. No flag is required. The model is whatever the existing server serves." + "notes": "On a qualified N1x host, this route requires explicit standard-onboarding intent and remains unvalidated. On other hosts, no explicit intent is required. It appears in the onboarding menu when NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. The model is whatever the existing server serves." }, { "name": "Local vLLM (managed install/start)", diff --git a/docs/get-started/quickstart-hermes.mdx b/docs/get-started/quickstart-hermes.mdx index 517eed11b51..2e4440aa6bf 100644 --- a/docs/get-started/quickstart-hermes.mdx +++ b/docs/get-started/quickstart-hermes.mdx @@ -61,14 +61,14 @@ Review the [Prerequisites](prerequisites) before you begin. Press Enter to use the recommended express install mode for that platform. This mode applies preset settings and runs the remaining onboarding non-interactively. Express install mode preserves the explicit Hermes selection in the install command. - On supported non-N1x express platforms, enter `n` if you want to select a sandbox name, an inference provider, and a model interactively. - On N1x, entering `n` stops installation because the Deferred managed-vLLM preview is the only admitted onboarding path. + Enter `n` if you want to select a sandbox name, an inference provider, and a model interactively. + On N1x, the selected provider must meet its existing requirements, and local NVIDIA NIM remains unavailable. Refer to [Use a container runtime and supported platforms](#use-a-container-runtime-and-supported-platforms) for more information. The install command creates the `my-hermes` sandbox used in the commands that follow. If you accept Express setup, wait for the installer to finish, then continue with **Confirm the Sandbox Is Ready**; Express selects the provider and model non-interactively. - If the installer does not offer Express setup, or if you enter `n` at the Express prompt on a supported non-N1x host, choose an inference provider and model, then provide its credential when prompted. + If the installer does not offer Express setup, or if you enter `n` at the Express prompt, choose an inference provider and model, then provide its credential when prompted. For that interactive path, skip optional web search and messaging setup on a first run, then accept the suggested network policy tier. With the OpenShell Docker driver, stock Hermes onboarding normally uses the release's exact managed-image digest. If registry or catalog availability prevents resolution, stock onboarding stops before sandbox creation and does not build a shipped Dockerfile. @@ -267,9 +267,9 @@ Use these details when your first-run path needs more control. N1x remains outside the supported-platform set until that validation passes and the platform matrix status is promoted. Press Enter at the express install prompt to apply preset settings, switch the remaining onboarding to non-interactive mode, and select the managed local inference path for that platform. On N1x, accept the preview prompt to apply those settings as explicit Deferred preview intent. - On supported non-N1x express platforms, enter `n` to continue with interactive onboarding when you want to select the agent or other settings yourself. - On N1x, declining the preview or setting only `NEMOCLAW_NO_EXPRESS=1` stops installation before onboarding. - Accept the preview, or set `NEMOCLAW_PROVIDER=install-vllm` before installation to provide the required explicit managed-vLLM intent. + Enter `n` to continue with interactive onboarding when you want to select the agent or other settings yourself. + On N1x, setting `NEMOCLAW_NO_EXPRESS=1` also continues with ordinary onboarding. + Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set another provider to enter standard onboarding directly. The first Hermes start can take several minutes while OpenShell pulls the managed image when it is not already cached. The N1x preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. Refer to [Set Up vLLM](../inference/local-inference/set-up-vllm) for managed model profiles and headless setup. diff --git a/docs/get-started/quickstart-langchain-deepagents-code.mdx b/docs/get-started/quickstart-langchain-deepagents-code.mdx index 2ddbfbef6a6..f37a137590e 100644 --- a/docs/get-started/quickstart-langchain-deepagents-code.mdx +++ b/docs/get-started/quickstart-langchain-deepagents-code.mdx @@ -57,14 +57,14 @@ If you use the coding-agent prompt in the preceding section, you can skip this p Press Enter to use the recommended express install mode for that platform. This mode applies preset settings and runs the remaining onboarding non-interactively. Express install mode preserves the explicit Deep Agents selection in the install command. - On supported non-N1x express platforms, enter `n` if you want to select a sandbox name, an inference provider, and a model interactively. - On N1x, entering `n` stops installation because the Deferred managed-vLLM preview is the only admitted onboarding path. + Enter `n` if you want to select a sandbox name, an inference provider, and a model interactively. + On N1x, the selected provider must meet its existing requirements, and local NVIDIA NIM remains unavailable. Refer to [Use a container runtime and supported platforms](#use-a-container-runtime-and-supported-platforms) for more information. The install command creates the `my-deepagents` sandbox used in the commands that follow. If you accept Express setup, wait for the installer to finish, then continue with **Confirm the Sandbox Is Ready**; Express selects the provider and model non-interactively. - If the installer does not offer Express setup, or if you enter `n` at the Express prompt on a supported non-N1x host, choose an inference provider and model, then provide its credential when prompted. + If the installer does not offer Express setup, or if you enter `n` at the Express prompt, choose an inference provider and model, then provide its credential when prompted. For that interactive path, accept the suggested network policy tier on a first run. With the OpenShell Docker driver, stock Deep Agents Code onboarding normally uses the release's exact managed-image digest. If registry or catalog availability prevents resolution, stock onboarding stops before sandbox creation and does not build a shipped Dockerfile. @@ -127,9 +127,9 @@ Use these details when you need more control during setup or after the first san N1x remains outside the supported-platform set until that validation passes and the platform matrix status is promoted. Press Enter at the express install prompt to apply preset settings, switch the remaining onboarding to non-interactive mode, and select the managed local inference path for that platform. On N1x, accept the preview prompt to apply those settings as explicit Deferred preview intent. - On supported non-N1x express platforms, enter `n` to continue with interactive onboarding when you want to select the agent or other settings yourself. - On N1x, declining the preview or setting only `NEMOCLAW_NO_EXPRESS=1` stops installation before onboarding. - Accept the preview, or set `NEMOCLAW_PROVIDER=install-vllm` before installation to provide the required explicit managed-vLLM intent. + Enter `n` to continue with interactive onboarding when you want to select the agent or other settings yourself. + On N1x, setting `NEMOCLAW_NO_EXPRESS=1` also continues with ordinary onboarding. + Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set another provider to enter standard onboarding directly. The N1x preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. diff --git a/docs/get-started/quickstart.mdx b/docs/get-started/quickstart.mdx index 1dde40622a4..292662074a0 100644 --- a/docs/get-started/quickstart.mdx +++ b/docs/get-started/quickstart.mdx @@ -61,9 +61,9 @@ Review the [Prerequisites](prerequisites) before you begin. Press Enter to use the recommended preset settings on a qualified platform or to explicitly opt into the Deferred N1x preview. Express install mode completes the remaining onboarding non-interactively. If you accept, wait for the installer to finish, then continue with **Confirm the Sandbox Is Ready**. - On N1x, entering `n` stops installation because the Deferred managed-vLLM preview is the only admitted onboarding path. - Re-run and accept the preview, or set `NEMOCLAW_PROVIDER=install-vllm` before installation. - On the other express platforms, enter `n` to continue with the interactive onboarding flow described in this step. + On N1x, enter `n` to continue with the interactive onboarding flow described in this step. + N1x remains Deferred, and the provider that you select must meet its existing requirements. + Local NVIDIA NIM remains unavailable on N1x. @@ -197,14 +197,14 @@ Use these details when your first-run path needs more control. After you press Enter at the express install prompt, or explicitly accept the Deferred N1x preview, the installer switches the remaining onboarding to non-interactive mode and selects the managed local inference path for that platform. Express install mode uses `my-assistant` unless you set `NEMOCLAW_SANDBOX_NAME`, and it applies the suggested Balanced policy. It can still prompt for `sudo` when host setup needs it. - On the other express platforms, set `NEMOCLAW_NO_EXPRESS=1` to skip the express prompt, or set `NEMOCLAW_PROVIDER` to choose a provider yourself. + On any express platform, set `NEMOCLAW_NO_EXPRESS=1` to skip the express prompt, or set `NEMOCLAW_PROVIDER` to choose a provider yourself. N1x Express selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. N1x detection requires Linux `arm64`, a trusted `/etc/fastos-release` marker with `NAME="N1x FASTOS"`, and NVIDIA vendor ID `10de` on a display-class PCI device. The N1x path does not use DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. - Accepting the N1x prompt supplies the narrow preview intent required to pass the pending-validation readiness finding. - Declining the N1x prompt or setting only `NEMOCLAW_NO_EXPRESS=1` stops installation before onboarding; neither path opens interactive provider selection. - Set `NEMOCLAW_PROVIDER=install-vllm` when you need to bypass the N1x preview prompt with the same explicit intent. + Accepting the N1x prompt supplies the narrow managed-vLLM preview intent required to pass the pending-validation readiness finding. + Declining the N1x prompt or setting `NEMOCLAW_NO_EXPRESS=1` continues with ordinary interactive onboarding. + Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set another provider to enter standard onboarding directly. Refer to [Platform Support](../reference/platform-support) before you rely on this path. Before the Station express confirmation, the installer reports whether the Hugging Face download is authenticated without displaying the token value. diff --git a/docs/inference/choose-inference-provider.mdx b/docs/inference/choose-inference-provider.mdx index 8f123ad2836..d1817dbbbff 100644 --- a/docs/inference/choose-inference-provider.mdx +++ b/docs/inference/choose-inference-provider.mdx @@ -13,8 +13,8 @@ Choose a provider based on where the model runs, which API it exposes, and which NemoClaw validates the provider and model before it creates the sandbox. -On native N1x FASTOS, the Deferred managed-vLLM preview is the only admitted inference route. -Accept the native N1x Express prompt or set `NEMOCLAW_PROVIDER=install-vllm` to provide the required explicit preview intent. +On native N1x FASTOS, accept the Deferred managed-vLLM preview or enter standard onboarding by declining Express, disabling Express without an excluded provider, or setting a recognized `NEMOCLAW_PROVIDER` value other than Local NVIDIA NIM. +N1x remains Deferred, each standard provider keeps its existing requirements, and local NVIDIA NIM remains unavailable. On a qualifying Windows WSL N1x host, Windows Express uses managed llama.cpp after Docker Desktop GPU passthrough succeeds. Other Windows WSL hosts retain Windows-host or WSL-local Ollama. @@ -37,8 +37,8 @@ Use this status table to distinguish validated provider integrations from adapte | 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 llama.cpp (already running) | Experimental | Local OpenAI-compatible | Attaches only to an authenticated operator-managed server on loopback port `8081` that passes cooperative native llama.cpp fingerprinting. NemoClaw does not own its server, model, or lifecycle. | | Local llama.cpp (managed DGX Spark) | Experimental | Local OpenAI-compatible | Uses the repository-owned YAML recipe for NVIDIA Nemotron 3 Nano 30B-A3B, exact GGUF verification, and Docker lifecycle on a qualified DGX Spark host. The recipe remains unqualified for agent support. | -| 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 healthy CDI spec. Onboarding evaluates the canonical `host.gpu.nvidia_available`, `host.gpu.container_toolkit_available`, and `host.gpu.cdi_healthy` readiness capabilities before gateway, image, or sandbox lifecycle effects. NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag. Onboarding filters the validated image list by usable memory. In non-interactive mode, onboarding selects the first compatible model unless the operator sets `NEMOCLAW_MODEL`. Model selection uses detected free GPU memory. If free memory is unavailable, NemoClaw uses total GPU memory. On DGX Spark, NemoClaw caps usable memory at 50 percent of total memory to match NIM's reported unified-memory limit. The Nemotron 3 Super catalog minimum includes NIM's reported runtime allocation. Local NVIDIA NIM is unavailable on N1x. NemoClaw omits the provider from onboarding and rejects `NEMOCLAW_PROVIDER=nim-local` on N1x. Use the Deferred managed-vLLM preview on N1x. On Linux arm64 DGX Spark and DGX Station hosts, onboarding warns that some NIM images may not publish a `linux/arm64` manifest; the warning is advisory, and the selected image pull can still fail when the registry has no matching platform manifest. 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 | Unavailable on N1x. On other hosts, it appears in the onboarding menu when NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. No flag is required. The model is whatever the existing server serves. | +| 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 healthy CDI spec. Onboarding evaluates the canonical `host.gpu.nvidia_available`, `host.gpu.container_toolkit_available`, and `host.gpu.cdi_healthy` readiness capabilities before gateway, image, or sandbox lifecycle effects. NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag. Onboarding filters the validated image list by usable memory. In non-interactive mode, onboarding selects the first compatible model unless the operator sets `NEMOCLAW_MODEL`. Model selection uses detected free GPU memory. If free memory is unavailable, NemoClaw uses total GPU memory. On DGX Spark, NemoClaw caps usable memory at 50 percent of total memory to match NIM's reported unified-memory limit. The Nemotron 3 Super catalog minimum includes NIM's reported runtime allocation. Local NVIDIA NIM is unavailable on N1x. NemoClaw omits the provider from onboarding and rejects `NEMOCLAW_PROVIDER=nim-local` on N1x. Select another standard provider or use the Deferred managed-vLLM preview on N1x. On Linux arm64 DGX Spark and DGX Station hosts, onboarding warns that some NIM images may not publish a `linux/arm64` manifest; the warning is advisory, and the selected image pull can still fail when the registry has no matching platform manifest. 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 | On a qualified N1x host, this route requires explicit standard-onboarding intent and remains unvalidated. On other hosts, no explicit intent is required. It appears in the onboarding menu when NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. The model is whatever the existing server serves. | | Local vLLM (managed install/start) | Tested with limitations | Local OpenAI-compatible | Managed install/start requires Docker on `PATH`; attaching an already-running local vLLM server is a separate path. Appears by default on DGX Spark and qualifying DGX Station GB300 hosts. A labeled Deferred preview appears on hosts that match the N1x identity requirements; N1x remains Deferred until a physical NemoClaw Express E2E test passes. DGX Station is Tested with limitations across qualified profiles on one physical DGX Station GB300. Full Station Express end-to-end qualification for the accepted no-OTA DGX OS `7.6.x` profile is pending. Dual-Station configurations are not yet validated, and dedicated CI coverage is not available. For canonical Station qualification and host preparation, see the Additional Setup page for [OpenClaw](/user-guide/openclaw/get-started/additional-setup/dgx-station-preparation), [Hermes](/user-guide/hermes/get-started/additional-setup/dgx-station-preparation), or [Deep Agents](/user-guide/deepagents/get-started/additional-setup/dgx-station-preparation). Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`, NVIDIA Container Toolkit, and CDI. NemoClaw pins runtime images to immutable digests. Station Express defaults to `nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-NVFP4`; `--station-deepseek` selects `deepseek-ai/DeepSeek-V4-Flash`. Direct managed-vLLM defaults are `nvidia/Qwen3.6-35B-A3B-NVFP4` on DGX Spark and the Deferred N1x preview, `deepseek-ai/DeepSeek-V4-Flash` on DGX Station, and `nvidia/NVIDIA-Nemotron-3-Nano-4B-FP8` on generic Linux NVIDIA GPU hosts. Image pulls from `nvcr.io` require NGC registry login. | {/* provider-status:end */} @@ -66,7 +66,7 @@ Local providers run on the host and keep inference traffic on infrastructure tha | Provider | When it appears | Setup guide | |---|---|---| | Local Ollama | Ollama is installed or running on the host. | [Use Ollama](../local-inference/set-up-ollama) | -| Local vLLM already running | On hosts other than native N1x FASTOS, NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. | [Set Up vLLM](../local-inference/set-up-vllm) | +| Local vLLM already running | NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. Native N1x requires explicit standard-onboarding intent. | [Set Up vLLM](../local-inference/set-up-vllm) | | Local vLLM managed install | Docker is available on `PATH`, the host matches a supported GPU profile, and any required opt-in is present. Attaching an already-running local vLLM server is separate. | [Set Up vLLM](../local-inference/set-up-vllm) | | Local llama.cpp already running | You explicitly select an authenticated server on loopback port `8081`. | [Set Up llama.cpp](../local-inference/set-up-llama-cpp) | | Local llama.cpp managed install | A qualified DGX Spark or Windows WSL N1x host satisfies the declarative recipe requirements. | [Set Up llama.cpp](../local-inference/set-up-llama-cpp) | @@ -76,7 +76,7 @@ Local providers run on the host and keep inference traffic on infrastructure tha | Provider | When it appears | Setup guide | |---|---|---| -| Local vLLM already running | On hosts other than native N1x FASTOS, NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. | [Set Up vLLM](../local-inference/set-up-vllm) | +| Local vLLM already running | NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. Native N1x requires explicit standard-onboarding intent. | [Set Up vLLM](../local-inference/set-up-vllm) | | Local vLLM managed install | Docker is available on `PATH`, the host matches a supported GPU profile, and any required opt-in is present. Attaching an already-running local vLLM server is separate. | [Set Up vLLM](../local-inference/set-up-vllm) | | Local llama.cpp already running | You explicitly select an authenticated server on loopback port `8081`. | [Set Up llama.cpp](../local-inference/set-up-llama-cpp) | | Local llama.cpp managed install | A qualified DGX Spark or Windows WSL N1x host satisfies the declarative recipe requirements. | [Set Up llama.cpp](../local-inference/set-up-llama-cpp) | diff --git a/docs/inference/choose-local-inference-server.mdx b/docs/inference/choose-local-inference-server.mdx index ebd1119a38b..b02308402b8 100644 --- a/docs/inference/choose-local-inference-server.mdx +++ b/docs/inference/choose-local-inference-server.mdx @@ -16,8 +16,8 @@ The agent inside the sandbox sends inference traffic to `inference.local`. OpenShell intercepts that traffic and forwards it to the local endpoint configured during onboarding. -On native N1x FASTOS, use the Deferred managed-vLLM preview. -It is the only admitted local inference route on native N1x FASTOS. +On native N1x FASTOS, use the Deferred managed-vLLM preview or enter standard onboarding explicitly. +N1x remains Deferred, each standard provider keeps its existing requirements, and local NVIDIA NIM remains unavailable. A qualifying Windows WSL N1x host instead uses the explicit managed llama.cpp recipe through Windows Express. @@ -27,8 +27,8 @@ A qualifying Windows WSL N1x host instead uses the explicit managed llama.cpp re | Option | When to use it | Availability | Runtime API | |---|---|---|---| | Ollama | You want the default local option and want NemoClaw to install, start, or use Ollama on supported hosts. | Appears when Ollama is installed or running, and the wizard can offer installation on supported hosts. | Ollama through the managed local route. | -| Existing vLLM | You already run vLLM on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. | Unavailable on native N1x FASTOS. On other hosts, it appears when NemoClaw detects the server. | `/v1/chat/completions`. | -| Managed vLLM | You want NemoClaw to pull an image, download model weights, and manage the server container. | Requires Docker on `PATH`. Appears by default on DGX Spark and DGX Station. Native N1x FASTOS offers this route as its only admitted provider through a Deferred preview. Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`. Attaching an already-running local vLLM server is separate. | `/v1/chat/completions`. | +| Existing vLLM | You already run vLLM on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. | Appears when NemoClaw detects the server. Native N1x requires explicit standard-onboarding intent, and this route remains unvalidated there. | `/v1/chat/completions`. | +| Managed vLLM | You want NemoClaw to pull an image, download model weights, and manage the server container. | Requires Docker on `PATH`. Appears by default on DGX Spark and DGX Station. Native N1x FASTOS offers this route through a Deferred preview. Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`. Attaching an already-running local vLLM server is separate. | `/v1/chat/completions`. | | Fixed vLLM profile | You need the fixed catalog-selected vLLM model and serving configuration. | Appears as option 2 in DGX Spark Express. Direct installation can use the dedicated installer flag. The profile does not appear in the provider menu. | `/v1/chat/completions`. | | Existing llama.cpp | You already operate an authenticated llama.cpp server on loopback port `8081`. | Experimental. Always available for explicit selection and attaches only after cooperative fingerprinting succeeds. | `/v1/chat/completions`. | | Managed llama.cpp | You want NemoClaw to acquire a verified GGUF file and manage an authenticated llama.cpp container. | Experimental. Lists compatible profiles on qualified DGX Spark hosts. Qualifying Windows WSL N1x Express hosts use the explicit Qwen 3.6 recipe. | `/v1/chat/completions`. | @@ -38,8 +38,8 @@ A qualifying Windows WSL N1x host instead uses the explicit managed llama.cpp re | Option | When to use it | Availability | Runtime API | |---|---|---|---| -| Existing vLLM | You already run vLLM on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. | Unavailable on native N1x FASTOS. On other hosts, it appears when NemoClaw detects the server. | `/v1/chat/completions`. | -| Managed vLLM | You want NemoClaw to pull an image, download model weights, and manage the server container. | Requires Docker on `PATH`. Appears by default on DGX Spark and DGX Station. Native N1x FASTOS offers this route as its only admitted provider through a Deferred preview. Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`. Attaching an already-running local vLLM server is separate. | `/v1/chat/completions`. | +| Existing vLLM | You already run vLLM on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. | Appears when NemoClaw detects the server. Native N1x requires explicit standard-onboarding intent, and this route remains unvalidated there. | `/v1/chat/completions`. | +| Managed vLLM | You want NemoClaw to pull an image, download model weights, and manage the server container. | Requires Docker on `PATH`. Appears by default on DGX Spark and DGX Station. Native N1x FASTOS offers this route through a Deferred preview. Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`. Attaching an already-running local vLLM server is separate. | `/v1/chat/completions`. | | Fixed vLLM profile | You need the fixed catalog-selected vLLM model and serving configuration. | Appears as option 2 in DGX Spark Express. Direct installation can use the dedicated installer flag. The profile does not appear in the provider menu. | `/v1/chat/completions`. | | Existing llama.cpp | You already operate an authenticated llama.cpp server on loopback port `8081`. | Experimental. Always available for explicit selection and attaches only after cooperative fingerprinting succeeds. | `/v1/chat/completions`. | | Managed llama.cpp | You want NemoClaw to acquire a verified GGUF file and manage an authenticated llama.cpp container. | Experimental. Lists compatible profiles on qualified DGX Spark hosts. Qualifying Windows WSL N1x Express hosts use the explicit Qwen 3.6 recipe. | `/v1/chat/completions`. | @@ -188,7 +188,7 @@ The managed procedure moved to [Set Up llama.cpp](set-up-llama-cpp#install-manag Choose NVIDIA NIM when you want a managed NIM container and your host has a NIM-capable NVIDIA GPU. This path is unavailable on N1x. -Use the [Deferred managed-vLLM preview](set-up-vllm#use-n1x-express) on N1x. +Select another standard provider or use the [Deferred managed-vLLM preview](set-up-vllm#use-n1x-express) on N1x. The path is experimental, requires NGC registry access, and can fail when a selected image does not publish a manifest for the host architecture. Refer to [Set Up NVIDIA NIM](set-up-nvidia-nim). diff --git a/docs/inference/set-up-vllm.mdx b/docs/inference/set-up-vllm.mdx index 3ee65fd5f78..b7cc66d8cb1 100644 --- a/docs/inference/set-up-vllm.mdx +++ b/docs/inference/set-up-vllm.mdx @@ -36,7 +36,8 @@ Follow [Set Up vLLM on Two DGX Sparks](set-up-vllm-on-two-dgx-sparks) or [Set Up ## Use an Existing Server -On hosts other than N1x, when vLLM is already running on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`, NemoClaw detects it and queries `/v1/models` for the loaded model. +When vLLM is already running on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`, NemoClaw detects it and queries `/v1/models` for the loaded model. +On N1x, decline or disable Express, or set `NEMOCLAW_PROVIDER=vllm`, to supply explicit standard-onboarding intent. Run onboarding. @@ -46,7 +47,7 @@ $$nemoclaw onboard Select **Local vLLM**. On generic hosts, the entry includes an experimental label. -On N1x, the readiness gate admits only explicit managed-preview intent, so the user-facing onboarding flow does not offer the existing-server entry. +On N1x, the existing-server route remains unvalidated even after standard-onboarding intent admits it. NemoClaw validates the detected endpoint and records the model reported by `/v1/models`. When you request a vLLM serving profile, the detected server must report the profile's served model alias or a safe `root` that matches the profile's declared model. @@ -152,12 +153,13 @@ $$nemoclaw onboard --profile --vllm-gpu-device The single-host managed vLLM runtime is host-global. Destroying a sandbox does not stop it, and the container remains available for other sandboxes. -On hosts other than N1x, later onboarding normally reuses a server that is already listening on the configured vLLM port instead of replacing it automatically. +Later standard onboarding normally reuses a server that is already listening on the configured vLLM port instead of replacing it automatically. When a managed GPU selection accompanies explicit managed-install intent, NemoClaw preserves that intent and reports the running-server conflict. Choose one action: -- On hosts other than N1x, to reuse the running server, omit `--vllm-gpu-device` and select **Local vLLM**. +- To reuse the running server, omit `--vllm-gpu-device` and select **Local vLLM**. + On N1x, decline or disable Express, or set `NEMOCLAW_PROVIDER=vllm`, before onboarding. - To change the GPU or port, inspect the sandbox registry for every gateway-port environment on the host. Run `NEMOCLAW_GATEWAY_PORT= $$nemoclaw list` for port `8080` and for every non-default port represented by a numeric directory under `~/.nemoclaw/gateways/`. Treat every sandbox that reports provider `vllm-local` as a possible consumer because the list output does not distinguish local vLLM from Local NIM. @@ -471,7 +473,10 @@ Generic DMI values, such as `SKU 1` and `JMJWOA-Generic`, do not establish N1x i N1x Express does not activate DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Accepting the preview prompt sets explicit managed-vLLM intent for the narrow pending-validation readiness exception. -Declining the prompt or setting only `NEMOCLAW_NO_EXPRESS=1` stops the installer before onboarding; neither path opens interactive provider selection. +Declining the prompt continues with ordinary interactive onboarding. +Setting `NEMOCLAW_NO_EXPRESS=1` also enters ordinary onboarding when no provider is preselected or the explicit provider is recognized and is not Local NVIDIA NIM. +An invalid or Local NVIDIA NIM provider remains blocked. +Set a recognized provider other than Local NVIDIA NIM to enter ordinary onboarding directly. Set `NEMOCLAW_PROVIDER=install-vllm` when you need to bypass the prompt with the same explicit preview intent. On a host that still meets the N1x identity checks, `rebuild` reuses the recorded route as explicit preview intent only when all of these conditions hold: @@ -545,13 +550,15 @@ If no server is running, it selects the managed install or start entry. If neither local entry is available, it selects NVIDIA Endpoints. On DGX Station and other hosts, an unset provider keeps NVIDIA Endpoints as the automatic default. -On hosts other than N1x, use an already-running server. +Use an already-running server. ```bash NEMOCLAW_PROVIDER=vllm \ $$nemoclaw onboard --non-interactive ``` +On N1x, this explicit provider supplies standard-onboarding intent, but the route remains unvalidated. + Install or start managed vLLM on a supported host, or explicitly enter the Deferred N1x preview. ```bash diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index 6d2fbc2b060..827ff7399cf 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -3622,7 +3622,7 @@ The following variables let you tune onboarding without editing the Dockerfile o | Variable | Format | Effect | | --- | --- | --- | -| `NEMOCLAW_PROVIDER` | provider key (e.g. `build`, `openrouter`, `openai`, `anthropic`, `anthropicCompatible`, `gemini`, `ollama`, `custom`, `vllm`, `nim-local`, `routed`, `hermes-provider`, `llama-cpp`, `install-llama-cpp`, `install-vllm`, `install-ollama`, `install-windows-ollama`, `start-windows-ollama`) | Selects the inference provider during onboarding. The wizard skips the provider menu in both interactive and non-interactive runs when this is set. On a qualified N1x host, only `install-vllm` supplies the explicit Deferred preview intent; another provider stops installation before onboarding. `llama-cpp` selects attachment of an authenticated, operator-managed llama.cpp server on loopback port `8081`. Set `NEMOCLAW_LLAMACPP_LOCAL_TOKEN`; set `NEMOCLAW_MODEL` to the served alias when the server exposes multiple models. `install-llama-cpp` selects the experimental DGX Spark managed path and rejects `NEMOCLAW_MODEL`; use `NEMOCLAW_LLAMACPP_RECIPE` for its declarative selection. If an operator-managed server does not provide consistent native llama.cpp evidence, select `custom`. Aliases: `cloud` → `build`, `open-router` / `openrouterai` → `openrouter`, `nim` → `nim-local`, `hermes` / `nous` / `nous-portal` → `hermes-provider`, `anthropiccompatible` → `anthropicCompatible`. Invalid values fail fast with the list of accepted keys. | +| `NEMOCLAW_PROVIDER` | provider key (e.g. `build`, `openrouter`, `openai`, `anthropic`, `anthropicCompatible`, `gemini`, `ollama`, `custom`, `vllm`, `nim-local`, `routed`, `hermes-provider`, `llama-cpp`, `install-llama-cpp`, `install-vllm`, `install-ollama`, `install-windows-ollama`, `start-windows-ollama`) | Selects the inference provider during onboarding. The wizard skips the provider menu in both interactive and non-interactive runs when this is set. On a qualified N1x host, `install-vllm` supplies Deferred preview intent, while another provider supplies standard-onboarding intent and retains its existing requirements. Local NVIDIA NIM remains unavailable on N1x. `llama-cpp` selects attachment of an authenticated, operator-managed llama.cpp server on loopback port `8081`. Set `NEMOCLAW_LLAMACPP_LOCAL_TOKEN`; set `NEMOCLAW_MODEL` to the served alias when the server exposes multiple models. `install-llama-cpp` selects the experimental DGX Spark managed path and rejects `NEMOCLAW_MODEL`; use `NEMOCLAW_LLAMACPP_RECIPE` for its declarative selection. If an operator-managed server does not provide consistent native llama.cpp evidence, select `custom`. Aliases: `cloud` → `build`, `open-router` / `openrouterai` → `openrouter`, `nim` → `nim-local`, `hermes` / `nous` / `nous-portal` → `hermes-provider`, `anthropiccompatible` → `anthropicCompatible`. Invalid values fail fast with the list of accepted keys. | | `NEMOCLAW_LLAMACPP_RECIPE` | repository-owned managed-inference recipe ID | Selects the managed llama.cpp recipe when `NEMOCLAW_PROVIDER=install-llama-cpp`, including a compatible lower-priority profile. When unset, NemoClaw selects the unique highest-priority compatible automatic profile. An unknown recipe, an ambiguous selection, or a stale or incompatible readiness report fails before image, model, or runtime effects. | | `NEMOCLAW_MODEL` | model ID | Selects an explicit model for a non-interactive onboarding run. NemoClaw preserves it across a detected provider switch, even when it matches the recorded provider's default. When this variable is unset during such a switch, NemoClaw ignores the `NEMOCLAW_PROVIDER_MODEL` compatibility fallback and uses normal provider model selection. | | `NEMOCLAW_TOOL_DISCLOSURE` | `progressive` or `direct` | Selects progressive tool discovery or the prior direct-exposure behavior outside the Portable experimental profile. Defaults to `progressive`; `--tool-disclosure` takes precedence when both are set. Fresh Portable onboarding ignores this variable and defaults to `direct` unless the flag explicitly selects a mode. | @@ -3843,7 +3843,7 @@ The following flags toggle optional behaviors during onboarding. Set them before | `NEMOCLAW_YES` | `1` to enable | Auto-accepts confirmation prompts (`--yes` equivalent) including in helpers like the Ollama proxy auth setup, but does not change managed-vLLM storage-warning handling. Express and other non-interactive setup stop after a verified insufficient-capacity warning, interactive setup still requires an explicit `y` or `yes`, and an inconclusive model-cache check stops non-interactive setup with guidance to rerun interactively. | | `NEMOCLAW_OLLAMA_NO_AUTOSTART` | `1` to enable | Skips the wizard's eager Ollama auto-start during inference-provider selection (equivalent to passing `--no-ollama-autostart`). When set and Ollama is not running on `localhost:11434`, an agent that uses the legacy `16384`-token context floor, currently OpenClaw, prints a warning and selects the default fallback model instead of spawning `ollama serve`. An agent that requires a larger verified runtime context, currently Hermes at `64000` tokens, returns to interactive provider selection or exits when the Ollama provider is pinned or onboarding is non-interactive. The flag covers only the provider-selection step; later setup steps (auth proxy, validation, model warm) still expect a reachable Ollama. On Linux hosts with a systemd Ollama unit, the loopback-override path may still restart the daemon before this gate runs. | | `NEMOCLAW_NON_INTERACTIVE_SUDO_MODE` | `prompt` or empty/unset | When set to `prompt`, allows non-interactive onboarding to use prompt-capable `sudo` for host setup steps that require elevation, which can ask for a password. Empty/unset is the default and uses `sudo -n`, which fails instead of asking for a password. Any other value is rejected. | -| `NEMOCLAW_NO_EXPRESS` | `1` to enable | Installer-only. Skips the DGX Spark, DGX Station, and Windows WSL express prompt, then continues with the normal interactive onboarding flow. On N1x, setting this variable alone stops installation before onboarding. Combine it with `NEMOCLAW_PROVIDER=install-vllm` only when you intend to bypass the preview prompt with explicit Deferred managed-vLLM intent. | +| `NEMOCLAW_NO_EXPRESS` | `1` to enable | Installer-only. Skips the express prompt, including the Deferred N1x preview, then continues with normal onboarding. On a qualified N1x host, this value supplies explicit standard-onboarding intent. Combine it with `NEMOCLAW_PROVIDER=install-vllm` only when you intend to bypass the preview prompt with Deferred managed-vLLM intent. | | `NEMOCLAW_EXPERIMENTAL` | `1` to enable | Surfaces experimental providers and flows in onboarding. | | `NEMOCLAW_IGNORE_RUNTIME_RESOURCES` | `1` to enable | Suppresses the under-provisioned runtime warning during preflight. Use only when you know the sandbox host meets the minimums. | | `NEMOCLAW_DISABLE_OVERLAY_FIX` | `1` to enable | Skips the Docker overlay-fix step during sandbox build. For environments where the fix is incompatible. | diff --git a/docs/reference/platform-support.mdx b/docs/reference/platform-support.mdx index e88076608b4..94fa5ee0e28 100644 --- a/docs/reference/platform-support.mdx +++ b/docs/reference/platform-support.mdx @@ -82,7 +82,7 @@ For install requirements and the shorter setup-oriented platform view, refer to | DGX OS (Station) | Docker | Tested with limitations | P1 | No | The PRD marks this platform as P1. Physical validation on one DGX Station GB300 covers generic Ubuntu 24.04 ARM64, stock DGX OS `7.5.0`, the April 2026 NVIDIA Colossus BaseOS profile, and the June 2026 NVIDIA AI Developer Tools profile. A physical no-OTA DGX OS `7.6.0` host provided the release and hardware profile used for its stable workstation-family classifier and passed read-only eligibility and runtime-command preflight. Full Station Express end-to-end qualification for the accepted no-OTA DGX OS `7.6.x` profile is pending. The profile remains subject to the same physical GB300, driver, ECC, Docker, CDI, and container GPU validation. Clean-host end-to-end validation passed on generic Ubuntu and Colossus BaseOS; stock DGX OS and AI Developer Tools completed Station Express validation. The DGX OS `7.5.0` run used released OpenShell `0.0.85`, local Nemotron Ultra serving, sandbox `cuInit(0)`, and a Hermes write/read file-tool task. A dual-Station configuration has not been validated, and dedicated CI coverage is not available. Direct-GPU policies expose only the exact read-only BDF directory for each discovered display-class PCI device with NVIDIA vendor ID (`0x10de`) and GB300 device ID (`0x31c2` or `0x31c3`) plus required existing topology and module paths; they do not expose `/sys`, the PCI parent subtree, or sysfs write access. During physical validation, reads of `/sys/fs/cgroup/cgroup.controllers` and `/sys/class/net/lo/address` remained denied. For canonical hardware qualification, image requirements, preparation, repair limits, reboot handoff, and the explicit temporary metadata override, see [Prepare DGX Station to Install NemoClaw](../get-started/additional-setup/dgx-station-preparation). | | Linux | Docker | Tested | P0 | Yes | Primary tested path. Ubuntu 24.04 has host-level onboarding validation. A digest-pinned Ubuntu 26.04 userspace lane builds the CLI and runs preflight, installer, and platform contracts on eligible main pushes; Docker-host, AppArmor, Landlock, and live onboarding validation on 26.04 remain pending. Other distros (Ubuntu 22.04, Fedora, Rocky, Alma, NixOS, Arch) may work but are not validated. | | macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | P0 | Yes | Start the container runtime (Colima or Docker Desktop) before installing; NemoClaw verifies the pinned official OpenShell formula and grants formula-scoped trust only around each Homebrew install, inspection, start, or stop operation, requires a legacy or changed formula to be repaired by rerunning the pinned installer, uses the standalone gateway only when Homebrew is absent or both the staged formula and installed keg are absent, requires Homebrew Colima users to install both Colima and the Docker CLI (`brew install colima docker`) before `docker info` can work, and recommends Xcode Command Line Tools (`xcode-select --install`) for Node native modules. | -| N1x FASTOS | Docker | Deferred | Unset | No | The accepted Deferred N1x Express preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. It does not activate DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Host identity requires Linux `arm64`, a regular `/etc/fastos-release` file of 1 through 4,096 bytes that is owned by UID 0 and GID 0, is not a symbolic link, is not group- or world-writable, and contains exactly one `NAME="N1x FASTOS"` line. It also requires NVIDIA vendor ID `10de` on a display-class PCI device. Generic DMI values are supplemental and FastOS version is not pinned. Physical CUDA and CDI checks passed on one N1x host, but full NemoClaw Express E2E validation and dedicated CI coverage are pending. `host.platform.supported` remains absent; only explicit managed-vLLM preview intent can waive the pending-validation finding. Do not claim N1x as supported until that validation passes and this row is promoted. | +| N1x FASTOS | Docker | Deferred | Unset | No | The accepted Deferred N1x Express preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. It does not activate DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Host identity requires Linux `arm64`, a regular `/etc/fastos-release` file of 1 through 4,096 bytes that is owned by UID 0 and GID 0, is not a symbolic link, is not group- or world-writable, and contains exactly one `NAME="N1x FASTOS"` line. It also requires NVIDIA vendor ID `10de` on a display-class PCI device. Generic DMI values are supplemental and FastOS version is not pinned. Physical CUDA and CDI checks passed on one N1x host, but full NemoClaw Express E2E validation and dedicated CI coverage are pending. `host.platform.supported` remains absent. After identity qualifies, accepting Express can waive only the pending-validation finding. Declining or disabling Express can apply the same waiver when no provider is preselected or the explicit provider is recognized and is not Local NVIDIA NIM. An invalid or Local NVIDIA NIM provider remains blocked. Existing provider requirements still apply, and this waiver does not promote N1x. Do not claim N1x as supported until validation passes and this row is promoted. | | NVIDIA RTX (consumer and Pro workstation GPUs) | Docker | Deferred | P1 | No | The PRD marks this platform as P1. Covers RTX consumer cards and RTX Pro workstation cards on Linux hosts that meet the generic-Linux-GPU requirements (NVIDIA Container Toolkit + CDI present). The provider menu emits managed vLLM behind `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm` for this host class today; the end-to-end onboard path on this hardware is not yet validated in CI. | | Windows WSL2 | Docker Desktop (WSL backend) | Tested with limitations | P1 | No | Requires WSL2 with Docker Desktop backend. Qualifying Windows WSL N1x hosts can use the explicit Experimental managed llama.cpp Qwen 3.6 recipe after local-default Docker authority, N1x product identity, ARM64, 48,000 MiB Docker and GPU memory, driver, NVIDIA integration, and Docker Desktop GPU-passthrough checks pass. Other Windows WSL hosts retain Windows-host or WSL-local Ollama. Dedicated N1x WSL CI and physical end-to-end validation are pending. | {/* platform-matrix-full:end */} @@ -105,8 +105,8 @@ NemoClaw routes inference through the OpenShell gateway. Each row below is a pro | 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 llama.cpp (already running) | Experimental | Local OpenAI-compatible | Attaches only to an authenticated operator-managed server on loopback port `8081` that passes cooperative native llama.cpp fingerprinting. NemoClaw does not own its server, model, or lifecycle. | | Local llama.cpp (managed DGX Spark) | Experimental | Local OpenAI-compatible | Uses the repository-owned YAML recipe for NVIDIA Nemotron 3 Nano 30B-A3B, exact GGUF verification, and Docker lifecycle on a qualified DGX Spark host. The recipe remains unqualified for agent support. | -| 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 healthy CDI spec. Onboarding evaluates the canonical `host.gpu.nvidia_available`, `host.gpu.container_toolkit_available`, and `host.gpu.cdi_healthy` readiness capabilities before gateway, image, or sandbox lifecycle effects. NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag. Onboarding filters the validated image list by usable memory. In non-interactive mode, onboarding selects the first compatible model unless the operator sets `NEMOCLAW_MODEL`. Model selection uses detected free GPU memory. If free memory is unavailable, NemoClaw uses total GPU memory. On DGX Spark, NemoClaw caps usable memory at 50 percent of total memory to match NIM's reported unified-memory limit. The Nemotron 3 Super catalog minimum includes NIM's reported runtime allocation. Local NVIDIA NIM is unavailable on N1x. NemoClaw omits the provider from onboarding and rejects `NEMOCLAW_PROVIDER=nim-local` on N1x. Use the Deferred managed-vLLM preview on N1x. On Linux arm64 DGX Spark and DGX Station hosts, onboarding warns that some NIM images may not publish a `linux/arm64` manifest; the warning is advisory, and the selected image pull can still fail when the registry has no matching platform manifest. 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 | Unavailable on N1x. On other hosts, it appears in the onboarding menu when NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. No flag is required. The model is whatever the existing server serves. | +| 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 healthy CDI spec. Onboarding evaluates the canonical `host.gpu.nvidia_available`, `host.gpu.container_toolkit_available`, and `host.gpu.cdi_healthy` readiness capabilities before gateway, image, or sandbox lifecycle effects. NIM images pull from `nvcr.io` and require NGC registry login. NemoClaw gates this path behind the experimental flag. Onboarding filters the validated image list by usable memory. In non-interactive mode, onboarding selects the first compatible model unless the operator sets `NEMOCLAW_MODEL`. Model selection uses detected free GPU memory. If free memory is unavailable, NemoClaw uses total GPU memory. On DGX Spark, NemoClaw caps usable memory at 50 percent of total memory to match NIM's reported unified-memory limit. The Nemotron 3 Super catalog minimum includes NIM's reported runtime allocation. Local NVIDIA NIM is unavailable on N1x. NemoClaw omits the provider from onboarding and rejects `NEMOCLAW_PROVIDER=nim-local` on N1x. Select another standard provider or use the Deferred managed-vLLM preview on N1x. On Linux arm64 DGX Spark and DGX Station hosts, onboarding warns that some NIM images may not publish a `linux/arm64` manifest; the warning is advisory, and the selected image pull can still fail when the registry has no matching platform manifest. 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 | On a qualified N1x host, this route requires explicit standard-onboarding intent and remains unvalidated. On other hosts, no explicit intent is required. It appears in the onboarding menu when NemoClaw detects a server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. The model is whatever the existing server serves. | | Local vLLM (managed install/start) | Tested with limitations | Local OpenAI-compatible | Managed install/start requires Docker on `PATH`; attaching an already-running local vLLM server is a separate path. Appears by default on DGX Spark and qualifying DGX Station GB300 hosts. A labeled Deferred preview appears on hosts that match the N1x identity requirements; N1x remains Deferred until a physical NemoClaw Express E2E test passes. DGX Station is Tested with limitations across qualified profiles on one physical DGX Station GB300. Full Station Express end-to-end qualification for the accepted no-OTA DGX OS `7.6.x` profile is pending. Dual-Station configurations are not yet validated, and dedicated CI coverage is not available. For canonical Station qualification and host preparation, see the Additional Setup page for [OpenClaw](/user-guide/openclaw/get-started/additional-setup/dgx-station-preparation), [Hermes](/user-guide/hermes/get-started/additional-setup/dgx-station-preparation), or [Deep Agents](/user-guide/deepagents/get-started/additional-setup/dgx-station-preparation). Generic Linux NVIDIA GPU hosts require `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm`, NVIDIA Container Toolkit, and CDI. NemoClaw pins runtime images to immutable digests. Station Express defaults to `nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-NVFP4`; `--station-deepseek` selects `deepseek-ai/DeepSeek-V4-Flash`. Direct managed-vLLM defaults are `nvidia/Qwen3.6-35B-A3B-NVFP4` on DGX Spark and the Deferred N1x preview, `deepseek-ai/DeepSeek-V4-Flash` on DGX Station, and `nvidia/NVIDIA-Nemotron-3-Nano-4B-FP8` on generic Linux NVIDIA GPU hosts. Image pulls from `nvcr.io` require NGC registry login. | {/* provider-status-full:end */} diff --git a/docs/reference/system-readiness.mdx b/docs/reference/system-readiness.mdx index d9cc8654bb7..94c9be4238f 100644 --- a/docs/reference/system-readiness.mdx +++ b/docs/reference/system-readiness.mdx @@ -177,12 +177,14 @@ Generic DMI values do not satisfy either requirement. A matching identity produces a `qualified` `host.platform.n1x` qualification, but `host.platform.supported` remains `absent` while N1x is Deferred. The blocking `host.platform.n1x_validation_pending` finding records the remaining physical NemoClaw Express E2E gate. -Explicit `NEMOCLAW_PROVIDER=install-vllm` intent, including acceptance of the installer N1x preview prompt, lets onboarding waive only that finding after `host.platform.n1x` is `present`. +After `host.platform.n1x` is `present`, accepting Express lets onboarding waive only that finding. +Declining or disabling Express can apply the same waiver when no provider is preselected or the explicit provider is recognized and is not Local NVIDIA NIM. +An invalid or Local NVIDIA NIM provider remains blocked. The read-only `host probe` report remains `incompatible` and does not apply that onboarding exception. Invalid marker metadata, invalid or ambiguous contents, the wrong PCI identity, the wrong architecture, or an unavailable NVIDIA GPU produces an `unqualified` qualification and the blocking `host.platform.n1x_unqualified` finding. Unreadable marker or PCI evidence produces an `unknown` qualification and the blocking `host.platform.n1x_inconclusive` finding. -Neither finding can be waived by managed-vLLM intent. +Neither finding can be waived by onboarding intent. Bounded evidence appears under `host.platform.identity` with `product`, `nvidiaPlatform`, `n1xCandidate`, `n1xFastOsMarker`, and `n1xPciGpu` fields. ## Interpret NVIDIA GPU Integration diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index 6179f966f91..0397ad8d973 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -2040,7 +2040,7 @@ Ollama can serve local chat and some simple tool surfaces, but agent loops with | Long system prompts or sender metadata | Risky | Yes | | Multi-turn tool dispatch | Risky | Yes | -On hosts other than N1x, [set up vLLM](../inference/local-inference/set-up-vllm) with automatic tool choice and the tool-call parser that matches the model family for persistent agent use. After the parser-aware server is ready, re-run onboarding. Select the **Local vLLM** entry marked **running (suggested)** for a server detected on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`; for another address, select **Other OpenAI-compatible endpoint**. On generic hosts, the Local vLLM entry includes an experimental label; on DGX Spark or DGX Station, it does not. On N1x, stop any server that occupies `${NEMOCLAW_VLLM_PORT:-8000}`, then use only the Deferred managed-vLLM preview; existing and compatible servers are not admitted. +[Set up vLLM](../inference/local-inference/set-up-vllm) with automatic tool choice and the tool-call parser that matches the model family for persistent agent use. After the parser-aware server is ready, re-run onboarding. Select the **Local vLLM** entry for a server detected on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`; for another address, select **Other OpenAI-compatible endpoint**. On generic hosts, the Local vLLM entry includes an experimental label; on DGX Spark or DGX Station, it does not. On N1x, decline or disable Express, or set `NEMOCLAW_PROVIDER=vllm`, to enter standard onboarding; this route remains unvalidated on N1x. Do not rely on direct edits to `openclaw.json` for a persistent provider change. NemoClaw-managed rebuilds can overwrite those edits, while onboarding keeps the sandbox image, OpenShell route, and host-managed credentials aligned. diff --git a/docs/resources/starter-prompt.md b/docs/resources/starter-prompt.md index e0c0c9c4adb..5de05610ffa 100644 --- a/docs/resources/starter-prompt.md +++ b/docs/resources/starter-prompt.md @@ -89,17 +89,17 @@ Do not load a platform asset for any other computer. ## Runtime and Provider Selection If the Windows WSL platform asset applies, follow that asset's provider selection and skip the native-N1x branch below. -If the readiness check confirms native N1x (not Windows WSL), do not show the generic provider menu. -Offer only the Deferred managed-vLLM preview with `NEMOCLAW_PROVIDER=install-vllm`. +If the readiness check confirms native N1x (not Windows WSL), offer the Deferred managed-vLLM preview before the generic provider menu. +If the user accepts, set `NEMOCLAW_PROVIDER=install-vllm`. Explain that N1x remains outside the supported-platform set pending complete physical NemoClaw Express E2E validation, accepting this path is explicit preview intent, and the preview uses one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. -Do not offer or reuse an existing vLLM server on N1x. -If the configured vLLM port, `${NEMOCLAW_VLLM_PORT:-8000}`, is occupied, stop and ask the user to stop that server before trying the Deferred preview again. -If the user declines the preview, stop before installation. +If the user declines, continue to the provider question below and explain that each provider retains its existing requirements. +Do not offer local NVIDIA NIM on native N1x. +If the user selects managed vLLM and the configured port, `${NEMOCLAW_VLLM_PORT:-8000}`, is occupied, stop and ask the user to stop that server before trying the Deferred preview again. -For a computer other than native N1x, if no platform asset applies or its offered install path is declined, ask: "Which inference runtime or provider would you like?" +If no platform asset applies or its offered install path is declined, ask: "Which inference runtime or provider would you like?" Choices: -1. Existing vLLM, only when the computer is not N1x and a ready server is detected on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`. +1. Existing vLLM, when a ready server is detected on `localhost:${NEMOCLAW_VLLM_PORT:-8000}`; native N1x requires explicit standard-onboarding intent. 2. Managed vLLM, optimized local inference with a large download. 3. Local Ollama, only when the selected agent and platform support it. 4. NVIDIA Endpoints, which requires an NVIDIA API key. @@ -191,8 +191,8 @@ Use this provider mapping for non-interactive setup: - OpenAI-compatible: `NEMOCLAW_PROVIDER=custom`, endpoint, model, `COMPATIBLE_API_KEY`. - Anthropic-compatible: `NEMOCLAW_PROVIDER=anthropicCompatible`, endpoint, model, `COMPATIBLE_ANTHROPIC_API_KEY`. - Ollama: `NEMOCLAW_PROVIDER=ollama`, optional `NEMOCLAW_MODEL`. -- Existing vLLM: `NEMOCLAW_PROVIDER=vllm`; unavailable on native N1x. -- Managed vLLM: `NEMOCLAW_PROVIDER=install-vllm`; this is the only admitted native-N1x provider and requires explicit Deferred preview intent there. Qualifying N1x WSL hosts instead follow the Windows WSL asset's managed llama.cpp path. Use an approved optional model override only when the selected platform supports it. +- Existing vLLM: `NEMOCLAW_PROVIDER=vllm`; on native N1x, this value supplies explicit standard-onboarding intent, but the route remains unvalidated. +- Managed vLLM: `NEMOCLAW_PROVIDER=install-vllm`; on native N1x, this value supplies explicit Deferred preview intent. Qualifying N1x WSL hosts instead follow the Windows WSL asset's managed llama.cpp path. Use an approved optional model override only when the selected platform supports it. Do not offer Hermes Provider for OpenClaw or Deep Agents. diff --git a/docs/security/best-practices.mdx b/docs/security/best-practices.mdx index 6bf2cb90e9b..c99295dc16b 100644 --- a/docs/security/best-practices.mdx +++ b/docs/security/best-practices.mdx @@ -331,19 +331,20 @@ Review the data policies of any cloud provider you use. The `NEMOCLAW_EXPERIMENTAL=1` environment variable gates local NVIDIA NIM on eligible hosts other than native N1x FASTOS and generic Linux managed vLLM install/start. Managed vLLM install/start is Docker-backed and requires Docker on `PATH`; attaching an already-running local vLLM server is separate. DGX Spark and DGX Station managed vLLM entries appear by default when Docker is available. -Native N1x FASTOS omits local NVIDIA NIM and offers only the Deferred managed-vLLM preview. +Native N1x FASTOS omits local NVIDIA NIM and offers the Deferred managed-vLLM preview or explicitly selected standard onboarding. Native N1x FASTOS remains outside the supported-platform set because its physical NemoClaw Express E2E validation is incomplete. -After NemoClaw qualifies the native N1x identity, you must provide explicit managed-vLLM preview intent before onboarding can use this path. +After NemoClaw qualifies the native N1x identity, you must provide explicit Express or standard-onboarding intent. A qualifying Windows WSL N1x host uses managed llama.cpp through Windows Express. The installer uses local Docker Desktop, Arm64, the Windows N1x product identity, and at least 48,000 MiB of GPU memory as preliminary selection conditions. Onboarding also requires the default local Docker context, at least 48,000 MiB of Docker memory, driver version `580.65.06` or later, Docker storage and runtime readiness, NVIDIA GPU integration, and a successful Docker Desktop GPU passthrough proof. Onboarding stops managed selection when any required readiness check fails. Other Windows WSL hosts retain Windows-host or WSL-local Ollama. -On hosts other than native N1x FASTOS, an already-running vLLM server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}` also appears in the menu without a flag because selecting it is an explicit user action. +An already-running vLLM server on `localhost:${NEMOCLAW_VLLM_PORT:-8000}` appears in standard onboarding because selecting it is an explicit user action. +Native N1x requires you to decline or disable Express or set `NEMOCLAW_PROVIDER=vllm` first. | Aspect | Detail | |---|---| -| Default | Local NVIDIA NIM and generic Linux managed vLLM install/start are hidden. When Docker is available, DGX Spark and DGX Station managed vLLM entries are offered by default. On hosts other than native N1x FASTOS, already-running vLLM on `localhost:${NEMOCLAW_VLLM_PORT:-8000}` is offered when detected. Qualifying Windows WSL N1x Express runs use managed llama.cpp. | +| Default | Local NVIDIA NIM and generic Linux managed vLLM install/start are hidden. When Docker is available, DGX Spark and DGX Station managed vLLM entries are offered by default. Standard onboarding offers already-running vLLM on `localhost:${NEMOCLAW_VLLM_PORT:-8000}` when detected; native N1x requires explicit standard-onboarding intent. Qualifying Windows WSL N1x Express runs use managed llama.cpp. | | What you can change | On eligible hosts other than native N1x FASTOS, set `NEMOCLAW_EXPERIMENTAL=1` before onboarding to surface Local NIM and, when Docker is available on `PATH`, generic Linux managed vLLM. To request only the Docker-backed managed vLLM path non-interactively, set `NEMOCLAW_PROVIDER=install-vllm`. | | Risk if selected | NemoClaw has not fully validated these providers. NIM requires a NIM-capable GPU. The managed vLLM path requires Docker, pulls a container image, and starts it on a supported NVIDIA GPU host. Misconfiguration can cause failed inference or unexpected behavior. | | Recommendation | Use experimental providers only for evaluation. Do not rely on them for always-on assistants. | diff --git a/scripts/install.sh b/scripts/install.sh index 566361503fd..c2cc9098325 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -3683,7 +3683,10 @@ run_installer_host_preflight() { try { const { assessHost, planHostAdvisories } = require(preflightPath); const { createHostReadinessReport } = require(hostReadinessPath); - const { evaluateOnboardReadinessAdmission } = require(onboardAdmissionPath); + const { + evaluateOnboardReadinessAdmission, + hasExplicitDeferredN1xOnboardingIntent, + } = require(onboardAdmissionPath); const { loadGatewayManagementDeclaration } = require(gatewayManagementPath); const { configuredRuntimeProviderOwnsHostReadiness } = require(gatewayRuntimePath); const host = assessHost(); @@ -3718,9 +3721,7 @@ run_installer_host_preflight() { // authoritative onboarding gate apply supported storage remediation, // but only when the gateway declaration confirms NemoClaw ownership. allowStorageRemediation, - allowDeferredN1xManagedVllm: - process.env.NEMOCLAW_PROVIDER === "install-vllm" || - process.env.NEMOCLAW_NO_EXPRESS === "1", + allowDeferredN1xManagedVllm: hasExplicitDeferredN1xOnboardingIntent(process.env), }); const infoLines = []; const actionLines = []; diff --git a/src/lib/onboard/fatal-runtime-preflight.ts b/src/lib/onboard/fatal-runtime-preflight.ts index fbe2ada7f58..42a92c17e6a 100644 --- a/src/lib/onboard/fatal-runtime-preflight.ts +++ b/src/lib/onboard/fatal-runtime-preflight.ts @@ -21,6 +21,7 @@ import { import { evaluateOnboardGatewayReadinessAdmission, evaluateOnboardReadinessAdmission, + hasExplicitDeferredN1xOnboardingIntent, } from "../readiness/onboard-admission"; import { composeSystemReadinessReport } from "../readiness/system"; import type { SystemReadinessReport } from "../readiness/types"; @@ -46,7 +47,6 @@ import { validateSandboxGpuPreflight, } from "./sandbox-gpu-preflight"; import type { OnboardOptions } from "./types"; -import { MANAGED_VLLM_PROVIDER_KEY } from "./vllm-menu"; export type FatalRuntimePreflightOptions = Pick< OnboardOptions, @@ -179,9 +179,7 @@ export function assertOnboardSystemReadiness( allowStorageRemediation: options.allowStorageRemediation === true, allowPortableHostPreparation: options.allowPortableHostPreparation, allowDeferredN1xManagedVllm: - options.allowDeferredN1xManagedVllm ?? - (process.env.NEMOCLAW_PROVIDER === MANAGED_VLLM_PROVIDER_KEY || - process.env.NEMOCLAW_NO_EXPRESS === "1"), + options.allowDeferredN1xManagedVllm ?? hasExplicitDeferredN1xOnboardingIntent(process.env), }); const advisories = planHostAdvisories(host, { providerOwnsHostReadiness: selectedRuntimeOwnsHostReadiness, diff --git a/src/lib/readiness/onboard-admission.test.ts b/src/lib/readiness/onboard-admission.test.ts index 0892a931b61..82a683bfc2b 100644 --- a/src/lib/readiness/onboard-admission.test.ts +++ b/src/lib/readiness/onboard-admission.test.ts @@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest"; import { evaluateOnboardGatewayReadinessAdmission, evaluateOnboardReadinessAdmission, + hasExplicitDeferredN1xOnboardingIntent, ONBOARD_READINESS_ADMISSION_REASON_IDS, ONBOARD_READINESS_FINDING_IDS, ONBOARD_REQUIRED_CAPABILITY_IDS, @@ -23,6 +24,18 @@ const DEFAULT_OPTIONS: OnboardReadinessAdmissionOptions = { allowStorageRemediation: true, }; +describe("Deferred N1x onboarding intent", () => { + it.each([ + ["managed-vLLM", { NEMOCLAW_PROVIDER: "install-vllm" }, true], + ["ordinary onboarding", { NEMOCLAW_NO_EXPRESS: "1" }, true], + ["another provider without installer state", { NEMOCLAW_PROVIDER: "ollama" }, false], + ["an unsupported opt-out value", { NEMOCLAW_NO_EXPRESS: "true" }, false], + ["no intent", {}, false], + ] as const)("recognizes %s (#11041)", (_scenario, env, expected) => { + expect(hasExplicitDeferredN1xOnboardingIntent(env)).toBe(expected); + }); +}); + function capability(id: string, state: ReadinessCapability["state"]): ReadinessCapability { return { id, state }; } @@ -242,10 +255,7 @@ describe("onboarding readiness admission (#7411)", () => { ONBOARD_REQUIRED_CAPABILITY_IDS.dockerRuntimeSupported, ONBOARD_REQUIRED_CAPABILITY_IDS.dockerStorageCompatible, ONBOARD_REQUIRED_CAPABILITY_IDS.dockerStorageRemediationAvailable, - ].reduce( - (current, id) => withCapabilityState(current, id, "unknown"), - requiredCapabilities(), - ); + ].reduce((current, id) => withCapabilityState(current, id, "unknown"), requiredCapabilities()); const dockerFindings = [ finding(ONBOARD_READINESS_FINDING_IDS.dockerUnavailable), finding(ONBOARD_READINESS_FINDING_IDS.dockerHostInvalid), @@ -286,13 +296,11 @@ describe("onboarding readiness admission (#7411)", () => { ).toMatchObject({ admitted: false, findingIds: ["host.example.blocked"] }); }); - it.each( - [ - ONBOARD_REQUIRED_CAPABILITY_IDS.dockerRuntimeSupported, - ONBOARD_REQUIRED_CAPABILITY_IDS.dockerStorageCompatible, - ONBOARD_REQUIRED_CAPABILITY_IDS.dockerStorageRemediationAvailable, - ], - )( + it.each([ + ONBOARD_REQUIRED_CAPABILITY_IDS.dockerRuntimeSupported, + ONBOARD_REQUIRED_CAPABILITY_IDS.dockerStorageCompatible, + ONBOARD_REQUIRED_CAPABILITY_IDS.dockerStorageRemediationAvailable, + ])( "admits only the pre-mutation facts that portable host preparation can replace [case %#]", (id) => { let capabilities = withCapabilityState( diff --git a/src/lib/readiness/onboard-admission.ts b/src/lib/readiness/onboard-admission.ts index e2ba71e6212..415f404a271 100644 --- a/src/lib/readiness/onboard-admission.ts +++ b/src/lib/readiness/onboard-admission.ts @@ -3,6 +3,13 @@ import type { ReadinessCapability, ReadinessFinding, SystemReadinessReport } from "./types"; +/** Return whether installer state records an accepted Deferred N1x onboarding path. */ +export function hasExplicitDeferredN1xOnboardingIntent( + env: Readonly>, +): boolean { + return env.NEMOCLAW_PROVIDER === "install-vllm" || env.NEMOCLAW_NO_EXPRESS === "1"; +} + export const ONBOARD_READINESS_ADMISSION_REASON_IDS = { blockingFindings: "onboard.readiness.blocking_findings", requiredCapabilitiesUnknown: "onboard.readiness.required_capabilities_unknown", @@ -53,7 +60,7 @@ export interface OnboardReadinessAdmissionOptions { allowStorageRemediation: boolean; /** The explicit portable profile may prepare its rootless runtime before revalidation. */ allowPortableHostPreparation?: boolean; - /** Explicit managed-vLLM intent may exercise the Deferred N1x validation path. */ + /** Explicit accepted onboarding intent may exercise the Deferred N1x validation path. */ allowDeferredN1xManagedVllm?: boolean; } diff --git a/test/helpers/installer-express-prompt-pty-harness.ts b/test/helpers/installer-express-prompt-pty-harness.ts index 3e232eace25..e1d6593495a 100644 --- a/test/helpers/installer-express-prompt-pty-harness.ts +++ b/test/helpers/installer-express-prompt-pty-harness.ts @@ -64,11 +64,11 @@ classify_dgx_station_release() { printf "%s" "\${EXPRESS_RELEASE_STATE:-generic- station_installer_revision() { printf 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; } station_express_resume_generation() { printf '0123456789abcdef0123456789abcdef'; } bash() { - printf "RESULT NON_INTERACTIVE=%s SUDO_MODE=%s PROVIDER=%s MODEL=%s VLLM_MODEL=%s POLICY=%s YES=%s SANDBOX=%s STATION_EXPRESS=%s PROFILE_GATE=%s PROFILE_RUNTIME=%s SPARK_SELECTION=%s\\n" \ + printf "RESULT NON_INTERACTIVE=%s SUDO_MODE=%s PROVIDER=%s MODEL=%s VLLM_MODEL=%s POLICY=%s YES=%s SANDBOX=%s STATION_EXPRESS=%s PROFILE_GATE=%s PROFILE_RUNTIME=%s SPARK_SELECTION=%s NO_EXPRESS=%s\\n" \ "\${NON_INTERACTIVE:-}" "\${NEMOCLAW_NON_INTERACTIVE_SUDO_MODE:-}" "\${NEMOCLAW_PROVIDER:-}" "\${NEMOCLAW_MODEL:-}" \ "\${NEMOCLAW_VLLM_MODEL:-}" "\${NEMOCLAW_POLICY_MODE:-}" "\${NEMOCLAW_YES:-}" "\${NEMOCLAW_SANDBOX_NAME:-}" \ "\${NEMOCLAW_STATION_EXPRESS:-}" "\${NEMOCLAW_ENABLE_LOCAL_MODEL_PROFILE:-}" "\${NEMOCLAW_LOCAL_MODEL_RUNTIME:-}" \ - "\${_SPARK_EXPRESS_INFERENCE_SELECTION:-}" + "\${_SPARK_EXPRESS_INFERENCE_SELECTION:-}" "\${NEMOCLAW_NO_EXPRESS:-}" exit 0 } main "$@" @@ -85,11 +85,11 @@ if [ "\${FORCE_EXPRESS_PROMPT_READ_FAILURE:-}" = "1" ]; then read() { return 1; } fi maybe_offer_express_install -printf "RESULT NON_INTERACTIVE=%s SUDO_MODE=%s PROVIDER=%s MODEL=%s VLLM_MODEL=%s POLICY=%s YES=%s SANDBOX=%s STATION_EXPRESS=%s PROFILE_GATE=%s PROFILE_RUNTIME=%s SPARK_SELECTION=%s\\n" \\ +printf "RESULT NON_INTERACTIVE=%s SUDO_MODE=%s PROVIDER=%s MODEL=%s VLLM_MODEL=%s POLICY=%s YES=%s SANDBOX=%s STATION_EXPRESS=%s PROFILE_GATE=%s PROFILE_RUNTIME=%s SPARK_SELECTION=%s NO_EXPRESS=%s\\n" \\ "\${NON_INTERACTIVE:-}" "\${NEMOCLAW_NON_INTERACTIVE_SUDO_MODE:-}" "\${NEMOCLAW_PROVIDER:-}" "\${NEMOCLAW_MODEL:-}" \\ "\${NEMOCLAW_VLLM_MODEL:-}" "\${NEMOCLAW_POLICY_MODE:-}" "\${NEMOCLAW_YES:-}" "\${NEMOCLAW_SANDBOX_NAME:-}" \\ "\${NEMOCLAW_STATION_EXPRESS:-}" "\${NEMOCLAW_ENABLE_LOCAL_MODEL_PROFILE:-}" "\${NEMOCLAW_LOCAL_MODEL_RUNTIME:-}" \\ - "\${_SPARK_EXPRESS_INFERENCE_SELECTION:-}" + "\${_SPARK_EXPRESS_INFERENCE_SELECTION:-}" "\${NEMOCLAW_NO_EXPRESS:-}" ''' env = dict(os.environ) env["INSTALLER_UNDER_TEST"] = installer diff --git a/test/helpers/installer-readiness-stubs.ts b/test/helpers/installer-readiness-stubs.ts index f352079265f..d045e5e19cd 100644 --- a/test/helpers/installer-readiness-stubs.ts +++ b/test/helpers/installer-readiness-stubs.ts @@ -84,6 +84,8 @@ export function writeInstallerReadinessModuleStubs(readinessDir: string): void { waivedFindingIds: [], }; }; +exports.hasExplicitDeferredN1xOnboardingIntent = (env) => + env.NEMOCLAW_PROVIDER === "install-vllm" || env.NEMOCLAW_NO_EXPRESS === "1"; `, ); fs.writeFileSync( diff --git a/test/install/install-express-n1x.test.ts b/test/install/install-express-n1x.test.ts index e4c1e29d05c..194ceae1d40 100644 --- a/test/install/install-express-n1x.test.ts +++ b/test/install/install-express-n1x.test.ts @@ -37,19 +37,20 @@ describe("installer N1x Express preview", () => { expect(result.status, output).toBe(0); expect(output).toMatch(/Skipping express install\. Continuing with interactive flow/); - expect(output).toMatch(/RESULT /); + expect(output).toMatch(/RESULT .*NO_EXPRESS=1/); }); it.each([ - ["NEMOCLAW_NO_EXPRESS", { NEMOCLAW_NO_EXPRESS: "1" }], - ["an explicit provider", { NEMOCLAW_PROVIDER: "ollama" }], - ])("continues with ordinary onboarding for %s (#11041)", (_scenario, env) => { + ["NEMOCLAW_NO_EXPRESS", { NEMOCLAW_NO_EXPRESS: "1" }, ""], + ["an explicit provider", { NEMOCLAW_PROVIDER: "ollama" }, "ollama"], + ])("continues with ordinary onboarding for %s (#11041)", (_scenario, env, provider) => { const result = runExpressPromptWithTty("", "pipe", "N1x", env); const output = `${result.stdout}${result.stderr}`; expect(result.status, output).toBe(0); expect(output).toMatch(/Skipping express prompt/); - expect(output).toMatch(/RESULT /); + expect(output).toMatch(/RESULT .*NO_EXPRESS=1/); + expect(output).toContain(`PROVIDER=${provider} `); }); it("allows the N1x prompt bypass with explicit managed-vLLM intent (#8574)", () => { @@ -93,16 +94,37 @@ detect_express_platform }); it.each([ - ["exact marker", 'NAME="DGX SPARK FASTOS"\nVERSION="1.23.0"\n', "81a4:0:0:644:64:1:2", "file", "DGX Spark"], + [ + "exact marker", + 'NAME="DGX SPARK FASTOS"\nVERSION="1.23.0"\n', + "81a4:0:0:644:64:1:2", + "file", + "DGX Spark", + ], ["unquoted marker", "NAME=DGX SPARK FASTOS\n", "81a4:0:0:644:64:1:2", "file", ""], - ["duplicate marker", 'NAME="DGX SPARK FASTOS"\nNAME="DGX SPARK FASTOS"\n', "81a4:0:0:644:64:1:2", "file", ""], + [ + "duplicate marker", + 'NAME="DGX SPARK FASTOS"\nNAME="DGX SPARK FASTOS"\n', + "81a4:0:0:644:64:1:2", + "file", + "", + ], ["unknown marker", 'NAME="OTHER FASTOS"\n', "81a4:0:0:644:64:1:2", "file", ""], ["non-root-owned marker", 'NAME="DGX SPARK FASTOS"\n', "81a4:1000:0:644:64:1:2", "file", ""], ["writable marker", 'NAME="DGX SPARK FASTOS"\n', "81a4:0:0:666:64:1:2", "file", ""], - ["oversized marker", 'NAME="DGX SPARK FASTOS"\n'.padEnd(4097, "x"), "81a4:0:0:644:4097:1:2", "file", ""], + [ + "oversized marker", + 'NAME="DGX SPARK FASTOS"\n'.padEnd(4097, "x"), + "81a4:0:0:644:4097:1:2", + "file", + "", + ], ["linked marker", 'NAME="DGX SPARK FASTOS"\n', "81a4:0:0:644:64:1:2", "link", ""], - ] as const)("routes an OEM FastOS marker only when trusted: %s (#10717)", (_scenario, contents, metadata, markerKind, expected) => { - const result = runInstallerSourced(` + ] as const)( + "routes an OEM FastOS marker only when trusted: %s (#10717)", + (_scenario, contents, metadata, markerKind, expected) => { + const result = runInstallerSourced( + ` test_marker="$HOME/fastos-release" test_target="$HOME/fastos-release-target" printf '%s' "$MARKER_CONTENT" >"$test_target" @@ -128,10 +150,13 @@ stat() { printf '%s' "$MARKER_METADATA"; } is_wsl_host() { return 1; } n1x_fastos_release_path() { printf '%s' "$test_marker"; } detect_express_platform -`, { MARKER_CONTENT: contents, MARKER_KIND: markerKind, MARKER_METADATA: metadata }); +`, + { MARKER_CONTENT: contents, MARKER_KIND: markerKind, MARKER_METADATA: metadata }, + ); - expect(result.result.stdout).toBe(expected); - }); + expect(result.result.stdout).toBe(expected); + }, + ); it("preserves harness-owned environment paths", () => { const result = runInstallerSourced( diff --git a/test/package-contract/installer-host-preflight.test.ts b/test/package-contract/installer-host-preflight.test.ts index 95e80aee7b3..6cb87ac44bd 100644 --- a/test/package-contract/installer-host-preflight.test.ts +++ b/test/package-contract/installer-host-preflight.test.ts @@ -30,6 +30,7 @@ exit 99`, function runInstallerHostAdmissionTest( host: { runtime: string; + isN1x?: boolean; hasNestedOverlayConflict?: boolean; isUnsupportedRuntime?: boolean; additionalFindingIds?: string[]; @@ -42,6 +43,8 @@ function runInstallerHostAdmissionTest( gatewayManagementMode?: string; portableProfileArtifact?: "present" | "missing"; providerResolutionFailure?: string; + provider?: string; + noExpress?: boolean; } = {}, ) { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-installer-host-admission-")); @@ -156,6 +159,11 @@ exports.evaluateOnboardReadinessAdmission = (report, options) => { ]); const findingIds = report.findings .filter((finding) => { + if ( + finding.id === "host.platform.n1x_validation_pending" && + options.allowDeferredN1xManagedVllm && + report.host.isN1x + ) return false; if ( options.providerOwnsHostReadiness && providerOwnedDockerFindings.has(finding.id) @@ -188,6 +196,8 @@ exports.evaluateOnboardReadinessAdmission = (report, options) => { ? { admitted: true, waivedFindingIds: [] } : { admitted: false, reasonIds: [], findingIds, capabilityIds, waivedFindingIds: [] }; }; +exports.hasExplicitDeferredN1xOnboardingIntent = (env) => + env.NEMOCLAW_PROVIDER === "install-vllm" || env.NEMOCLAW_NO_EXPRESS === "1"; `, ); writeNodeStub(fakeBin); @@ -195,6 +205,8 @@ exports.evaluateOnboardReadinessAdmission = (report, options) => { const { NEMOCLAW_EXPERIMENTAL_PROFILE: _experimentalProfile, NEMOCLAW_GATEWAY_RUNTIME: _gatewayRuntime, + NEMOCLAW_NO_EXPRESS: _noExpress, + NEMOCLAW_PROVIDER: _provider, TEST_GATEWAY_MANAGEMENT_MODE: _gatewayManagementMode, ...inheritedEnv } = process.env; @@ -208,6 +220,8 @@ exports.evaluateOnboardReadinessAdmission = (report, options) => { ? { NEMOCLAW_EXPERIMENTAL_PROFILE: options.experimentalProfile } : {}), ...(options.gatewayRuntime ? { NEMOCLAW_GATEWAY_RUNTIME: options.gatewayRuntime } : {}), + ...(options.noExpress ? { NEMOCLAW_NO_EXPRESS: "1" } : {}), + ...(options.provider ? { NEMOCLAW_PROVIDER: options.provider } : {}), TEST_GATEWAY_MANAGEMENT_MODE: options.gatewayManagementMode ?? "", }; @@ -232,6 +246,39 @@ run_installer_host_preflight } describe("installer host preflight package contract", () => { + it.each([ + ["NEMOCLAW_NO_EXPRESS", { noExpress: true }], + ["an explicit standard provider", { provider: "ollama", noExpress: true }], + ])( + "admits qualified Deferred N1x through %s (#11041)", + (_scenario, intent) => { + const { output, result } = runInstallerHostAdmissionTest( + { + runtime: "docker", + isN1x: true, + additionalFindingIds: ["host.platform.n1x_validation_pending"], + }, + undefined, + intent, + ); + + expect(result.status, output).toBe(0); + expect(output).not.toMatch(/Host preflight found issues/); + }, + 15_000, + ); + + it("keeps Deferred N1x blocked without explicit onboarding intent (#11041)", () => { + const { output, result } = runInstallerHostAdmissionTest({ + runtime: "docker", + isN1x: true, + additionalFindingIds: ["host.platform.n1x_validation_pending"], + }); + + expect(result.status).toBe(1); + expect(output).toContain("host.platform.n1x_validation_pending"); + }); + it("continues to onboarding when managed storage remediation is available", () => { const { output, result } = runInstallerHostAdmissionTest({ runtime: "docker", From fa5ac846ff5ab1f3fbe30d5669338d24dafb5cf9 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 4 Sep 2026 12:01:57 -0700 Subject: [PATCH 08/19] fix(onboard): preserve validated N1x provider intent Signed-off-by: Aaron Erickson --- .../onboard/fatal-runtime-preflight.test.ts | 4 +- src/lib/onboard/fatal-runtime-preflight.ts | 11 +- .../provider-selection-keys.ts | 92 ++++++++++++++ .../machine/handlers/preflight.test.ts | 61 +++++++++- src/lib/onboard/machine/handlers/preflight.ts | 14 ++- src/lib/onboard/providers.ts | 42 ++----- src/lib/readiness/onboard-admission.test.ts | 11 +- src/lib/readiness/onboard-admission.ts | 5 +- .../installer-host-preflight.test.ts | 115 ++++++++++++++++-- 9 files changed, 294 insertions(+), 61 deletions(-) create mode 100644 src/lib/onboard/inference-providers/provider-selection-keys.ts diff --git a/src/lib/onboard/fatal-runtime-preflight.test.ts b/src/lib/onboard/fatal-runtime-preflight.test.ts index 1cc8974a8ac..197f1aaf1fd 100644 --- a/src/lib/onboard/fatal-runtime-preflight.test.ts +++ b/src/lib/onboard/fatal-runtime-preflight.test.ts @@ -201,7 +201,7 @@ describe("report-backed runtime readiness (#7411)", () => { expect( assertOnboardSystemReadiness(readiness, hostWithRuntime("docker"), { explicitlyOptedOutGpuPassthrough: false, - allowDeferredN1xManagedVllm: true, + allowDeferredN1xOnboarding: true, exitProcess: exit as never, }), ).toBe(readiness); @@ -226,7 +226,7 @@ describe("report-backed runtime readiness (#7411)", () => { expect(() => assertOnboardSystemReadiness(readiness, hostWithRuntime("docker"), { explicitlyOptedOutGpuPassthrough: false, - allowDeferredN1xManagedVllm: false, + allowDeferredN1xOnboarding: false, exitProcess: exit as never, }), ).toThrow("exit"); diff --git a/src/lib/onboard/fatal-runtime-preflight.ts b/src/lib/onboard/fatal-runtime-preflight.ts index 42a92c17e6a..e708981953b 100644 --- a/src/lib/onboard/fatal-runtime-preflight.ts +++ b/src/lib/onboard/fatal-runtime-preflight.ts @@ -119,7 +119,8 @@ export interface OnboardHostReadinessOptions { resuming?: boolean; allowStorageRemediation?: boolean; allowPortableHostPreparation?: boolean; - allowDeferredN1xManagedVllm?: boolean; + /** A trusted current or recorded choice may exercise the Deferred N1x path. */ + allowDeferredN1xOnboarding?: boolean; /** Print warning-severity host advisories before returning an admitted report. */ presentAdvisories?: boolean; exitProcess?: (code: number) => never; @@ -179,7 +180,7 @@ export function assertOnboardSystemReadiness( allowStorageRemediation: options.allowStorageRemediation === true, allowPortableHostPreparation: options.allowPortableHostPreparation, allowDeferredN1xManagedVllm: - options.allowDeferredN1xManagedVllm ?? hasExplicitDeferredN1xOnboardingIntent(process.env), + options.allowDeferredN1xOnboarding ?? hasExplicitDeferredN1xOnboardingIntent(process.env), }); const advisories = planHostAdvisories(host, { providerOwnsHostReadiness: selectedRuntimeOwnsHostReadiness, @@ -314,7 +315,7 @@ function collectOnboardHostReadiness( wslDockerDesktopGpuProofPassed: runtimeGpu?.wslDockerDesktopGpuProofPassed, resuming: context.resuming, allowStorageRemediation, - allowDeferredN1xManagedVllm: options.allowDeferredN1xManagedVllm, + allowDeferredN1xOnboarding: options.allowDeferredN1xManagedVllm, // The initial host readiness gate already presented warning advisories. presentAdvisories: false, exitProcess: context.exitProcess, @@ -409,7 +410,7 @@ async function collectAdmittedReadinessPair( host.result.sandboxGpuConfig.mode === "0" || options.optedOutGpuPassthrough === true, resuming: context.resuming, allowStorageRemediation: isManagedGatewayReadiness(gateway), - allowDeferredN1xManagedVllm: options.allowDeferredN1xManagedVllm, + allowDeferredN1xOnboarding: options.allowDeferredN1xManagedVllm, presentAdvisories: false, exitProcess, }); @@ -592,7 +593,7 @@ export function runFatalOnboardRuntimePreflight( explicitlyOptedOutGpuPassthrough, resuming: context.resuming, allowStorageRemediation: context.allowStorageRemediation, - allowDeferredN1xManagedVllm: options.allowDeferredN1xManagedVllm, + allowDeferredN1xOnboarding: options.allowDeferredN1xManagedVllm, exitProcess, observedAt, now, diff --git a/src/lib/onboard/inference-providers/provider-selection-keys.ts b/src/lib/onboard/inference-providers/provider-selection-keys.ts new file mode 100644 index 00000000000..25547953128 --- /dev/null +++ b/src/lib/onboard/inference-providers/provider-selection-keys.ts @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +export const NON_INTERACTIVE_PROVIDER_ALIASES: Readonly> = { + cloud: "build", + nim: "nim-local", + vllm: "vllm", + "open-router": "openrouter", + openrouterai: "openrouter", + anthropiccompatible: "anthropicCompatible", + hermes: "hermesProvider", + "hermes-provider": "hermesProvider", + hermesprovider: "hermesProvider", + nous: "hermesProvider", + "nous-portal": "hermesProvider", +}; + +export const NON_INTERACTIVE_PROVIDER_KEYS: ReadonlySet = new Set([ + "build", + "openrouter", + "openai", + "anthropic", + "anthropicCompatible", + "gemini", + "hermesProvider", + "ollama", + "llama-cpp", + "install-llama-cpp", + "custom", + "nim-local", + "vllm", + "routed", + "install-vllm", + "install-ollama", + "install-windows-ollama", + "start-windows-ollama", +]); + +const PERSISTED_PROVIDER_SELECTION_KEYS: Readonly> = { + "nvidia-router": "routed", + "ollama-local": "ollama", + "nvidia-prod": "build", + // This legacy name identifies NVIDIA Endpoints, not Local NVIDIA NIM. + "nvidia-nim": "build", + "openai-api": "openai", + "openrouter-api": "openrouter", + "anthropic-prod": "anthropic", + "compatible-anthropic-endpoint": "anthropicCompatible", + "gemini-api": "gemini", + "compatible-endpoint": "custom", + "llama-cpp-local": "llama-cpp", + "hermes-provider": "hermesProvider", +}; + +export function normalizeNonInteractiveProviderKey( + value: string | null | undefined, +): string | null { + const key = String(value ?? "") + .trim() + .toLowerCase(); + if (!key) return null; + const normalized = NON_INTERACTIVE_PROVIDER_ALIASES[key] ?? key; + return NON_INTERACTIVE_PROVIDER_KEYS.has(normalized) ? normalized : null; +} + +/** True only for a recognized provider route that N1x onboarding permits. */ +export function isN1xOnboardingProviderKey(value: string | null | undefined): boolean { + const normalized = normalizeNonInteractiveProviderKey(value); + return normalized !== null && normalized !== "nim-local"; +} + +/** Resolve the provider representation persisted in an onboarding session. */ +export function persistedProviderNameToSelectionKey( + value: string | null | undefined, + options: { hasNimContainer?: boolean } = {}, +): string | null { + const name = String(value ?? "").trim(); + if (!name) return null; + // Local NIM and standalone vLLM both persist as vllm-local. Only the + // owner-only NIM container record distinguishes the excluded NIM route. + if (name === "vllm-local") return options.hasNimContainer ? "nim-local" : "vllm"; + return PERSISTED_PROVIDER_SELECTION_KEYS[name] ?? normalizeNonInteractiveProviderKey(name); +} + +/** True only for a recognized persisted provider route that N1x permits. */ +export function isN1xOnboardingRecordedProvider( + value: string | null | undefined, + options: { hasNimContainer?: boolean } = {}, +): boolean { + const selection = persistedProviderNameToSelectionKey(value, options); + return selection !== null && selection !== "nim-local"; +} diff --git a/src/lib/onboard/machine/handlers/preflight.test.ts b/src/lib/onboard/machine/handlers/preflight.test.ts index de5cf84b4d0..a768ed0929a 100644 --- a/src/lib/onboard/machine/handlers/preflight.test.ts +++ b/src/lib/onboard/machine/handlers/preflight.test.ts @@ -212,16 +212,73 @@ describe("handlePreflightState", () => { 1, expect.anything(), expect.anything(), - expect.objectContaining({ allowDeferredN1xManagedVllm: true }), + expect.objectContaining({ allowDeferredN1xOnboarding: true }), ); expect(assertOnboardHostReadiness).toHaveBeenNthCalledWith( 2, expect.anything(), expect.anything(), - expect.objectContaining({ allowDeferredN1xManagedVllm: true }), + expect.objectContaining({ allowDeferredN1xOnboarding: true }), ); }); + it("reuses a recorded standard provider for Deferred N1x resume (#11041)", async () => { + const session = createSession({ provider: "ollama-local" }); + session.steps.preflight.status = "complete"; + const assertOnboardHostReadiness = vi.fn(); + const harness = createDeps({ assertOnboardHostReadiness }); + + await handlePreflightState({ ...baseOptions(harness.deps, session), resume: true }); + + expect(assertOnboardHostReadiness).toHaveBeenCalledTimes(2); + expect(assertOnboardHostReadiness).toHaveBeenNthCalledWith( + 1, + expect.anything(), + expect.anything(), + expect.objectContaining({ allowDeferredN1xOnboarding: true }), + ); + expect(assertOnboardHostReadiness).toHaveBeenNthCalledWith( + 2, + expect.anything(), + expect.anything(), + expect.objectContaining({ allowDeferredN1xOnboarding: true }), + ); + }); + + it.each([ + ["persisted Local NIM", "vllm-local", "nemoclaw-nim", undefined, false], + ["an unknown persisted provider", "obsolete-provider", null, undefined, false], + ["legacy NVIDIA Endpoints", "nvidia-nim", null, undefined, true], + ["an explicit rebuild denial", "ollama-local", null, false, false], + ] as const)( + "classifies %s for Deferred N1x resume (#11041)", + async (_case, provider, nimContainer, authority, expected) => { + const session = createSession({ provider, nimContainer }); + session.steps.preflight.status = "complete"; + const assertOnboardHostReadiness = vi.fn(); + const harness = createDeps({ assertOnboardHostReadiness }); + + await handlePreflightState({ + ...baseOptions(harness.deps, session), + resume: true, + ...(authority === undefined ? {} : { allowDeferredN1xManagedVllm: authority }), + }); + + expect(assertOnboardHostReadiness).toHaveBeenNthCalledWith( + 1, + expect.anything(), + expect.anything(), + expect.objectContaining({ allowDeferredN1xOnboarding: expected }), + ); + expect(assertOnboardHostReadiness).toHaveBeenNthCalledWith( + 2, + expect.anything(), + expect.anything(), + expect.objectContaining({ allowDeferredN1xOnboarding: expected }), + ); + }, + ); + it("rejects changed gateway ownership before cached resume probe effects (#7411)", async () => { const session = createSession(); session.steps.preflight.status = "complete"; diff --git a/src/lib/onboard/machine/handlers/preflight.ts b/src/lib/onboard/machine/handlers/preflight.ts index 2b96bd96171..51669940d54 100644 --- a/src/lib/onboard/machine/handlers/preflight.ts +++ b/src/lib/onboard/machine/handlers/preflight.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { Session } from "../../../state/onboard-session"; +import { isN1xOnboardingRecordedProvider } from "../../inference-providers/provider-selection-keys"; import { withPreflightTrace } from "../../tracing"; import { advanceTo, type OnboardStateTransitionResult } from "../result"; @@ -56,7 +57,7 @@ export interface PreflightStateOptions< observedAt?: string; now?: () => Date; wslDockerDesktopGpuProofPassed?: boolean; - allowDeferredN1xManagedVllm?: boolean; + allowDeferredN1xOnboarding?: boolean; resuming: true; presentAdvisories?: boolean; }, @@ -151,6 +152,13 @@ export async function handlePreflightState< : { flag: null, device: null }; const effectiveSandboxGpuFlag = explicitSandboxGpuFlag ?? resumedSandboxGpuOverrides.flag; const effectiveSandboxGpuDevice = sandboxGpuDevice ?? resumedSandboxGpuOverrides.device; + const recordedProviderAllowsDeferredN1x = isN1xOnboardingRecordedProvider(session?.provider, { + hasNimContainer: Boolean(session?.nimContainer), + }); + // An explicit false is authoritative for rebuilds. Ordinary resume may use + // the provider that this owner-only session already validated and recorded. + const allowDeferredN1xOnboarding = + allowDeferredN1xManagedVllm ?? recordedProviderAllowsDeferredN1x; let gpu: Gpu; if (resumePreflight) { @@ -177,7 +185,7 @@ export async function handlePreflightState< explicitlyOptedOutGpuPassthrough: resumeSandboxGpuConfig.mode === "0", observedAt: hostObservedAt, now, - allowDeferredN1xManagedVllm, + allowDeferredN1xOnboarding, resuming: true, }); // A full detector can run the bounded ARM64 WSL Docker GPU proof. Keep it @@ -200,7 +208,7 @@ export async function handlePreflightState< observedAt: hostObservedAt, now, ...(wslDockerDesktopGpuProofPassed === undefined ? {} : { wslDockerDesktopGpuProofPassed }), - allowDeferredN1xManagedVllm, + allowDeferredN1xOnboarding, resuming: true, presentAdvisories: false, }); diff --git a/src/lib/onboard/providers.ts b/src/lib/onboard/providers.ts index a60d00bc1b2..9be9b5c093b 100644 --- a/src/lib/onboard/providers.ts +++ b/src/lib/onboard/providers.ts @@ -32,6 +32,11 @@ const { MESSAGING_CREDENTIAL_PROVIDER_TYPE, } = require("../messaging/provider-profile"); const { ensureWebSearchProviderProfiles } = require("./brave-provider-profile"); +const { + NON_INTERACTIVE_PROVIDER_ALIASES, + NON_INTERACTIVE_PROVIDER_KEYS, + normalizeNonInteractiveProviderKey, +} = require("./inference-providers/provider-selection-keys"); const MESSAGING_PROVIDER_BINDING_CONFLICT = "NEMOCLAW_MESSAGING_PROVIDER_BINDING_CONFLICT"; const MESSAGING_PROVIDER_MUTATION_FAILURE = "NEMOCLAW_MESSAGING_PROVIDER_MUTATION_FAILURE"; @@ -164,39 +169,6 @@ const PROVIDER_MODEL_ENV = "NEMOCLAW_PROVIDER_MODEL"; // provider/namespace/model convention. This endpoint is staged as a custom // OpenAI-compatible provider, not as the public build.nvidia.com provider. const HOSTED_INFERENCE_MODEL = "nvidia/nvidia/nemotron-3-ultra"; -const NON_INTERACTIVE_PROVIDER_ALIASES = { - cloud: "build", - nim: "nim-local", - vllm: "vllm", - "open-router": "openrouter", - openrouterai: "openrouter", - anthropiccompatible: "anthropicCompatible", - hermes: "hermesProvider", - "hermes-provider": "hermesProvider", - hermesprovider: "hermesProvider", - nous: "hermesProvider", - "nous-portal": "hermesProvider", -}; -const NON_INTERACTIVE_PROVIDER_KEYS = new Set([ - "build", - "openrouter", - "openai", - "anthropic", - "anthropicCompatible", - "gemini", - "hermesProvider", - "ollama", - "llama-cpp", - "install-llama-cpp", - "custom", - "nim-local", - "vllm", - "routed", - "install-vllm", - "install-ollama", - "install-windows-ollama", - "start-windows-ollama", -]); const NON_INTERACTIVE_PROVIDER_VALID_VALUES = "Valid values: build, openrouter, openai, anthropic, anthropicCompatible, gemini, hermes-provider, ollama, llama-cpp, install-llama-cpp, custom, nim-local, vllm, routed, install-vllm, install-ollama, install-windows-ollama, start-windows-ollama"; const PROVIDER_KEY_ROUTE_VALUES = new Set( @@ -384,8 +356,8 @@ function getNonInteractiveProvider(allowHostedInferenceStaging = true) { if (allowHostedInferenceStaging) stageHostedInferenceSourceSecretEnv(); const providerKey = (process.env.NEMOCLAW_PROVIDER || "").trim().toLowerCase(); if (!providerKey) return null; - const normalized = NON_INTERACTIVE_PROVIDER_ALIASES[providerKey] || providerKey; - if (!NON_INTERACTIVE_PROVIDER_KEYS.has(normalized)) { + const normalized = normalizeNonInteractiveProviderKey(providerKey); + if (!normalized) { console.error(` Unsupported NEMOCLAW_PROVIDER: ${providerKey}`); console.error(` ${NON_INTERACTIVE_PROVIDER_VALID_VALUES}`); process.exit(1); diff --git a/src/lib/readiness/onboard-admission.test.ts b/src/lib/readiness/onboard-admission.test.ts index 82a683bfc2b..f17d3c39d4e 100644 --- a/src/lib/readiness/onboard-admission.test.ts +++ b/src/lib/readiness/onboard-admission.test.ts @@ -28,7 +28,16 @@ describe("Deferred N1x onboarding intent", () => { it.each([ ["managed-vLLM", { NEMOCLAW_PROVIDER: "install-vllm" }, true], ["ordinary onboarding", { NEMOCLAW_NO_EXPRESS: "1" }, true], - ["another provider without installer state", { NEMOCLAW_PROVIDER: "ollama" }, false], + ["a standard provider", { NEMOCLAW_PROVIDER: "ollama" }, true], + ["a normalized provider alias", { NEMOCLAW_PROVIDER: " Open-Router " }, true], + ["an unknown provider", { NEMOCLAW_PROVIDER: "unknown-provider" }, false], + ["the excluded NIM provider", { NEMOCLAW_PROVIDER: "nim-local" }, false], + ["the excluded NIM alias", { NEMOCLAW_PROVIDER: "nim" }, false], + [ + "the excluded NIM provider with Express disabled", + { NEMOCLAW_PROVIDER: "nim-local", NEMOCLAW_NO_EXPRESS: "1" }, + false, + ], ["an unsupported opt-out value", { NEMOCLAW_NO_EXPRESS: "true" }, false], ["no intent", {}, false], ] as const)("recognizes %s (#11041)", (_scenario, env, expected) => { diff --git a/src/lib/readiness/onboard-admission.ts b/src/lib/readiness/onboard-admission.ts index 415f404a271..e2c28721a9e 100644 --- a/src/lib/readiness/onboard-admission.ts +++ b/src/lib/readiness/onboard-admission.ts @@ -2,12 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 import type { ReadinessCapability, ReadinessFinding, SystemReadinessReport } from "./types"; +import { isN1xOnboardingProviderKey } from "../onboard/inference-providers/provider-selection-keys"; /** Return whether installer state records an accepted Deferred N1x onboarding path. */ export function hasExplicitDeferredN1xOnboardingIntent( env: Readonly>, ): boolean { - return env.NEMOCLAW_PROVIDER === "install-vllm" || env.NEMOCLAW_NO_EXPRESS === "1"; + const provider = String(env.NEMOCLAW_PROVIDER ?? "").trim(); + if (provider) return isN1xOnboardingProviderKey(provider); + return env.NEMOCLAW_NO_EXPRESS === "1"; } export const ONBOARD_READINESS_ADMISSION_REASON_IDS = { diff --git a/test/package-contract/installer-host-preflight.test.ts b/test/package-contract/installer-host-preflight.test.ts index 6cb87ac44bd..ab45864bb07 100644 --- a/test/package-contract/installer-host-preflight.test.ts +++ b/test/package-contract/installer-host-preflight.test.ts @@ -27,6 +27,17 @@ exit 99`, ); } +function copyCompiledOnboardAdmission(readinessDir: string, onboardDir: string): void { + const compiledAdmission = path.resolve("dist/lib/readiness/onboard-admission.js"); + const compiledProviderKeys = path.resolve( + "dist/lib/onboard/inference-providers/provider-selection-keys.js", + ); + const targetProviderDir = path.join(onboardDir, "inference-providers"); + fs.mkdirSync(targetProviderDir, { recursive: true }); + fs.copyFileSync(compiledAdmission, path.join(readinessDir, "onboard-admission.js")); + fs.copyFileSync(compiledProviderKeys, path.join(targetProviderDir, "provider-selection-keys.js")); +} + function runInstallerHostAdmissionTest( host: { runtime: string; @@ -45,6 +56,7 @@ function runInstallerHostAdmissionTest( providerResolutionFailure?: string; provider?: string; noExpress?: boolean; + useCompiledAdmission?: boolean; } = {}, ) { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-installer-host-admission-")); @@ -132,13 +144,42 @@ exports.configuredRuntimeProviderOwnsHostReadiness = ({ environment = process.en for (const id of host.additionalFindingIds || []) { findings.push({ id, severity: "blocking", summary: "Blocking finding: " + id }); } - return { findings, capabilityIds: host.unknownCapabilityIds || [], host }; + const requiredCapabilities = [ + "host.docker.available", + "host.docker.daemon_reachable", + "host.docker.runtime_supported", + "host.docker.storage_compatible", + "host.gpu.nvidia_available", + "host.gpu.container_toolkit_available", + "host.gpu.cdi_healthy", + "host.platform.supported", + ]; + const unknown = new Set(host.unknownCapabilityIds || []); + const capabilities = requiredCapabilities.map((id) => ({ + id, + state: + unknown.has(id) ? "unknown" : id === "host.platform.supported" && host.isN1x ? "absent" : "present", + })); + if (host.isN1x) capabilities.push({ id: "host.platform.n1x", state: "present" }); + for (const id of unknown) { + if (!capabilities.some((capability) => capability.id === id)) { + capabilities.push({ id, state: "unknown" }); + } + } + return { + observations: [], + capabilities, + findings, + capabilityIds: host.unknownCapabilityIds || [], + host, + }; }; `, ); - fs.writeFileSync( - path.join(readinessDir, "onboard-admission.js"), - `const forcedRejection = ${JSON.stringify(forcedRejection ?? null)}; + const writeStubbedOnboardAdmission = () => + fs.writeFileSync( + path.join(readinessDir, "onboard-admission.js"), + `const forcedRejection = ${JSON.stringify(forcedRejection ?? null)}; exports.evaluateOnboardReadinessAdmission = (report, options) => { if (forcedRejection) { return { admitted: false, reasonIds: [], ...forcedRejection, waivedFindingIds: [] }; @@ -199,7 +240,11 @@ exports.evaluateOnboardReadinessAdmission = (report, options) => { exports.hasExplicitDeferredN1xOnboardingIntent = (env) => env.NEMOCLAW_PROVIDER === "install-vllm" || env.NEMOCLAW_NO_EXPRESS === "1"; `, - ); + ); + const installOnboardAdmission = options.useCompiledAdmission + ? () => copyCompiledOnboardAdmission(readinessDir, onboardDir) + : writeStubbedOnboardAdmission; + installOnboardAdmission(); writeNodeStub(fakeBin); const { @@ -247,8 +292,8 @@ run_installer_host_preflight describe("installer host preflight package contract", () => { it.each([ - ["NEMOCLAW_NO_EXPRESS", { noExpress: true }], - ["an explicit standard provider", { provider: "ollama", noExpress: true }], + ["NEMOCLAW_NO_EXPRESS", { noExpress: true, useCompiledAdmission: true }], + ["an explicit standard provider", { provider: "ollama", useCompiledAdmission: true }], ])( "admits qualified Deferred N1x through %s (#11041)", (_scenario, intent) => { @@ -269,16 +314,62 @@ describe("installer host preflight package contract", () => { ); it("keeps Deferred N1x blocked without explicit onboarding intent (#11041)", () => { - const { output, result } = runInstallerHostAdmissionTest({ - runtime: "docker", - isN1x: true, - additionalFindingIds: ["host.platform.n1x_validation_pending"], - }); + const { output, result } = runInstallerHostAdmissionTest( + { + runtime: "docker", + isN1x: true, + additionalFindingIds: ["host.platform.n1x_validation_pending"], + }, + undefined, + { useCompiledAdmission: true }, + ); expect(result.status).toBe(1); expect(output).toContain("host.platform.n1x_validation_pending"); }); + it.each([ + ["an unknown provider", "unknown-provider", false], + ["the NIM provider", "nim-local", true], + ["the NIM provider alias", "nim", true], + ] as const)( + "keeps Deferred N1x blocked for %s (#11041)", + (_scenario, provider, noExpress) => { + const { output, result } = runInstallerHostAdmissionTest( + { + runtime: "docker", + isN1x: true, + additionalFindingIds: ["host.platform.n1x_validation_pending"], + }, + undefined, + { provider, noExpress, useCompiledAdmission: true }, + ); + + expect(result.status).toBe(1); + expect(output).toContain("host.platform.n1x_validation_pending"); + }, + 15_000, + ); + + it("keeps unrelated blockers fail-closed for Deferred N1x intent (#11041)", () => { + const { output, result } = runInstallerHostAdmissionTest( + { + runtime: "docker", + isN1x: true, + additionalFindingIds: [ + "host.platform.n1x_validation_pending", + "host.test.additional_blocker", + ], + }, + undefined, + { provider: "ollama", useCompiledAdmission: true }, + ); + + expect(result.status).toBe(1); + expect(output).not.toContain("host.platform.n1x_validation_pending"); + expect(output).toContain("host.test.additional_blocker"); + }); + it("continues to onboarding when managed storage remediation is available", () => { const { output, result } = runInstallerHostAdmissionTest({ runtime: "docker", From ea14fe38345b1d4400280a7974453e01d5316fc2 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 4 Sep 2026 12:25:12 -0700 Subject: [PATCH 09/19] test(installer): prove N1x decline reaches onboarding Signed-off-by: Aaron Erickson --- docs/inference/choose-inference-provider.mdx | 3 +- .../installer-express-prompt-pty-harness.ts | 87 ++++++++++++++++++- .../install-express-prompt.test.ts | 11 +++ 3 files changed, 98 insertions(+), 3 deletions(-) diff --git a/docs/inference/choose-inference-provider.mdx b/docs/inference/choose-inference-provider.mdx index d1817dbbbff..83674679d5f 100644 --- a/docs/inference/choose-inference-provider.mdx +++ b/docs/inference/choose-inference-provider.mdx @@ -13,7 +13,8 @@ Choose a provider based on where the model runs, which API it exposes, and which NemoClaw validates the provider and model before it creates the sandbox. -On native N1x FASTOS, accept the Deferred managed-vLLM preview or enter standard onboarding by declining Express, disabling Express without an excluded provider, or setting a recognized `NEMOCLAW_PROVIDER` value other than Local NVIDIA NIM. +On native N1x FASTOS, accept the Deferred managed-vLLM preview or enter standard onboarding by declining Express. +You can also set `NEMOCLAW_NO_EXPRESS=1` without a provider, or set a recognized `NEMOCLAW_PROVIDER` value other than Local NVIDIA NIM. N1x remains Deferred, each standard provider keeps its existing requirements, and local NVIDIA NIM remains unavailable. On a qualifying Windows WSL N1x host, Windows Express uses managed llama.cpp after Docker Desktop GPU passthrough succeeds. Other Windows WSL hosts retain Windows-host or WSL-local Ollama. diff --git a/test/helpers/installer-express-prompt-pty-harness.ts b/test/helpers/installer-express-prompt-pty-harness.ts index e1d6593495a..06d5b532c04 100644 --- a/test/helpers/installer-express-prompt-pty-harness.ts +++ b/test/helpers/installer-express-prompt-pty-harness.ts @@ -25,7 +25,7 @@ export function runExpressPromptWithTty( stdinMode: "pipe" | "tty", platform = "DGX Spark", extraEnv: Record = {}, - entrypoint: "prompt" | "accepted-station-main" = "prompt", + entrypoint: "prompt" | "accepted-station-main" | "n1x-standard-main" = "prompt", entrypointArgs: string[] = [], harnessFixture?: InstallerExpressPtyFixture, ) { @@ -51,7 +51,90 @@ harness_mode = sys.argv[6] timeout_seconds = float(sys.argv[7]) pid_file = sys.argv[8] entrypoint_args = sys.argv[9:] -if entrypoint == "accepted-station-main": +if entrypoint == "n1x-standard-main": + script = r''' +source "$INSTALLER_UNDER_TEST" >/dev/null +node() { ${JSON.stringify(process.execPath)} "$@"; } +detect_express_platform() { printf "$EXPRESS_PLATFORM"; } +validate_express_platform_boundary() { :; } +validate_force_station_install_override() { :; } +validate_station_deepseek_override() { :; } +describe_express_install() { :; } +load_station_vllm_conflict_helpers() { :; } +consume_station_local_vllm_resume() { return 1; } +resolve_nemoclaw_gateway_port() { printf '8080'; } +preflight_explicit_express_flags() { :; } +print_banner() { :; } +preflight_usage_notice_prompt() { :; } +validate_station_pair_selection() { :; } +ensure_station_express_host() { :; } +prepare_portable_experimental_runtime_override() { :; } +installer_requires_legacy_docker_bootstrap() { return 1; } +ensure_openshell_build_deps() { :; } +step() { :; } +show_usage_notice() { :; } +nemoclaw_state_dir() { printf '%s' "$HOME/state"; } +registered_sandbox_count() { printf '0'; } +recover_preexisting_sandboxes_before_onboard() { + _PREEXISTING_SANDBOX_RECOVERY_RAN=false + return 0 +} +restore_onboard_forward_after_post_checks() { return 0; } +finalize_install() { :; } +clear_station_resume_after_completed_onboarding() { :; } +repair_installer_nvidia_cdi_spec() { :; } +install_nemoclaw_before_onboarding() { + repo_root="$(cd "$(dirname "$INSTALLER_UNDER_TEST")/.." && pwd)" + NEMOCLAW_SOURCE_ROOT="$PWD/source" + onboard_dir="$NEMOCLAW_SOURCE_ROOT/dist/lib/onboard" + readiness_dir="$NEMOCLAW_SOURCE_ROOT/dist/lib/readiness" + provider_dir="$onboard_dir/inference-providers" + mkdir -p "$onboard_dir/experimental" "$readiness_dir" "$provider_dir" + printf '%s\n' \ + 'exports.assessHost = () => ({ runtime: "docker", isWsl: false });' \ + 'exports.planHostAdvisories = () => [];' \ + >"$onboard_dir/preflight.js" + printf '%s\n' \ + 'exports.loadGatewayManagementDeclaration = () => ({ ok: true, declaration: null });' \ + >"$onboard_dir/gateway-management.js" + printf '%s\n' \ + 'exports.configuredRuntimeProviderOwnsHostReadiness = () => false;' \ + >"$onboard_dir/docker-driver-gateway-env.js" + printf '%s\n' \ + 'exports.isPortableExperimentalProfile = () => false;' \ + >"$onboard_dir/experimental/portable-profile.js" + printf '%s\n' \ + 'exports.createHostReadinessReport = () => ({' \ + ' schemaVersion: "1.1.0", status: "incompatible", exitCode: 2, observations: [],' \ + ' capabilities: [' \ + ' { id: "host.docker.available", state: "present" },' \ + ' { id: "host.docker.daemon_reachable", state: "present" },' \ + ' { id: "host.docker.runtime_supported", state: "present" },' \ + ' { id: "host.docker.storage_compatible", state: "present" },' \ + ' { id: "host.gpu.nvidia_available", state: "present" },' \ + ' { id: "host.gpu.container_toolkit_available", state: "present" },' \ + ' { id: "host.gpu.cdi_healthy", state: "present" },' \ + ' { id: "host.platform.supported", state: "absent" },' \ + ' { id: "host.platform.n1x", state: "present" },' \ + ' ],' \ + ' findings: [{ id: "host.platform.n1x_validation_pending", severity: "blocking", summary: "N1x validation pending" }],' \ + '});' \ + >"$readiness_dir/host.js" + cp "$repo_root/dist/lib/readiness/onboard-admission.js" "$readiness_dir/onboard-admission.js" + cp "$repo_root/dist/lib/onboard/inference-providers/provider-selection-keys.js" \ + "$provider_dir/provider-selection-keys.js" + fake_cli="$PWD/nemoclaw-under-test" + printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'printf "ONBOARD NO_EXPRESS=%s PROVIDER=%s ARGS=%s\\n" "\${NEMOCLAW_NO_EXPRESS:-}" "\${NEMOCLAW_PROVIDER:-}" "$*"' \ + >"$fake_cli" + chmod +x "$fake_cli" + _CLI_BIN="nemoclaw" + _CLI_PATH="$fake_cli" +} +main "$@" +''' +elif entrypoint == "accepted-station-main": script = r''' source "$INSTALLER_UNDER_TEST" >/dev/null detect_express_platform() { printf "$EXPRESS_PLATFORM"; } diff --git a/test/installer-integration/install-express-prompt.test.ts b/test/installer-integration/install-express-prompt.test.ts index 3c9fcb57509..c70c9a2151a 100644 --- a/test/installer-integration/install-express-prompt.test.ts +++ b/test/installer-integration/install-express-prompt.test.ts @@ -11,6 +11,17 @@ import { runExpressPromptWithTty } from "../helpers/installer-express-prompt-pty import { INSTALLER_PAYLOAD, TEST_SYSTEM_PATH } from "../helpers/installer-sourced-env"; describe("installer express install prompt (sourced)", () => { + it("carries a declined N1x preview through preflight into ordinary onboarding (#11041)", () => { + const result = runExpressPromptWithTty("n\n", "pipe", "N1x", {}, "n1x-standard-main"); + const output = `${result.stdout}${result.stderr}`; + + expect(result.error, output).toBeUndefined(); + expect(result.status, output).toBe(0); + expect(output.match(/Detected container runtime: docker/g)).toHaveLength(1); + expect(output).not.toContain("Host preflight found issues"); + expect(output.match(/ONBOARD NO_EXPRESS=1 PROVIDER= ARGS=onboard/g)).toHaveLength(1); + }); + it("drains PTY output after the child exits", () => { const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-pty-tail-")); try { From 1519c5738594a9ce811eab210c9768d5928a72cc Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 4 Sep 2026 12:47:30 -0700 Subject: [PATCH 10/19] fix(rebuild): preserve standard N1x provider intent Signed-off-by: Aaron Erickson --- .../actions/sandbox/rebuild-gpu-opt-out.ts | 2 +- ...eflight-target-phase-orchestration.test.ts | 28 +++++++++++++++-- .../sandbox/rebuild-preflight-target-phase.ts | 4 +-- .../sandbox/rebuild-target-preflight.ts | 7 ++--- .../actions/sandbox/rebuild-target-staging.ts | 31 +++++++++++++++---- .../sandbox/n1x-managed-vllm-rebuild.ts | 11 +++++-- src/lib/onboard/types.ts | 2 +- 7 files changed, 66 insertions(+), 19 deletions(-) diff --git a/src/lib/actions/sandbox/rebuild-gpu-opt-out.ts b/src/lib/actions/sandbox/rebuild-gpu-opt-out.ts index 5a1f5a5a6c2..0f53345ce6c 100644 --- a/src/lib/actions/sandbox/rebuild-gpu-opt-out.ts +++ b/src/lib/actions/sandbox/rebuild-gpu-opt-out.ts @@ -130,7 +130,7 @@ export type RebuildRecreateOnboardOpts = { rebuildRegistryInferenceRoute?: RebuildRouteHandoff; rebuildProviderReconfigure?: RebuildProviderReconfigureHandoff; providerRecoveryReceipt?: ProviderRecoveryReceipt; - /** Recorded managed-vLLM intent admitted only by the N1x readiness exception. */ + /** Recorded provider intent admitted only by the N1x readiness exception. */ allowDeferredN1xManagedVllm?: true; /** Target-scoped authority admitted by the authoritative rebuild preflight. */ rebuildGatewayAuthority?: CheckpointGatewayAuthority; diff --git a/src/lib/actions/sandbox/rebuild-preflight-target-phase-orchestration.test.ts b/src/lib/actions/sandbox/rebuild-preflight-target-phase-orchestration.test.ts index 359a6fecf61..18bf36d66ad 100644 --- a/src/lib/actions/sandbox/rebuild-preflight-target-phase-orchestration.test.ts +++ b/src/lib/actions/sandbox/rebuild-preflight-target-phase-orchestration.test.ts @@ -73,10 +73,13 @@ describe("prepareRebuildTargetPreflights", () => { async function prepareN1xTarget( endpointSource: "onboard" | "inference-set", mcp: { bridges: Record } | null = null, + provider = "vllm-local", + model = "nvidia/Qwen3.6-35B-A3B-NVFP4", + nimContainer: string | null = null, ) { const resumeConfig = { - provider: "vllm-local", - model: "nvidia/Qwen3.6-35B-A3B-NVFP4", + provider, + model, preferredInferenceApi: "openai-completions", pinEndpoint: true, endpointUrl: null, @@ -115,6 +118,7 @@ describe("prepareRebuildTargetPreflights", () => { model: resumeConfig.model, endpointUrl: "http://host.openshell.internal:8000/v1", endpointSource, + nimContainer, mcp, } as never, rebuildAgent: "openclaw", @@ -208,6 +212,26 @@ describe("prepareRebuildTargetPreflights", () => { ); }); + it("passes recorded Ollama intent into authoritative readiness (#11041)", async () => { + const readinessOptions = await prepareN1xTarget("onboard", null, "ollama-local", "qwen3.5:9b"); + + expect(readinessOptions).toEqual( + expect.objectContaining({ allowDeferredN1xManagedVllm: true }), + ); + }); + + it("withholds recorded Local NIM intent from authoritative readiness (#11041)", async () => { + const readinessOptions = await prepareN1xTarget( + "onboard", + null, + "vllm-local", + "nvidia/Qwen3.6-35B-A3B-NVFP4", + "nemoclaw-nim", + ); + + expect(readinessOptions).not.toHaveProperty("allowDeferredN1xManagedVllm"); + }); + it("withholds N1x intent for a mismatched endpoint source (#9292)", async () => { const readinessOptions = await prepareN1xTarget("inference-set"); diff --git a/src/lib/actions/sandbox/rebuild-preflight-target-phase.ts b/src/lib/actions/sandbox/rebuild-preflight-target-phase.ts index 496005d3755..5ab1071e1c9 100644 --- a/src/lib/actions/sandbox/rebuild-preflight-target-phase.ts +++ b/src/lib/actions/sandbox/rebuild-preflight-target-phase.ts @@ -62,7 +62,7 @@ import { prepareRebuildTargetConfig, type RebuildTargetConfig, stageRebuildHermesDashboardConfig, - stageRecordedManagedVllmIntent, + stageRecordedDeferredN1xIntent, } from "./rebuild-target-preflight"; /** Upper bound on how long a minted provider-recovery receipt stays valid. */ @@ -267,7 +267,7 @@ export async function prepareRebuildTargetPreflights(args: { recreateOptions.observabilityEnabled = requestedObservabilityEnabled ?? recreateOptions.observabilityEnabled; recreateOptions.observabilityRequestedExplicitly = requestedObservabilityEnabled !== undefined; - stageRecordedManagedVllmIntent(recreateOptions, sandboxEntry, resumeConfig); + stageRecordedDeferredN1xIntent(recreateOptions, sandboxEntry, resumeConfig); if ( !stageRebuildHermesDashboardConfig( rebuildAgent, diff --git a/src/lib/actions/sandbox/rebuild-target-preflight.ts b/src/lib/actions/sandbox/rebuild-target-preflight.ts index c0368f626bd..8bac8ff7ccd 100644 --- a/src/lib/actions/sandbox/rebuild-target-preflight.ts +++ b/src/lib/actions/sandbox/rebuild-target-preflight.ts @@ -7,10 +7,7 @@ * staging remain independently reviewable. */ export { printRebuildPreflightFailure } from "./rebuild-preflight-error"; -export { - prepareRebuildTargetConfig, - type RebuildTargetConfig, -} from "./rebuild-target-config"; +export { prepareRebuildTargetConfig, type RebuildTargetConfig } from "./rebuild-target-config"; export { preflightAuthoritativeOnboardRuntime, preflightRebuildTargetRuntime, @@ -19,5 +16,5 @@ export { hydrateMessagingConfigForRebuild, prepareRebuildRecreateOptions, stageRebuildHermesDashboardConfig, - stageRecordedManagedVllmIntent, + stageRecordedDeferredN1xIntent, } from "./rebuild-target-staging"; diff --git a/src/lib/actions/sandbox/rebuild-target-staging.ts b/src/lib/actions/sandbox/rebuild-target-staging.ts index e703e3ec364..8faf2536fad 100644 --- a/src/lib/actions/sandbox/rebuild-target-staging.ts +++ b/src/lib/actions/sandbox/rebuild-target-staging.ts @@ -2,7 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 import { hydrateMessagingChannelConfig } from "../../messaging-channel-config"; -import { isRecordedN1xManagedVllmRebuildEligible } from "../../domain/sandbox/n1x-managed-vllm-rebuild"; +import { + isN1xManagedVllmProviderModel, + isRecordedN1xManagedVllmRebuildEligible, +} from "../../domain/sandbox/n1x-managed-vllm-rebuild"; +import { isN1xOnboardingRecordedProvider } from "../../onboard/inference-providers/provider-selection-keys"; import { getStoredMessagingChannelConfig } from "../../onboard/messaging-config"; import { createRebuildRouteHandoff, @@ -86,8 +90,8 @@ export function stageRebuildHermesDashboardConfig( return true; } -/** Stage the recorded N1x decision without moving action state into the domain layer. */ -export function stageRecordedManagedVllmIntent( +/** Stage a validated recorded N1x provider decision for authoritative rebuild readiness. */ +export function stageRecordedDeferredN1xIntent( recreateOptions: Pick, sandboxEntry: Pick< RebuildSandboxEntry, @@ -97,6 +101,7 @@ export function stageRecordedManagedVllmIntent( | "endpointSource" | "openshellDriver" | "hostLocalInferenceReceipt" + | "nimContainer" >, rebuildSelection: { provider: string; @@ -105,13 +110,27 @@ export function stageRecordedManagedVllmIntent( endpointUrl: string | null; }, ): void { - if ( + const selectionMatchesRecord = + rebuildSelection.provider === sandboxEntry.provider && + rebuildSelection.model === sandboxEntry.model; + const isManagedVllmIdentity = isN1xManagedVllmProviderModel( + sandboxEntry.provider, + sandboxEntry.model, + ); + const recordedStandardProviderIsEligible = + selectionMatchesRecord && + !isManagedVllmIdentity && + isN1xOnboardingRecordedProvider(sandboxEntry.provider, { + hasNimContainer: Boolean(sandboxEntry.nimContainer), + }); + const recordedManagedVllmIsEligible = + !sandboxEntry.nimContainer && isRecordedN1xManagedVllmRebuildEligible( sandboxEntry, rebuildSelection, parseHostLocalInferenceReceipt, - ) - ) { + ); + if (recordedStandardProviderIsEligible || recordedManagedVllmIsEligible) { recreateOptions.allowDeferredN1xManagedVllm = true; } } diff --git a/src/lib/domain/sandbox/n1x-managed-vllm-rebuild.ts b/src/lib/domain/sandbox/n1x-managed-vllm-rebuild.ts index a3821aec1dc..c69ae648ae0 100644 --- a/src/lib/domain/sandbox/n1x-managed-vllm-rebuild.ts +++ b/src/lib/domain/sandbox/n1x-managed-vllm-rebuild.ts @@ -6,6 +6,14 @@ import { VLLM_PORT } from "../../core/vllm-port"; const N1X_EXPRESS_PROVIDER = "vllm-local"; const N1X_EXPRESS_MODEL = "nvidia/Qwen3.6-35B-A3B-NVFP4"; +/** True only for the provider and model identity reserved by N1x Express. */ +export function isN1xManagedVllmProviderModel( + provider: string | null | undefined, + model: string | null | undefined, +): boolean { + return provider === N1X_EXPRESS_PROVIDER && model === N1X_EXPRESS_MODEL; +} + export interface RecordedN1xManagedVllmRoute { provider?: string | null; model?: string | null; @@ -42,8 +50,7 @@ export function isRecordedN1xManagedVllmRebuildEligible( const recordedEndpointUsesCanonicalLocalRoute = sandboxEntry.endpointUrl === null || sandboxEntry.endpointUrl === canonicalEndpointUrl; if ( - sandboxEntry.provider !== N1X_EXPRESS_PROVIDER || - sandboxEntry.model !== N1X_EXPRESS_MODEL || + !isN1xManagedVllmProviderModel(sandboxEntry.provider, sandboxEntry.model) || !recordedEndpointUsesCanonicalLocalRoute || sandboxEntry.endpointSource !== "onboard" || sandboxEntry.openshellDriver !== "docker" || diff --git a/src/lib/onboard/types.ts b/src/lib/onboard/types.ts index c8e8b63deb3..c5f83f92dea 100644 --- a/src/lib/onboard/types.ts +++ b/src/lib/onboard/types.ts @@ -156,7 +156,7 @@ export type OnboardOptions = { rebuildProviderReconfigure?: import("./rebuild-route-handoff").RebuildProviderReconfigureHandoff; /** Internal one-shot authority to recover the recorded provider during a locked rebuild resume. */ providerRecoveryReceipt?: import("./rebuild-route-handoff").ProviderRecoveryReceipt; - /** Internal rebuild handoff for a recorded managed-vLLM N1x preview selection. */ + /** Internal rebuild handoff for a recorded provider admitted by Deferred N1x readiness. */ allowDeferredN1xManagedVllm?: true; /** Internal one-shot handoff for the exact image context validated before rebuild deletion. */ preparedImageRebuild?: import("./prepared-dcode-rebuild").PreparedImageRebuildHandoff; From 30db99ccd29b5a09bae680bcc175f8cd34eb1bfc Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 4 Sep 2026 13:03:46 -0700 Subject: [PATCH 11/19] refactor(onboard): unify persisted provider mapping Signed-off-by: Aaron Erickson --- src/lib/onboard/provider-recovery.test.ts | 121 +++++++++++++--------- src/lib/onboard/provider-recovery.ts | 21 +--- 2 files changed, 76 insertions(+), 66 deletions(-) diff --git a/src/lib/onboard/provider-recovery.test.ts b/src/lib/onboard/provider-recovery.test.ts index 9649ab5883d..de35cc2aa8a 100644 --- a/src/lib/onboard/provider-recovery.test.ts +++ b/src/lib/onboard/provider-recovery.test.ts @@ -5,10 +5,12 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import * as onboardSession from "../state/onboard-session"; import * as registry from "../state/registry"; +import { persistedProviderNameToSelectionKey } from "./inference-providers/provider-selection-keys"; import { classifySandboxRecoveryAuthority, createProviderRecoveryHelpers, getSandboxRecoveryAuthority, + providerNameToOptionKey, shouldRecoverRecordedProvider, validateLiveGatewayInference, } from "./provider-recovery"; @@ -17,6 +19,30 @@ afterEach(() => { vi.restoreAllMocks(); }); +describe("persisted provider selection", () => { + it.each([ + ["Model Router", "nvidia-router", false], + ["Ollama", "ollama-local", false], + ["vLLM", "vllm-local", false], + ["Local NVIDIA NIM", "vllm-local", true], + ["legacy NVIDIA Endpoints", "nvidia-nim", false], + ["OpenAI", "openai-api", false], + ["OpenRouter", "openrouter-api", false], + ["Anthropic", "anthropic-prod", false], + ["Anthropic-compatible", "compatible-anthropic-endpoint", false], + ["Gemini", "gemini-api", false], + ["OpenAI-compatible", "compatible-endpoint", false], + ["llama.cpp", "llama-cpp-local", false], + ["Hermes Provider", "hermes-provider", false], + ["an unknown provider", "unknown-provider", false], + ] as const)("uses the shared mapping for %s (#11041)", (_label, provider, hasNimContainer) => { + const options = { hasNimContainer }; + expect(providerNameToOptionKey({}, provider, options)).toBe( + persistedProviderNameToSelectionKey(provider, options), + ); + }); +}); + describe("validateLiveGatewayInference", () => { it("accepts a complete bounded provider/model pair", () => { expect( @@ -97,22 +123,19 @@ describe("shouldRecoverRecordedProvider", () => { sessionSandboxName: "dc-after", expected: false, }, - ] as const)("$label", ({ - fresh, - sandboxName, - sandboxRecoveryAuthority, - sessionSandboxName, - expected, - }) => { - expect( - shouldRecoverRecordedProvider({ - fresh, - sandboxName, - sandboxRecoveryAuthority, - sessionSandboxName, - }), - ).toBe(expected); - }); + ] as const)( + "$label", + ({ fresh, sandboxName, sandboxRecoveryAuthority, sessionSandboxName, expected }) => { + expect( + shouldRecoverRecordedProvider({ + fresh, + sandboxName, + sandboxRecoveryAuthority, + sessionSandboxName, + }), + ).toBe(expected); + }, + ); }); describe("sandbox recovery authority", () => { @@ -305,39 +328,40 @@ describe("provider recovery persisted routing state", () => { it.each([ { label: "ownerless", reservationSessionId: undefined }, { label: "foreign-owned", reservationSessionId: "session-other" }, - ])("rejects every $label pending route reader before session fallback", ({ - reservationSessionId, - }) => { - vi.spyOn(registry, "getSandbox").mockReturnValue({ - name: "alpha", - pendingRouteReservation: true, - ...(reservationSessionId ? { reservationSessionId } : {}), - provider: "compatible-endpoint", - model: "registry-model", - endpointUrl: "https://registry.example/v1", - endpointSource: null, - preferredInferenceApi: "openai-completions", - nimContainer: "registry-container", - }); - vi.spyOn(onboardSession, "loadSession").mockReturnValue( - onboardSession.createSession({ - sessionId: "session-current", - sandboxName: "alpha", + ])( + "rejects every $label pending route reader before session fallback", + ({ reservationSessionId }) => { + vi.spyOn(registry, "getSandbox").mockReturnValue({ + name: "alpha", + pendingRouteReservation: true, + ...(reservationSessionId ? { reservationSessionId } : {}), provider: "compatible-endpoint", - model: "session-model", - endpointUrl: "https://session.example/v1", - preferredInferenceApi: "openai-responses", - nimContainer: "session-container", - }), - ); - const recovery = helpers(); + model: "registry-model", + endpointUrl: "https://registry.example/v1", + endpointSource: null, + preferredInferenceApi: "openai-completions", + nimContainer: "registry-container", + }); + vi.spyOn(onboardSession, "loadSession").mockReturnValue( + onboardSession.createSession({ + sessionId: "session-current", + sandboxName: "alpha", + provider: "compatible-endpoint", + model: "session-model", + endpointUrl: "https://session.example/v1", + preferredInferenceApi: "openai-responses", + nimContainer: "session-container", + }), + ); + const recovery = helpers(); - expect(recovery.readRecordedProvider("alpha", "session-current")).toBeNull(); - expect(recovery.readRecordedModel("alpha", "session-current")).toBeNull(); - expect(recovery.readRecordedEndpointUrl("alpha", "session-current")).toBeNull(); - expect(recovery.readRecordedNimContainer("alpha", "session-current")).toBeNull(); - expect(recovery.readRecordedInferenceRoute("alpha", "session-current")).toBeNull(); - }); + expect(recovery.readRecordedProvider("alpha", "session-current")).toBeNull(); + expect(recovery.readRecordedModel("alpha", "session-current")).toBeNull(); + expect(recovery.readRecordedEndpointUrl("alpha", "session-current")).toBeNull(); + expect(recovery.readRecordedNimContainer("alpha", "session-current")).toBeNull(); + expect(recovery.readRecordedInferenceRoute("alpha", "session-current")).toBeNull(); + }, + ); it("allows the current session to read its pending route", () => { vi.spyOn(registry, "getSandbox").mockReturnValue({ @@ -450,8 +474,7 @@ describe("provider recovery persisted routing state", () => { }); const captureOpenshell = vi.fn(() => ({ status: 0, - output: - "Gateway inference:\n Provider: compatible-endpoint\n Model: gateway-model\n", + output: "Gateway inference:\n Provider: compatible-endpoint\n Model: gateway-model\n", })); const recovery = createProviderRecoveryHelpers({ captureOpenshell, diff --git a/src/lib/onboard/provider-recovery.ts b/src/lib/onboard/provider-recovery.ts index 65136cb00c0..39f6b90ee41 100644 --- a/src/lib/onboard/provider-recovery.ts +++ b/src/lib/onboard/provider-recovery.ts @@ -10,6 +10,7 @@ import { normalizeInferenceEndpointSource, } from "../inference/selection"; import { getLiveGatewayInference } from "../inference/live"; +import { persistedProviderNameToSelectionKey } from "./inference-providers/provider-selection-keys"; export type RemoteProviderConfigEntryLike = { providerName?: string }; @@ -52,10 +53,7 @@ export function applyVllmInstallResumeDefaults( } export function vllmInstallRecoveryOptions( - deps: Pick< - VllmInstallResumeDeps, - "checkpointVllmInstallModel" | "getVllmInstallResumeModel" - >, + deps: Pick, access: VllmInstallResumeSessionAccess = onboardSession, ): { checkpointInstallIntent?: (modelId: string) => void; @@ -79,19 +77,8 @@ export function providerNameToOptionKey( opts: { hasNimContainer?: boolean } = {}, ): string | null { if (!name) return null; - if (name === "nvidia-router") return "routed"; - if (name === "ollama-local") return "ollama"; - // Local NIM and standalone vLLM both persist as provider="vllm-local". NIM - // is positively identified by a nimContainer record; the absence of one in - // registry/session recovery reliably means standalone vLLM (the standalone - // path never records a container), so default to "vllm" there. Live-gateway - // recovery doesn't carry container info either, but the caller's - // option-availability check still gates on whether vllm is actually running. - if (name === "vllm-local") return opts.hasNimContainer ? "nim-local" : "vllm"; - // `nvidia-nim` is a legacy alias for cloud NVIDIA Endpoints (see - // setupInference: it routes nvidia-nim through REMOTE_PROVIDER_CONFIG.build), - // not a marker for Local NIM. Local NIM persists as vllm-local + nimContainer. - if (name === "nvidia-nim") return "build"; + const builtIn = persistedProviderNameToSelectionKey(name, opts); + if (builtIn) return builtIn; for (const [key, cfg] of Object.entries(remoteProviderConfig)) { if (cfg.providerName === name) return key; } From b9c1f1f877cdd88623bd121c49b8bd35c8d6be1c Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 4 Sep 2026 13:18:47 -0700 Subject: [PATCH 12/19] refactor(onboard): centralize provider definitions Signed-off-by: Aaron Erickson --- .../provider-selection-keys.ts | 31 +++++++++++++------ src/lib/onboard/providers.test.ts | 11 +++++++ src/lib/onboard/providers.ts | 23 +++++++------- 3 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/lib/onboard/inference-providers/provider-selection-keys.ts b/src/lib/onboard/inference-providers/provider-selection-keys.ts index 25547953128..4e7c94ec9d5 100644 --- a/src/lib/onboard/inference-providers/provider-selection-keys.ts +++ b/src/lib/onboard/inference-providers/provider-selection-keys.ts @@ -36,20 +36,33 @@ export const NON_INTERACTIVE_PROVIDER_KEYS: ReadonlySet = new Set([ "start-windows-ollama", ]); +export const NON_INTERACTIVE_PROVIDER_VALID_VALUES = `Valid values: ${Array.from( + NON_INTERACTIVE_PROVIDER_KEYS, + (key) => (key === "hermesProvider" ? "hermes-provider" : key), +).join(", ")}`; + +/** Gateway provider names declared by built-in provider selection keys. */ +export const REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY: Readonly> = { + build: "nvidia-prod", + openrouter: "openrouter-api", + openai: "openai-api", + anthropic: "anthropic-prod", + anthropicCompatible: "compatible-anthropic-endpoint", + gemini: "gemini-api", + hermesProvider: "hermes-provider", + custom: "compatible-endpoint", + "llama-cpp": "llama-cpp-local", +}; + const PERSISTED_PROVIDER_SELECTION_KEYS: Readonly> = { + ...Object.fromEntries( + Object.entries(REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY).map(([key, name]) => [name, key]), + ), "nvidia-router": "routed", "ollama-local": "ollama", - "nvidia-prod": "build", + "vllm-local": "vllm", // This legacy name identifies NVIDIA Endpoints, not Local NVIDIA NIM. "nvidia-nim": "build", - "openai-api": "openai", - "openrouter-api": "openrouter", - "anthropic-prod": "anthropic", - "compatible-anthropic-endpoint": "anthropicCompatible", - "gemini-api": "gemini", - "compatible-endpoint": "custom", - "llama-cpp-local": "llama-cpp", - "hermes-provider": "hermesProvider", }; export function normalizeNonInteractiveProviderKey( diff --git a/src/lib/onboard/providers.test.ts b/src/lib/onboard/providers.test.ts index 86935e42a70..4ac162550e7 100644 --- a/src/lib/onboard/providers.test.ts +++ b/src/lib/onboard/providers.test.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { describe, expect, it, vi } from "vitest"; +import { persistedProviderNameToSelectionKey } from "./inference-providers/provider-selection-keys"; type RunResult = { error?: unknown; @@ -203,6 +204,16 @@ function withProviderEnv(next: Record, testBody: () } describe("onboard provider helpers", () => { + it.each(Object.entries(REMOTE_PROVIDER_CONFIG))( + "maps declared provider %s back from its persisted name (#11041)", + (key, provider) => { + expect( + persistedProviderNameToSelectionKey(provider.providerName), + provider.providerName, + ).toBe(key); + }, + ); + it("uses Gemini 3.6 Flash as the onboarding default (#9298)", () => { expect(REMOTE_PROVIDER_CONFIG.gemini.defaultModel).toBe("gemini-3.6-flash"); }); diff --git a/src/lib/onboard/providers.ts b/src/lib/onboard/providers.ts index 9be9b5c093b..20cd1bfdb87 100644 --- a/src/lib/onboard/providers.ts +++ b/src/lib/onboard/providers.ts @@ -19,7 +19,6 @@ const { compactText } = require("../core/url-utils"); const { LLAMA_CPP_CREDENTIAL_ENV, LLAMA_CPP_HOST_OPENAI_BASE_URL, - LLAMA_CPP_PROVIDER_NAME, } = require("../inference/llama-cpp/contract"); const { inspectGatewayCredentialFamilyProviderBinding, @@ -35,6 +34,8 @@ const { ensureWebSearchProviderProfiles } = require("./brave-provider-profile"); const { NON_INTERACTIVE_PROVIDER_ALIASES, NON_INTERACTIVE_PROVIDER_KEYS, + NON_INTERACTIVE_PROVIDER_VALID_VALUES, + REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY, normalizeNonInteractiveProviderKey, } = require("./inference-providers/provider-selection-keys"); @@ -169,8 +170,6 @@ const PROVIDER_MODEL_ENV = "NEMOCLAW_PROVIDER_MODEL"; // provider/namespace/model convention. This endpoint is staged as a custom // OpenAI-compatible provider, not as the public build.nvidia.com provider. const HOSTED_INFERENCE_MODEL = "nvidia/nvidia/nemotron-3-ultra"; -const NON_INTERACTIVE_PROVIDER_VALID_VALUES = - "Valid values: build, openrouter, openai, anthropic, anthropicCompatible, gemini, hermes-provider, ollama, llama-cpp, install-llama-cpp, custom, nim-local, vllm, routed, install-vllm, install-ollama, install-windows-ollama, start-windows-ollama"; const PROVIDER_KEY_ROUTE_VALUES = new Set( [ "inference", @@ -182,7 +181,7 @@ const PROVIDER_KEY_ROUTE_VALUES = new Set( const REMOTE_PROVIDER_CONFIG = { build: { label: "NVIDIA Endpoints", - providerName: "nvidia-prod", + providerName: REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY.build, providerType: "nvidia", credentialEnv: "NVIDIA_INFERENCE_API_KEY", endpointUrl: BUILD_ENDPOINT_URL, @@ -193,7 +192,7 @@ const REMOTE_PROVIDER_CONFIG = { }, openrouter: { label: "OpenRouter", - providerName: openrouter.OPENROUTER_PROVIDER_NAME, + providerName: REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY.openrouter, providerType: openrouter.OPENROUTER_PROVIDER_TYPE, credentialEnv: openrouter.OPENROUTER_CREDENTIAL_ENV, endpointUrl: openrouter.OPENROUTER_ENDPOINT_URL, @@ -204,7 +203,7 @@ const REMOTE_PROVIDER_CONFIG = { }, openai: { label: "OpenAI", - providerName: "openai-api", + providerName: REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY.openai, providerType: "openai", credentialEnv: "OPENAI_API_KEY", endpointUrl: OPENAI_ENDPOINT_URL, @@ -215,7 +214,7 @@ const REMOTE_PROVIDER_CONFIG = { }, anthropic: { label: "Anthropic", - providerName: "anthropic-prod", + providerName: REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY.anthropic, providerType: "anthropic", credentialEnv: "ANTHROPIC_API_KEY", endpointUrl: ANTHROPIC_ENDPOINT_URL, @@ -225,7 +224,7 @@ const REMOTE_PROVIDER_CONFIG = { }, anthropicCompatible: { label: "Other Anthropic-compatible endpoint", - providerName: "compatible-anthropic-endpoint", + providerName: REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY.anthropicCompatible, providerType: "anthropic", credentialEnv: "COMPATIBLE_ANTHROPIC_API_KEY", endpointUrl: "", @@ -235,7 +234,7 @@ const REMOTE_PROVIDER_CONFIG = { }, gemini: { label: "Google Gemini", - providerName: "gemini-api", + providerName: REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY.gemini, providerType: "openai", credentialEnv: "GEMINI_API_KEY", endpointUrl: GEMINI_ENDPOINT_URL, @@ -253,7 +252,7 @@ const REMOTE_PROVIDER_CONFIG = { // without first selecting the entry. hermesProvider: { label: "Hermes Provider (Moonshot, Z-AI, MiniMax, Qwen, Xiaomi, Tencent, StepFun, xAI, Arcee)", - providerName: "hermes-provider", + providerName: REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY.hermesProvider, providerType: "openai", credentialEnv: "OPENAI_API_KEY", endpointUrl: HERMES_INFERENCE_ENDPOINT_URL, @@ -264,7 +263,7 @@ const REMOTE_PROVIDER_CONFIG = { }, custom: { label: "Other OpenAI-compatible endpoint", - providerName: "compatible-endpoint", + providerName: REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY.custom, providerType: "openai", credentialEnv: "COMPATIBLE_API_KEY", endpointUrl: "", @@ -275,7 +274,7 @@ const REMOTE_PROVIDER_CONFIG = { }, "llama-cpp": { label: "Local llama.cpp", - providerName: LLAMA_CPP_PROVIDER_NAME, + providerName: REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY["llama-cpp"], providerType: "openai", credentialEnv: LLAMA_CPP_CREDENTIAL_ENV, endpointUrl: LLAMA_CPP_HOST_OPENAI_BASE_URL, From e253c209428affe1aa4f01c087e34b6288550759 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 4 Sep 2026 13:31:30 -0700 Subject: [PATCH 13/19] fix(ci): read shared provider definitions Signed-off-by: Aaron Erickson --- test/e2e/e2e-cloud-experimental/check-docs.sh | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/test/e2e/e2e-cloud-experimental/check-docs.sh b/test/e2e/e2e-cloud-experimental/check-docs.sh index ce6a40d659a..4de40642e1b 100755 --- a/test/e2e/e2e-cloud-experimental/check-docs.sh +++ b/test/e2e/e2e-cloud-experimental/check-docs.sh @@ -461,10 +461,11 @@ run_install_check() { local BOOTSTRAP_SH="$REPO_ROOT/install.sh" local PAYLOAD_SH="$REPO_ROOT/scripts/install.sh" - # The providers list has moved between layouts; tolerate both the legacy - # flat path and the post-refactor layered path. + # The provider definition has moved between layouts. Prefer the shared + # selection-key owner, then tolerate both legacy provider-module paths. local PROVIDERS_TS="" for _candidate in \ + "$REPO_ROOT/src/lib/onboard/inference-providers/provider-selection-keys.ts" \ "$REPO_ROOT/src/lib/onboard/providers.ts" \ "$REPO_ROOT/src/lib/onboard-providers.ts"; do if [[ -f "$_candidate" ]]; then @@ -483,12 +484,25 @@ run_install_check() { fi log "[install] comparing: NEMOCLAW_PROVIDER values in install.sh + scripts/install.sh" - log "[install] vs: ${PROVIDERS_TS#"$REPO_ROOT"/} canonical 'Valid values' list" + log "[install] vs: ${PROVIDERS_TS#"$REPO_ROOT"/} canonical provider list" - # The canonical values live in a single error-message line that lists every - # accepted NEMOCLAW_PROVIDER input. Extract the comma-separated payload. + # Read the shared TypeScript owner when present. Legacy layouts keep the + # canonical values in one error-message line. local _canonical - _canonical="$(grep -oE 'Valid values: [^"]+' "$PROVIDERS_TS" | head -1 | sed 's/^Valid values: //')" + if [[ "$PROVIDERS_TS" == */provider-selection-keys.ts ]]; then + _canonical="$( + "$NODE" --experimental-strip-types --no-warnings --input-type=module -e ' + import { pathToFileURL } from "node:url"; + const source = await import(pathToFileURL(process.argv[1]).href); + const prefix = "Valid values: "; + const value = source.NON_INTERACTIVE_PROVIDER_VALID_VALUES; + if (typeof value !== "string" || !value.startsWith(prefix)) process.exit(1); + process.stdout.write(value.slice(prefix.length)); + ' "$PROVIDERS_TS" + )" + else + _canonical="$(grep -oE 'Valid values: [^"]+' "$PROVIDERS_TS" | head -1 | sed 's/^Valid values: //')" + fi if [[ -z "$_canonical" ]]; then echo "check-docs: [install] could not locate canonical provider list in $PROVIDERS_TS" >&2 return 1 From 96ae71ed9aac1753a9a29f349b03a5c6841041ea Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 4 Sep 2026 13:57:50 -0700 Subject: [PATCH 14/19] refactor(onboard): preserve provider identity owners Signed-off-by: Aaron Erickson --- .../actions/sandbox/rebuild-target-staging.ts | 18 +++++++--- src/lib/hermes-provider-auth.ts | 4 ++- src/lib/onboard.ts | 7 ++-- .../hermes-provider-identity.ts | 5 +++ .../provider-selection-keys.test.ts | 28 +++++++++++++++ .../provider-selection-keys.ts | 35 +++++-------------- .../machine/handlers/preflight.test.ts | 5 +++ src/lib/onboard/machine/handlers/preflight.ts | 14 +++++--- .../machine/initial-flow-phases.test.ts | 2 ++ src/lib/onboard/provider-recovery.test.ts | 8 +++-- src/lib/onboard/provider-recovery.ts | 14 ++++---- src/lib/onboard/providers.test.ts | 11 ------ src/lib/onboard/providers.ts | 21 +++++------ 13 files changed, 102 insertions(+), 70 deletions(-) create mode 100644 src/lib/onboard/inference-providers/hermes-provider-identity.ts create mode 100644 src/lib/onboard/inference-providers/provider-selection-keys.test.ts diff --git a/src/lib/actions/sandbox/rebuild-target-staging.ts b/src/lib/actions/sandbox/rebuild-target-staging.ts index 8faf2536fad..9e1591e0c13 100644 --- a/src/lib/actions/sandbox/rebuild-target-staging.ts +++ b/src/lib/actions/sandbox/rebuild-target-staging.ts @@ -6,8 +6,12 @@ import { isN1xManagedVllmProviderModel, isRecordedN1xManagedVllmRebuildEligible, } from "../../domain/sandbox/n1x-managed-vllm-rebuild"; -import { isN1xOnboardingRecordedProvider } from "../../onboard/inference-providers/provider-selection-keys"; +import { isN1xOnboardingProviderKey } from "../../onboard/inference-providers/provider-selection-keys"; import { getStoredMessagingChannelConfig } from "../../onboard/messaging-config"; +import { + providerNameToOptionKey, + type RemoteProviderConfigEntryLike, +} from "../../onboard/provider-recovery"; import { createRebuildRouteHandoff, type RegistryInferenceRoute, @@ -27,6 +31,10 @@ import { } from "./rebuild-gpu-opt-out"; import { printRebuildPreflightFailure } from "./rebuild-preflight-error"; +const { REMOTE_PROVIDER_CONFIG } = require("../../onboard/providers") as { + REMOTE_PROVIDER_CONFIG: Record; +}; + export function prepareRebuildRecreateOptions( sandboxName: string, sb: RebuildSandboxEntry, @@ -120,9 +128,11 @@ export function stageRecordedDeferredN1xIntent( const recordedStandardProviderIsEligible = selectionMatchesRecord && !isManagedVllmIdentity && - isN1xOnboardingRecordedProvider(sandboxEntry.provider, { - hasNimContainer: Boolean(sandboxEntry.nimContainer), - }); + isN1xOnboardingProviderKey( + providerNameToOptionKey(REMOTE_PROVIDER_CONFIG, sandboxEntry.provider, { + hasNimContainer: Boolean(sandboxEntry.nimContainer), + }), + ); const recordedManagedVllmIsEligible = !sandboxEntry.nimContainer && isRecordedN1xManagedVllmRebuildEligible( diff --git a/src/lib/hermes-provider-auth.ts b/src/lib/hermes-provider-auth.ts index 327bfc11b50..624857d268f 100644 --- a/src/lib/hermes-provider-auth.ts +++ b/src/lib/hermes-provider-auth.ts @@ -12,8 +12,11 @@ import type { StdioOptions } from "node:child_process"; import { checkOpenAiInferenceProviderProfile } from "./adapters/openshell/provider-profile-registration"; +import { HERMES_PROVIDER_NAME } from "./onboard/inference-providers/hermes-provider-identity"; import * as oauth from "./oauth-device-code"; +export { HERMES_PROVIDER_NAME }; + const onboardProviders = require("./onboard/providers") as { providerExistsInGateway: (name: string, runOpenshell: RunOpenshell) => boolean; upsertProvider: ( @@ -39,7 +42,6 @@ function getHermesToolGatewayBroker(): HermesToolGatewayBroker { return require("./hermes-tool-gateway-broker") as HermesToolGatewayBroker; } -export const HERMES_PROVIDER_NAME = "hermes-provider"; export const HERMES_INFERENCE_CREDENTIAL_ENV = "OPENAI_API_KEY"; export const HERMES_NOUS_API_KEY_CREDENTIAL_ENV = "NOUS_API_KEY"; export const AGENT_KEY_MIN_TTL_SECONDS = 1800; diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 44fb3caf150..54674c214c9 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -364,6 +364,7 @@ const { ...onboardPromptHelpers }: typeof import("./onboard/prompt-helpers") = require("./onboard/prompt-helpers"); const providerRecovery: typeof import("./onboard/provider-recovery") = require("./onboard/provider-recovery"); +const providerKey = providerRecovery.providerNameToOptionKey.bind(null, REMOTE_PROVIDER_CONFIG); const openclawSetup: typeof import("./onboard/openclaw-setup") = require("./onboard/openclaw-setup"); const { createWebSearchFlowHelpers, @@ -2922,6 +2923,7 @@ async function runOnboard(opts: OnboardOptions = {}): Promise { detectGpu: nim.detectGpu, runPreflight: (preflightOptions) => preflight({ ...opts, ...preflightOptions }), assessHost, + providerNameToOptionKey: providerKey, assertOnboardHostReadiness: (host, gpu, options) => fatalRuntimePreflight.assertOnboardHostReadiness(host, gpu ?? null, { ...options, @@ -3484,10 +3486,7 @@ module.exports = { MESSAGING_CHANNELS, selectOnboardAgent, setupNim, - providerNameToOptionKey: ( - name: string | null | undefined, - opts: { hasNimContainer?: boolean } = {}, - ) => providerRecovery.providerNameToOptionKey(REMOTE_PROVIDER_CONFIG, name, opts), + providerNameToOptionKey: providerKey, readRecordedProvider, readRecordedModel, readRecordedNimContainer, diff --git a/src/lib/onboard/inference-providers/hermes-provider-identity.ts b/src/lib/onboard/inference-providers/hermes-provider-identity.ts new file mode 100644 index 00000000000..ed2e9a58486 --- /dev/null +++ b/src/lib/onboard/inference-providers/hermes-provider-identity.ts @@ -0,0 +1,5 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** Stable OpenShell provider name for Hermes Provider inference. */ +export const HERMES_PROVIDER_NAME = "hermes-provider"; diff --git a/src/lib/onboard/inference-providers/provider-selection-keys.test.ts b/src/lib/onboard/inference-providers/provider-selection-keys.test.ts new file mode 100644 index 00000000000..68eaa625ce9 --- /dev/null +++ b/src/lib/onboard/inference-providers/provider-selection-keys.test.ts @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; + +import { HERMES_PROVIDER_NAME } from "../../hermes-provider-auth"; +import { LLAMA_CPP_PROVIDER_NAME } from "../../inference/llama-cpp/contract"; +import { OPENROUTER_PROVIDER_NAME } from "../../inference/openrouter"; +import { providerNameToOptionKey } from "../provider-recovery"; + +const { REMOTE_PROVIDER_CONFIG } = require("../providers") as { + REMOTE_PROVIDER_CONFIG: Record; +}; + +describe("provider selection identities", () => { + it.each(Object.entries(REMOTE_PROVIDER_CONFIG))( + "maps declared provider %s back from its persisted name (#11041)", + (key, provider) => { + expect(providerNameToOptionKey(REMOTE_PROVIDER_CONFIG, provider.providerName)).toBe(key); + }, + ); + + it("uses the established runtime provider identities (#11041)", () => { + expect(REMOTE_PROVIDER_CONFIG.openrouter.providerName).toBe(OPENROUTER_PROVIDER_NAME); + expect(REMOTE_PROVIDER_CONFIG["llama-cpp"].providerName).toBe(LLAMA_CPP_PROVIDER_NAME); + expect(REMOTE_PROVIDER_CONFIG.hermesProvider.providerName).toBe(HERMES_PROVIDER_NAME); + }); +}); diff --git a/src/lib/onboard/inference-providers/provider-selection-keys.ts b/src/lib/onboard/inference-providers/provider-selection-keys.ts index 4e7c94ec9d5..e2708961853 100644 --- a/src/lib/onboard/inference-providers/provider-selection-keys.ts +++ b/src/lib/onboard/inference-providers/provider-selection-keys.ts @@ -41,23 +41,7 @@ export const NON_INTERACTIVE_PROVIDER_VALID_VALUES = `Valid values: ${Array.from (key) => (key === "hermesProvider" ? "hermes-provider" : key), ).join(", ")}`; -/** Gateway provider names declared by built-in provider selection keys. */ -export const REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY: Readonly> = { - build: "nvidia-prod", - openrouter: "openrouter-api", - openai: "openai-api", - anthropic: "anthropic-prod", - anthropicCompatible: "compatible-anthropic-endpoint", - gemini: "gemini-api", - hermesProvider: "hermes-provider", - custom: "compatible-endpoint", - "llama-cpp": "llama-cpp-local", -}; - const PERSISTED_PROVIDER_SELECTION_KEYS: Readonly> = { - ...Object.fromEntries( - Object.entries(REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY).map(([key, name]) => [name, key]), - ), "nvidia-router": "routed", "ollama-local": "ollama", "vllm-local": "vllm", @@ -65,6 +49,8 @@ const PERSISTED_PROVIDER_SELECTION_KEYS: Readonly> = { "nvidia-nim": "build", }; +export type RemoteProviderConfigEntryLike = { providerName?: string }; + export function normalizeNonInteractiveProviderKey( value: string | null | undefined, ): string | null { @@ -86,20 +72,17 @@ export function isN1xOnboardingProviderKey(value: string | null | undefined): bo export function persistedProviderNameToSelectionKey( value: string | null | undefined, options: { hasNimContainer?: boolean } = {}, + remoteProviderConfig: Readonly> = {}, ): string | null { const name = String(value ?? "").trim(); if (!name) return null; // Local NIM and standalone vLLM both persist as vllm-local. Only the // owner-only NIM container record distinguishes the excluded NIM route. if (name === "vllm-local") return options.hasNimContainer ? "nim-local" : "vllm"; - return PERSISTED_PROVIDER_SELECTION_KEYS[name] ?? normalizeNonInteractiveProviderKey(name); -} - -/** True only for a recognized persisted provider route that N1x permits. */ -export function isN1xOnboardingRecordedProvider( - value: string | null | undefined, - options: { hasNimContainer?: boolean } = {}, -): boolean { - const selection = persistedProviderNameToSelectionKey(value, options); - return selection !== null && selection !== "nim-local"; + const localOrLegacy = PERSISTED_PROVIDER_SELECTION_KEYS[name]; + if (localOrLegacy) return localOrLegacy; + for (const [key, config] of Object.entries(remoteProviderConfig)) { + if (config.providerName === name) return normalizeNonInteractiveProviderKey(key); + } + return normalizeNonInteractiveProviderKey(name); } diff --git a/src/lib/onboard/machine/handlers/preflight.test.ts b/src/lib/onboard/machine/handlers/preflight.test.ts index a768ed0929a..914265c708f 100644 --- a/src/lib/onboard/machine/handlers/preflight.test.ts +++ b/src/lib/onboard/machine/handlers/preflight.test.ts @@ -5,6 +5,7 @@ import { describe, expect, it, vi } from "vitest"; import type { GpuDetection } from "../../../inference/nim"; import { createSession, type Session } from "../../../state/onboard-session"; +import { persistedProviderNameToSelectionKey } from "../../inference-providers/provider-selection-keys"; import { resolveSandboxGpuConfig } from "../../sandbox-gpu-mode"; import { handlePreflightState, type PreflightStateOptions } from "./preflight"; @@ -54,6 +55,10 @@ function createDeps( detectGpu: () => ({ type: "nvidia" }) as Gpu, runPreflight: async () => ({ type: "nvidia" }) as Gpu, assessHost: () => ({ cdiNvidiaGpuSpecMissing: false }), + providerNameToOptionKey: ( + name: string | null | undefined, + options?: { hasNimContainer?: boolean }, + ) => persistedProviderNameToSelectionKey(name, options), assertOnboardHostReadiness: vi.fn(), assertGatewayReadiness: vi.fn(async () => undefined), resolveSandboxGpuConfig: ( diff --git a/src/lib/onboard/machine/handlers/preflight.ts b/src/lib/onboard/machine/handlers/preflight.ts index 51669940d54..1586f03884c 100644 --- a/src/lib/onboard/machine/handlers/preflight.ts +++ b/src/lib/onboard/machine/handlers/preflight.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { Session } from "../../../state/onboard-session"; -import { isN1xOnboardingRecordedProvider } from "../../inference-providers/provider-selection-keys"; +import { isN1xOnboardingProviderKey } from "../../inference-providers/provider-selection-keys"; import { withPreflightTrace } from "../../tracing"; import { advanceTo, type OnboardStateTransitionResult } from "../result"; @@ -49,6 +49,10 @@ export interface PreflightStateOptions< detectGpu(): Gpu; runPreflight(options: { optedOutGpuPassthrough?: boolean }): Promise; assessHost(): Host; + providerNameToOptionKey( + name: string | null | undefined, + options?: { hasNimContainer?: boolean }, + ): string | null; assertOnboardHostReadiness( host: Host, gpu: Gpu, @@ -152,9 +156,11 @@ export async function handlePreflightState< : { flag: null, device: null }; const effectiveSandboxGpuFlag = explicitSandboxGpuFlag ?? resumedSandboxGpuOverrides.flag; const effectiveSandboxGpuDevice = sandboxGpuDevice ?? resumedSandboxGpuOverrides.device; - const recordedProviderAllowsDeferredN1x = isN1xOnboardingRecordedProvider(session?.provider, { - hasNimContainer: Boolean(session?.nimContainer), - }); + const recordedProviderAllowsDeferredN1x = isN1xOnboardingProviderKey( + deps.providerNameToOptionKey(session?.provider, { + hasNimContainer: Boolean(session?.nimContainer), + }), + ); // An explicit false is authoritative for rebuilds. Ordinary resume may use // the provider that this owner-only session already validated and recorded. const allowDeferredN1xOnboarding = diff --git a/src/lib/onboard/machine/initial-flow-phases.test.ts b/src/lib/onboard/machine/initial-flow-phases.test.ts index 84788071e94..bdbab002d33 100644 --- a/src/lib/onboard/machine/initial-flow-phases.test.ts +++ b/src/lib/onboard/machine/initial-flow-phases.test.ts @@ -147,6 +147,7 @@ describe("initial onboard flow phases", () => { detectGpu: () => gpu, runPreflight: async () => (preflightFailure ? Promise.reject(preflightFailure) : gpu), assessHost: () => ({}), + providerNameToOptionKey: vi.fn(() => null), assertOnboardHostReadiness: vi.fn(), resolveSandboxGpuConfig: config, validateSandboxGpuPreflight: vi.fn(), @@ -399,6 +400,7 @@ describe("initial onboard flow phases", () => { calls.push("assess-host"); return { docker: true }; }), + providerNameToOptionKey: vi.fn(() => null), assertOnboardHostReadiness: vi.fn(() => { calls.push("assert-host-readiness"); }), diff --git a/src/lib/onboard/provider-recovery.test.ts b/src/lib/onboard/provider-recovery.test.ts index de35cc2aa8a..283880c8569 100644 --- a/src/lib/onboard/provider-recovery.test.ts +++ b/src/lib/onboard/provider-recovery.test.ts @@ -15,6 +15,10 @@ import { validateLiveGatewayInference, } from "./provider-recovery"; +const { REMOTE_PROVIDER_CONFIG } = require("./providers") as { + REMOTE_PROVIDER_CONFIG: Record; +}; + afterEach(() => { vi.restoreAllMocks(); }); @@ -37,8 +41,8 @@ describe("persisted provider selection", () => { ["an unknown provider", "unknown-provider", false], ] as const)("uses the shared mapping for %s (#11041)", (_label, provider, hasNimContainer) => { const options = { hasNimContainer }; - expect(providerNameToOptionKey({}, provider, options)).toBe( - persistedProviderNameToSelectionKey(provider, options), + expect(providerNameToOptionKey(REMOTE_PROVIDER_CONFIG, provider, options)).toBe( + persistedProviderNameToSelectionKey(provider, options, REMOTE_PROVIDER_CONFIG), ); }); }); diff --git a/src/lib/onboard/provider-recovery.ts b/src/lib/onboard/provider-recovery.ts index 39f6b90ee41..706b9e75ac0 100644 --- a/src/lib/onboard/provider-recovery.ts +++ b/src/lib/onboard/provider-recovery.ts @@ -10,9 +10,12 @@ import { normalizeInferenceEndpointSource, } from "../inference/selection"; import { getLiveGatewayInference } from "../inference/live"; -import { persistedProviderNameToSelectionKey } from "./inference-providers/provider-selection-keys"; +import { + persistedProviderNameToSelectionKey, + type RemoteProviderConfigEntryLike, +} from "./inference-providers/provider-selection-keys"; -export type RemoteProviderConfigEntryLike = { providerName?: string }; +export type { RemoteProviderConfigEntryLike } from "./inference-providers/provider-selection-keys"; interface VllmInstallResumeSession { readonly vllmInstallModel?: string | null; @@ -77,12 +80,7 @@ export function providerNameToOptionKey( opts: { hasNimContainer?: boolean } = {}, ): string | null { if (!name) return null; - const builtIn = persistedProviderNameToSelectionKey(name, opts); - if (builtIn) return builtIn; - for (const [key, cfg] of Object.entries(remoteProviderConfig)) { - if (cfg.providerName === name) return key; - } - return null; + return persistedProviderNameToSelectionKey(name, opts, remoteProviderConfig); } export interface ProviderRecoveryDeps { diff --git a/src/lib/onboard/providers.test.ts b/src/lib/onboard/providers.test.ts index 4ac162550e7..86935e42a70 100644 --- a/src/lib/onboard/providers.test.ts +++ b/src/lib/onboard/providers.test.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 import { describe, expect, it, vi } from "vitest"; -import { persistedProviderNameToSelectionKey } from "./inference-providers/provider-selection-keys"; type RunResult = { error?: unknown; @@ -204,16 +203,6 @@ function withProviderEnv(next: Record, testBody: () } describe("onboard provider helpers", () => { - it.each(Object.entries(REMOTE_PROVIDER_CONFIG))( - "maps declared provider %s back from its persisted name (#11041)", - (key, provider) => { - expect( - persistedProviderNameToSelectionKey(provider.providerName), - provider.providerName, - ).toBe(key); - }, - ); - it("uses Gemini 3.6 Flash as the onboarding default (#9298)", () => { expect(REMOTE_PROVIDER_CONFIG.gemini.defaultModel).toBe("gemini-3.6-flash"); }); diff --git a/src/lib/onboard/providers.ts b/src/lib/onboard/providers.ts index 20cd1bfdb87..e510ced1cb0 100644 --- a/src/lib/onboard/providers.ts +++ b/src/lib/onboard/providers.ts @@ -19,6 +19,7 @@ const { compactText } = require("../core/url-utils"); const { LLAMA_CPP_CREDENTIAL_ENV, LLAMA_CPP_HOST_OPENAI_BASE_URL, + LLAMA_CPP_PROVIDER_NAME, } = require("../inference/llama-cpp/contract"); const { inspectGatewayCredentialFamilyProviderBinding, @@ -35,9 +36,9 @@ const { NON_INTERACTIVE_PROVIDER_ALIASES, NON_INTERACTIVE_PROVIDER_KEYS, NON_INTERACTIVE_PROVIDER_VALID_VALUES, - REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY, normalizeNonInteractiveProviderKey, } = require("./inference-providers/provider-selection-keys"); +const { HERMES_PROVIDER_NAME } = require("./inference-providers/hermes-provider-identity"); const MESSAGING_PROVIDER_BINDING_CONFLICT = "NEMOCLAW_MESSAGING_PROVIDER_BINDING_CONFLICT"; const MESSAGING_PROVIDER_MUTATION_FAILURE = "NEMOCLAW_MESSAGING_PROVIDER_MUTATION_FAILURE"; @@ -181,7 +182,7 @@ const PROVIDER_KEY_ROUTE_VALUES = new Set( const REMOTE_PROVIDER_CONFIG = { build: { label: "NVIDIA Endpoints", - providerName: REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY.build, + providerName: "nvidia-prod", providerType: "nvidia", credentialEnv: "NVIDIA_INFERENCE_API_KEY", endpointUrl: BUILD_ENDPOINT_URL, @@ -192,7 +193,7 @@ const REMOTE_PROVIDER_CONFIG = { }, openrouter: { label: "OpenRouter", - providerName: REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY.openrouter, + providerName: openrouter.OPENROUTER_PROVIDER_NAME, providerType: openrouter.OPENROUTER_PROVIDER_TYPE, credentialEnv: openrouter.OPENROUTER_CREDENTIAL_ENV, endpointUrl: openrouter.OPENROUTER_ENDPOINT_URL, @@ -203,7 +204,7 @@ const REMOTE_PROVIDER_CONFIG = { }, openai: { label: "OpenAI", - providerName: REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY.openai, + providerName: "openai-api", providerType: "openai", credentialEnv: "OPENAI_API_KEY", endpointUrl: OPENAI_ENDPOINT_URL, @@ -214,7 +215,7 @@ const REMOTE_PROVIDER_CONFIG = { }, anthropic: { label: "Anthropic", - providerName: REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY.anthropic, + providerName: "anthropic-prod", providerType: "anthropic", credentialEnv: "ANTHROPIC_API_KEY", endpointUrl: ANTHROPIC_ENDPOINT_URL, @@ -224,7 +225,7 @@ const REMOTE_PROVIDER_CONFIG = { }, anthropicCompatible: { label: "Other Anthropic-compatible endpoint", - providerName: REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY.anthropicCompatible, + providerName: "compatible-anthropic-endpoint", providerType: "anthropic", credentialEnv: "COMPATIBLE_ANTHROPIC_API_KEY", endpointUrl: "", @@ -234,7 +235,7 @@ const REMOTE_PROVIDER_CONFIG = { }, gemini: { label: "Google Gemini", - providerName: REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY.gemini, + providerName: "gemini-api", providerType: "openai", credentialEnv: "GEMINI_API_KEY", endpointUrl: GEMINI_ENDPOINT_URL, @@ -252,7 +253,7 @@ const REMOTE_PROVIDER_CONFIG = { // without first selecting the entry. hermesProvider: { label: "Hermes Provider (Moonshot, Z-AI, MiniMax, Qwen, Xiaomi, Tencent, StepFun, xAI, Arcee)", - providerName: REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY.hermesProvider, + providerName: HERMES_PROVIDER_NAME, providerType: "openai", credentialEnv: "OPENAI_API_KEY", endpointUrl: HERMES_INFERENCE_ENDPOINT_URL, @@ -263,7 +264,7 @@ const REMOTE_PROVIDER_CONFIG = { }, custom: { label: "Other OpenAI-compatible endpoint", - providerName: REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY.custom, + providerName: "compatible-endpoint", providerType: "openai", credentialEnv: "COMPATIBLE_API_KEY", endpointUrl: "", @@ -274,7 +275,7 @@ const REMOTE_PROVIDER_CONFIG = { }, "llama-cpp": { label: "Local llama.cpp", - providerName: REMOTE_PROVIDER_NAMES_BY_SELECTION_KEY["llama-cpp"], + providerName: LLAMA_CPP_PROVIDER_NAME, providerType: "openai", credentialEnv: LLAMA_CPP_CREDENTIAL_ENV, endpointUrl: LLAMA_CPP_HOST_OPENAI_BASE_URL, From 525ce43b7d9bab1667fdbe89a6cee56eec0b430a Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 4 Sep 2026 14:23:28 -0700 Subject: [PATCH 15/19] fix(onboard): finish N1x ownership and guidance Signed-off-by: Aaron Erickson --- docs/get-started/quickstart-hermes.mdx | 2 +- .../quickstart-langchain-deepagents-code.mdx | 2 +- docs/get-started/quickstart.mdx | 2 +- .../actions/sandbox/rebuild-target-staging.ts | 16 +++++++++------- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/get-started/quickstart-hermes.mdx b/docs/get-started/quickstart-hermes.mdx index 2e4440aa6bf..ff37a433ee8 100644 --- a/docs/get-started/quickstart-hermes.mdx +++ b/docs/get-started/quickstart-hermes.mdx @@ -269,7 +269,7 @@ Use these details when your first-run path needs more control. On N1x, accept the preview prompt to apply those settings as explicit Deferred preview intent. Enter `n` to continue with interactive onboarding when you want to select the agent or other settings yourself. On N1x, setting `NEMOCLAW_NO_EXPRESS=1` also continues with ordinary onboarding. - Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set another provider to enter standard onboarding directly. + Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set a recognized provider other than Local NVIDIA NIM to enter standard onboarding directly. The first Hermes start can take several minutes while OpenShell pulls the managed image when it is not already cached. The N1x preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. Refer to [Set Up vLLM](../inference/local-inference/set-up-vllm) for managed model profiles and headless setup. diff --git a/docs/get-started/quickstart-langchain-deepagents-code.mdx b/docs/get-started/quickstart-langchain-deepagents-code.mdx index f37a137590e..a60d1fe5fb1 100644 --- a/docs/get-started/quickstart-langchain-deepagents-code.mdx +++ b/docs/get-started/quickstart-langchain-deepagents-code.mdx @@ -129,7 +129,7 @@ Use these details when you need more control during setup or after the first san On N1x, accept the preview prompt to apply those settings as explicit Deferred preview intent. Enter `n` to continue with interactive onboarding when you want to select the agent or other settings yourself. On N1x, setting `NEMOCLAW_NO_EXPRESS=1` also continues with ordinary onboarding. - Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set another provider to enter standard onboarding directly. + Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set a recognized provider other than Local NVIDIA NIM to enter standard onboarding directly. The N1x preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. diff --git a/docs/get-started/quickstart.mdx b/docs/get-started/quickstart.mdx index 292662074a0..878ea6aabf2 100644 --- a/docs/get-started/quickstart.mdx +++ b/docs/get-started/quickstart.mdx @@ -204,7 +204,7 @@ Use these details when your first-run path needs more control. The N1x path does not use DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Accepting the N1x prompt supplies the narrow managed-vLLM preview intent required to pass the pending-validation readiness finding. Declining the N1x prompt or setting `NEMOCLAW_NO_EXPRESS=1` continues with ordinary interactive onboarding. - Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set another provider to enter standard onboarding directly. + Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set a recognized provider other than Local NVIDIA NIM to enter standard onboarding directly. Refer to [Platform Support](../reference/platform-support) before you rely on this path. Before the Station express confirmation, the installer reports whether the Hugging Face download is authenticated without displaying the token value. diff --git a/src/lib/actions/sandbox/rebuild-target-staging.ts b/src/lib/actions/sandbox/rebuild-target-staging.ts index 9e1591e0c13..70f8fa7a842 100644 --- a/src/lib/actions/sandbox/rebuild-target-staging.ts +++ b/src/lib/actions/sandbox/rebuild-target-staging.ts @@ -6,12 +6,12 @@ import { isN1xManagedVllmProviderModel, isRecordedN1xManagedVllmRebuildEligible, } from "../../domain/sandbox/n1x-managed-vllm-rebuild"; -import { isN1xOnboardingProviderKey } from "../../onboard/inference-providers/provider-selection-keys"; -import { getStoredMessagingChannelConfig } from "../../onboard/messaging-config"; import { - providerNameToOptionKey, + isN1xOnboardingProviderKey, + persistedProviderNameToSelectionKey, type RemoteProviderConfigEntryLike, -} from "../../onboard/provider-recovery"; +} from "../../onboard/inference-providers/provider-selection-keys"; +import { getStoredMessagingChannelConfig } from "../../onboard/messaging-config"; import { createRebuildRouteHandoff, type RegistryInferenceRoute, @@ -129,9 +129,11 @@ export function stageRecordedDeferredN1xIntent( selectionMatchesRecord && !isManagedVllmIdentity && isN1xOnboardingProviderKey( - providerNameToOptionKey(REMOTE_PROVIDER_CONFIG, sandboxEntry.provider, { - hasNimContainer: Boolean(sandboxEntry.nimContainer), - }), + persistedProviderNameToSelectionKey( + sandboxEntry.provider, + { hasNimContainer: Boolean(sandboxEntry.nimContainer) }, + REMOTE_PROVIDER_CONFIG, + ), ); const recordedManagedVllmIsEligible = !sandboxEntry.nimContainer && From 1263985c7168269748d39ef26c5e331942a23d57 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 4 Sep 2026 14:41:03 -0700 Subject: [PATCH 16/19] fix(onboard): preserve providerless N1x resume intent Signed-off-by: Aaron Erickson --- .../machine/handlers/preflight.test.ts | 30 +++++++++++++++++++ src/lib/onboard/machine/handlers/preflight.ts | 6 ++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/lib/onboard/machine/handlers/preflight.test.ts b/src/lib/onboard/machine/handlers/preflight.test.ts index 914265c708f..4189602e436 100644 --- a/src/lib/onboard/machine/handlers/preflight.test.ts +++ b/src/lib/onboard/machine/handlers/preflight.test.ts @@ -250,6 +250,36 @@ describe("handlePreflightState", () => { ); }); + it.each([ + ["a declined preview before provider selection", { NEMOCLAW_NO_EXPRESS: "1" }, true], + [ + "an excluded NIM provider", + { NEMOCLAW_NO_EXPRESS: "1", NEMOCLAW_PROVIDER: "nim-local" }, + false, + ], + ] as const)("classifies %s during Deferred N1x resume (#11041)", async (_case, env, expected) => { + const session = createSession({ provider: null }); + session.steps.preflight.status = "complete"; + const assertOnboardHostReadiness = vi.fn(); + const harness = createDeps({ assertOnboardHostReadiness }); + + await handlePreflightState({ ...baseOptions(harness.deps, session), resume: true, env }); + + expect(assertOnboardHostReadiness).toHaveBeenCalledTimes(2); + expect(assertOnboardHostReadiness).toHaveBeenNthCalledWith( + 1, + expect.anything(), + expect.anything(), + expect.objectContaining({ allowDeferredN1xOnboarding: expected }), + ); + expect(assertOnboardHostReadiness).toHaveBeenNthCalledWith( + 2, + expect.anything(), + expect.anything(), + expect.objectContaining({ allowDeferredN1xOnboarding: expected }), + ); + }); + it.each([ ["persisted Local NIM", "vllm-local", "nemoclaw-nim", undefined, false], ["an unknown persisted provider", "obsolete-provider", null, undefined, false], diff --git a/src/lib/onboard/machine/handlers/preflight.ts b/src/lib/onboard/machine/handlers/preflight.ts index 1586f03884c..837c0b9f0d9 100644 --- a/src/lib/onboard/machine/handlers/preflight.ts +++ b/src/lib/onboard/machine/handlers/preflight.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { Session } from "../../../state/onboard-session"; +import { hasExplicitDeferredN1xOnboardingIntent } from "../../../readiness/onboard-admission"; import { isN1xOnboardingProviderKey } from "../../inference-providers/provider-selection-keys"; import { withPreflightTrace } from "../../tracing"; import { advanceTo, type OnboardStateTransitionResult } from "../result"; @@ -162,9 +163,10 @@ export async function handlePreflightState< }), ); // An explicit false is authoritative for rebuilds. Ordinary resume may use - // the provider that this owner-only session already validated and recorded. + // the current installer choice or the provider already validated and recorded. const allowDeferredN1xOnboarding = - allowDeferredN1xManagedVllm ?? recordedProviderAllowsDeferredN1x; + allowDeferredN1xManagedVllm ?? + (recordedProviderAllowsDeferredN1x || hasExplicitDeferredN1xOnboardingIntent(env)); let gpu: Gpu; if (resumePreflight) { From 94230236d20468ace052ad8f69cf38c2c5c8f484 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 4 Sep 2026 14:57:09 -0700 Subject: [PATCH 17/19] test(onboard): prove the N1x decline transition Signed-off-by: Aaron Erickson --- docs/inference/choose-inference-provider.mdx | 3 ++- docs/inference/set-up-vllm.mdx | 2 +- docs/security/best-practices.mdx | 2 +- test/installer-integration/install-express-prompt.test.ts | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/inference/choose-inference-provider.mdx b/docs/inference/choose-inference-provider.mdx index 83674679d5f..2ba5f884504 100644 --- a/docs/inference/choose-inference-provider.mdx +++ b/docs/inference/choose-inference-provider.mdx @@ -14,7 +14,8 @@ NemoClaw validates the provider and model before it creates the sandbox. On native N1x FASTOS, accept the Deferred managed-vLLM preview or enter standard onboarding by declining Express. -You can also set `NEMOCLAW_NO_EXPRESS=1` without a provider, or set a recognized `NEMOCLAW_PROVIDER` value other than Local NVIDIA NIM. +You can also enter standard onboarding by setting `NEMOCLAW_NO_EXPRESS=1` without a provider, or by setting a recognized `NEMOCLAW_PROVIDER` value other than Local NVIDIA NIM or `install-vllm`. +Set `NEMOCLAW_PROVIDER=install-vllm` only to bypass the prompt with the same Deferred managed-vLLM preview intent. N1x remains Deferred, each standard provider keeps its existing requirements, and local NVIDIA NIM remains unavailable. On a qualifying Windows WSL N1x host, Windows Express uses managed llama.cpp after Docker Desktop GPU passthrough succeeds. Other Windows WSL hosts retain Windows-host or WSL-local Ollama. diff --git a/docs/inference/set-up-vllm.mdx b/docs/inference/set-up-vllm.mdx index b7cc66d8cb1..7e008810ed3 100644 --- a/docs/inference/set-up-vllm.mdx +++ b/docs/inference/set-up-vllm.mdx @@ -476,7 +476,7 @@ Accepting the preview prompt sets explicit managed-vLLM intent for the narrow pe Declining the prompt continues with ordinary interactive onboarding. Setting `NEMOCLAW_NO_EXPRESS=1` also enters ordinary onboarding when no provider is preselected or the explicit provider is recognized and is not Local NVIDIA NIM. An invalid or Local NVIDIA NIM provider remains blocked. -Set a recognized provider other than Local NVIDIA NIM to enter ordinary onboarding directly. +Set a recognized provider other than Local NVIDIA NIM or `install-vllm` to enter ordinary onboarding directly. Set `NEMOCLAW_PROVIDER=install-vllm` when you need to bypass the prompt with the same explicit preview intent. On a host that still meets the N1x identity checks, `rebuild` reuses the recorded route as explicit preview intent only when all of these conditions hold: diff --git a/docs/security/best-practices.mdx b/docs/security/best-practices.mdx index c99295dc16b..faa0e447060 100644 --- a/docs/security/best-practices.mdx +++ b/docs/security/best-practices.mdx @@ -344,7 +344,7 @@ Native N1x requires you to decline or disable Express or set `NEMOCLAW_PROVIDER= | Aspect | Detail | |---|---| -| Default | Local NVIDIA NIM and generic Linux managed vLLM install/start are hidden. When Docker is available, DGX Spark and DGX Station managed vLLM entries are offered by default. Standard onboarding offers already-running vLLM on `localhost:${NEMOCLAW_VLLM_PORT:-8000}` when detected; native N1x requires explicit standard-onboarding intent. Qualifying Windows WSL N1x Express runs use managed llama.cpp. | +| Default | Local NVIDIA NIM and generic Linux managed vLLM install/start are hidden. When Docker is available, DGX Spark and DGX Station managed vLLM entries are offered by default. Native N1x separately offers the Deferred managed-vLLM preview; its standard onboarding offers already-running vLLM on `localhost:${NEMOCLAW_VLLM_PORT:-8000}` when detected and requires explicit standard-onboarding intent. Qualifying Windows WSL N1x Express runs use managed llama.cpp. | | What you can change | On eligible hosts other than native N1x FASTOS, set `NEMOCLAW_EXPERIMENTAL=1` before onboarding to surface Local NIM and, when Docker is available on `PATH`, generic Linux managed vLLM. To request only the Docker-backed managed vLLM path non-interactively, set `NEMOCLAW_PROVIDER=install-vllm`. | | Risk if selected | NemoClaw has not fully validated these providers. NIM requires a NIM-capable GPU. The managed vLLM path requires Docker, pulls a container image, and starts it on a supported NVIDIA GPU host. Misconfiguration can cause failed inference or unexpected behavior. | | Recommendation | Use experimental providers only for evaluation. Do not rely on them for always-on assistants. | diff --git a/test/installer-integration/install-express-prompt.test.ts b/test/installer-integration/install-express-prompt.test.ts index c70c9a2151a..e87753c1cf3 100644 --- a/test/installer-integration/install-express-prompt.test.ts +++ b/test/installer-integration/install-express-prompt.test.ts @@ -17,6 +17,7 @@ describe("installer express install prompt (sourced)", () => { expect(result.error, output).toBeUndefined(); expect(result.status, output).toBe(0); + expect(output).toContain("Skipping express install. Continuing with interactive flow."); expect(output.match(/Detected container runtime: docker/g)).toHaveLength(1); expect(output).not.toContain("Host preflight found issues"); expect(output.match(/ONBOARD NO_EXPRESS=1 PROVIDER= ARGS=onboard/g)).toHaveLength(1); From 93fd95342077bba7892b075b20a59db67a249d0e Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 4 Sep 2026 15:14:06 -0700 Subject: [PATCH 18/19] test(ci): own installer provider parity Signed-off-by: Aaron Erickson --- .../cli/public-cli-contracts.test.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/test/package-contract/cli/public-cli-contracts.test.ts b/test/package-contract/cli/public-cli-contracts.test.ts index 59a5b46039c..8b60f89ccdf 100644 --- a/test/package-contract/cli/public-cli-contracts.test.ts +++ b/test/package-contract/cli/public-cli-contracts.test.ts @@ -87,6 +87,35 @@ function runCliParity(fixture: CliParityFixture, env: NodeJS.ProcessEnv = {}) { }); } +function createInstallParityFixture() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-docs-install-parity-")); + for (const relativeFile of [ + "install.sh", + "scripts/install.sh", + "src/lib/onboard/inference-providers/provider-selection-keys.ts", + "docs/reference/commands.mdx", + "test/e2e/e2e-cloud-experimental/check-docs.sh", + ]) { + const destination = path.join(root, relativeFile); + fs.mkdirSync(path.dirname(destination), { recursive: true }); + fs.copyFileSync(path.join(REPO_ROOT, relativeFile), destination); + } + return { + checkDocs: path.join(root, "test", "e2e", "e2e-cloud-experimental", "check-docs.sh"), + root, + }; +} + +function runInstallParity(fixture: ReturnType) { + return spawnSync("bash", [fixture.checkDocs, "--only-install"], { + cwd: fixture.root, + encoding: "utf-8", + env: { ...process.env, NODE: process.execPath }, + killSignal: "SIGKILL", + timeout: 30_000, + }); +} + function readCliInvocations(fixture: CliParityFixture): string[] { return fs.readFileSync(fixture.nodeInvocationLog, "utf-8").trim().split("\n"); } @@ -153,6 +182,39 @@ describe("public compiled CLI contracts", () => { } }); + it("keeps installer provider help aligned with its shared owner (#11041)", () => { + const fixture = createInstallParityFixture(); + + try { + const result = runInstallParity(fixture); + expect(result.error).toBeUndefined(); + expect(result.signal).toBeNull(); + expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); + expect(result.stdout).toContain("check-docs: running: [install]"); + expect(result.stdout).toContain("[install] parity OK"); + } finally { + fs.rmSync(fixture.root, { force: true, recursive: true }); + } + }); + + it("rejects a canonical provider missing from installer help (#11041)", () => { + const fixture = createInstallParityFixture(); + const bootstrap = path.join(fixture.root, "install.sh"); + const source = fs.readFileSync(bootstrap, "utf-8"); + fs.writeFileSync(bootstrap, source.replace("| gemini |", "|")); + + try { + const result = runInstallParity(fixture); + expect(result.error).toBeUndefined(); + expect(result.signal).toBeNull(); + expect(result.status).toBe(1); + expect(result.stderr).toContain('provider "gemini" canonical but absent'); + expect(result.stderr).toContain("install.sh bootstrap_usage"); + } finally { + fs.rmSync(fixture.root, { force: true, recursive: true }); + } + }); + it("validates every repository-local documentation link (#7616)", { timeout: 150_000, }, () => { From 60226d090172432408205104d78cf1d1b84f7ff2 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Fri, 4 Sep 2026 15:47:26 -0700 Subject: [PATCH 19/19] docs(onboard): distinguish N1x preview intent Signed-off-by: Aaron Erickson --- docs/get-started/quickstart-hermes.mdx | 2 +- docs/get-started/quickstart-langchain-deepagents-code.mdx | 2 +- docs/get-started/quickstart.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/get-started/quickstart-hermes.mdx b/docs/get-started/quickstart-hermes.mdx index ff37a433ee8..b865d109b12 100644 --- a/docs/get-started/quickstart-hermes.mdx +++ b/docs/get-started/quickstart-hermes.mdx @@ -269,7 +269,7 @@ Use these details when your first-run path needs more control. On N1x, accept the preview prompt to apply those settings as explicit Deferred preview intent. Enter `n` to continue with interactive onboarding when you want to select the agent or other settings yourself. On N1x, setting `NEMOCLAW_NO_EXPRESS=1` also continues with ordinary onboarding. - Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set a recognized provider other than Local NVIDIA NIM to enter standard onboarding directly. + Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set a recognized provider other than Local NVIDIA NIM or `install-vllm` to enter standard onboarding directly. The first Hermes start can take several minutes while OpenShell pulls the managed image when it is not already cached. The N1x preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. Refer to [Set Up vLLM](../inference/local-inference/set-up-vllm) for managed model profiles and headless setup. diff --git a/docs/get-started/quickstart-langchain-deepagents-code.mdx b/docs/get-started/quickstart-langchain-deepagents-code.mdx index a60d1fe5fb1..de574fed1a8 100644 --- a/docs/get-started/quickstart-langchain-deepagents-code.mdx +++ b/docs/get-started/quickstart-langchain-deepagents-code.mdx @@ -129,7 +129,7 @@ Use these details when you need more control during setup or after the first san On N1x, accept the preview prompt to apply those settings as explicit Deferred preview intent. Enter `n` to continue with interactive onboarding when you want to select the agent or other settings yourself. On N1x, setting `NEMOCLAW_NO_EXPRESS=1` also continues with ordinary onboarding. - Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set a recognized provider other than Local NVIDIA NIM to enter standard onboarding directly. + Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set a recognized provider other than Local NVIDIA NIM or `install-vllm` to enter standard onboarding directly. The N1x preview selects one-host managed vLLM with `nvidia/Qwen3.6-35B-A3B-NVFP4`. diff --git a/docs/get-started/quickstart.mdx b/docs/get-started/quickstart.mdx index 878ea6aabf2..d414b28054e 100644 --- a/docs/get-started/quickstart.mdx +++ b/docs/get-started/quickstart.mdx @@ -204,7 +204,7 @@ Use these details when your first-run path needs more control. The N1x path does not use DGX Spark cluster discovery, the fixed catalog path, managed llama.cpp, or NVIDIA NIM. Accepting the N1x prompt supplies the narrow managed-vLLM preview intent required to pass the pending-validation readiness finding. Declining the N1x prompt or setting `NEMOCLAW_NO_EXPRESS=1` continues with ordinary interactive onboarding. - Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set a recognized provider other than Local NVIDIA NIM to enter standard onboarding directly. + Set `NEMOCLAW_PROVIDER=install-vllm` to bypass the prompt with managed-vLLM preview intent, or set a recognized provider other than Local NVIDIA NIM or `install-vllm` to enter standard onboarding directly. Refer to [Platform Support](../reference/platform-support) before you rely on this path. Before the Station express confirmation, the installer reports whether the Hugging Face download is authenticated without displaying the token value.