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..b865d109b12 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 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 2ddbfbef6a6..de574fed1a8 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 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 1dde40622a4..d414b28054e 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 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.
diff --git a/docs/inference/choose-inference-provider.mdx b/docs/inference/choose-inference-provider.mdx
index 8f123ad2836..2ba5f884504 100644
--- a/docs/inference/choose-inference-provider.mdx
+++ b/docs/inference/choose-inference-provider.mdx
@@ -13,8 +13,10 @@ 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.
+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.
@@ -37,8 +39,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 +68,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 +78,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..7e008810ed3 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 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:
@@ -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..faa0e447060 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. 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/scripts/install.sh b/scripts/install.sh
index 7b5ba14f419..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,6 +3721,7 @@ run_installer_host_preflight() {
// authoritative onboarding gate apply supported storage remediation,
// but only when the gateway declaration confirms NemoClaw ownership.
allowStorageRemediation,
+ allowDeferredN1xManagedVllm: 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."
@@ -6066,7 +6067,7 @@ maybe_offer_express_install() {
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}."
+ 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
@@ -6160,7 +6161,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/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..70f8fa7a842 100644
--- a/src/lib/actions/sandbox/rebuild-target-staging.ts
+++ b/src/lib/actions/sandbox/rebuild-target-staging.ts
@@ -2,7 +2,15 @@
// 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 {
+ isN1xOnboardingProviderKey,
+ persistedProviderNameToSelectionKey,
+ type RemoteProviderConfigEntryLike,
+} from "../../onboard/inference-providers/provider-selection-keys";
import { getStoredMessagingChannelConfig } from "../../onboard/messaging-config";
import {
createRebuildRouteHandoff,
@@ -23,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,
@@ -86,8 +98,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 +109,7 @@ export function stageRecordedManagedVllmIntent(
| "endpointSource"
| "openshellDriver"
| "hostLocalInferenceReceipt"
+ | "nimContainer"
>,
rebuildSelection: {
provider: string;
@@ -105,13 +118,31 @@ 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 &&
+ isN1xOnboardingProviderKey(
+ persistedProviderNameToSelectionKey(
+ sandboxEntry.provider,
+ { hasNimContainer: Boolean(sandboxEntry.nimContainer) },
+ REMOTE_PROVIDER_CONFIG,
+ ),
+ );
+ 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/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/fatal-runtime-preflight.test.ts b/src/lib/onboard/fatal-runtime-preflight.test.ts
index 4c0651bce19..197f1aaf1fd 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("requires explicit N1x intent and lets rebuild reject ambient intent (#9292)", () => {
const readiness: SystemReadinessReport = {
schemaVersion: "1.1.0",
status: "incompatible",
@@ -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);
@@ -214,10 +214,19 @@ 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);
+
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 5609b9ba2f0..e708981953b 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,
@@ -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,8 +180,7 @@ export function assertOnboardSystemReadiness(
allowStorageRemediation: options.allowStorageRemediation === true,
allowPortableHostPreparation: options.allowPortableHostPreparation,
allowDeferredN1xManagedVllm:
- options.allowDeferredN1xManagedVllm ??
- process.env.NEMOCLAW_PROVIDER === MANAGED_VLLM_PROVIDER_KEY,
+ options.allowDeferredN1xOnboarding ?? hasExplicitDeferredN1xOnboardingIntent(process.env),
});
const advisories = planHostAdvisories(host, {
providerOwnsHostReadiness: selectedRuntimeOwnsHostReadiness,
@@ -315,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,
@@ -410,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,
});
@@ -593,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/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
new file mode 100644
index 00000000000..e2708961853
--- /dev/null
+++ b/src/lib/onboard/inference-providers/provider-selection-keys.ts
@@ -0,0 +1,88 @@
+// 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 const NON_INTERACTIVE_PROVIDER_VALID_VALUES = `Valid values: ${Array.from(
+ NON_INTERACTIVE_PROVIDER_KEYS,
+ (key) => (key === "hermesProvider" ? "hermes-provider" : key),
+).join(", ")}`;
+
+const PERSISTED_PROVIDER_SELECTION_KEYS: Readonly> = {
+ "nvidia-router": "routed",
+ "ollama-local": "ollama",
+ "vllm-local": "vllm",
+ // This legacy name identifies NVIDIA Endpoints, not Local NVIDIA NIM.
+ "nvidia-nim": "build",
+};
+
+export type RemoteProviderConfigEntryLike = { providerName?: string };
+
+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 } = {},
+ 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";
+ 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 de5cf84b4d0..4189602e436 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: (
@@ -212,16 +217,103 @@ 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([
+ ["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],
+ ["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..837c0b9f0d9 100644
--- a/src/lib/onboard/machine/handlers/preflight.ts
+++ b/src/lib/onboard/machine/handlers/preflight.ts
@@ -2,6 +2,8 @@
// 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";
@@ -48,6 +50,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,
@@ -56,7 +62,7 @@ export interface PreflightStateOptions<
observedAt?: string;
now?: () => Date;
wslDockerDesktopGpuProofPassed?: boolean;
- allowDeferredN1xManagedVllm?: boolean;
+ allowDeferredN1xOnboarding?: boolean;
resuming: true;
presentAdvisories?: boolean;
},
@@ -151,6 +157,16 @@ export async function handlePreflightState<
: { flag: null, device: null };
const effectiveSandboxGpuFlag = explicitSandboxGpuFlag ?? resumedSandboxGpuOverrides.flag;
const effectiveSandboxGpuDevice = sandboxGpuDevice ?? resumedSandboxGpuOverrides.device;
+ const recordedProviderAllowsDeferredN1x = isN1xOnboardingProviderKey(
+ deps.providerNameToOptionKey(session?.provider, {
+ hasNimContainer: Boolean(session?.nimContainer),
+ }),
+ );
+ // An explicit false is authoritative for rebuilds. Ordinary resume may use
+ // the current installer choice or the provider already validated and recorded.
+ const allowDeferredN1xOnboarding =
+ allowDeferredN1xManagedVllm ??
+ (recordedProviderAllowsDeferredN1x || hasExplicitDeferredN1xOnboardingIntent(env));
let gpu: Gpu;
if (resumePreflight) {
@@ -177,7 +193,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 +216,7 @@ export async function handlePreflightState<
observedAt: hostObservedAt,
now,
...(wslDockerDesktopGpuProofPassed === undefined ? {} : { wslDockerDesktopGpuProofPassed }),
- allowDeferredN1xManagedVllm,
+ allowDeferredN1xOnboarding,
resuming: true,
presentAdvisories: false,
});
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 9649ab5883d..283880c8569 100644
--- a/src/lib/onboard/provider-recovery.test.ts
+++ b/src/lib/onboard/provider-recovery.test.ts
@@ -5,18 +5,48 @@ 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";
+const { REMOTE_PROVIDER_CONFIG } = require("./providers") as {
+ REMOTE_PROVIDER_CONFIG: Record;
+};
+
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(REMOTE_PROVIDER_CONFIG, provider, options)).toBe(
+ persistedProviderNameToSelectionKey(provider, options, REMOTE_PROVIDER_CONFIG),
+ );
+ });
+});
+
describe("validateLiveGatewayInference", () => {
it("accepts a complete bounded provider/model pair", () => {
expect(
@@ -97,22 +127,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 +332,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 +478,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..706b9e75ac0 100644
--- a/src/lib/onboard/provider-recovery.ts
+++ b/src/lib/onboard/provider-recovery.ts
@@ -10,8 +10,12 @@ import {
normalizeInferenceEndpointSource,
} from "../inference/selection";
import { getLiveGatewayInference } from "../inference/live";
+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;
@@ -52,10 +56,7 @@ export function applyVllmInstallResumeDefaults(
}
export function vllmInstallRecoveryOptions(
- deps: Pick<
- VllmInstallResumeDeps,
- "checkpointVllmInstallModel" | "getVllmInstallResumeModel"
- >,
+ deps: Pick,
access: VllmInstallResumeSessionAccess = onboardSession,
): {
checkpointInstallIntent?: (modelId: string) => void;
@@ -79,23 +80,7 @@ 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";
- 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.ts b/src/lib/onboard/providers.ts
index a60d00bc1b2..e510ced1cb0 100644
--- a/src/lib/onboard/providers.ts
+++ b/src/lib/onboard/providers.ts
@@ -32,6 +32,13 @@ const {
MESSAGING_CREDENTIAL_PROVIDER_TYPE,
} = require("../messaging/provider-profile");
const { ensureWebSearchProviderProfiles } = require("./brave-provider-profile");
+const {
+ NON_INTERACTIVE_PROVIDER_ALIASES,
+ NON_INTERACTIVE_PROVIDER_KEYS,
+ NON_INTERACTIVE_PROVIDER_VALID_VALUES,
+ 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";
@@ -164,41 +171,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(
[
"inference",
@@ -281,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: "hermes-provider",
+ providerName: HERMES_PROVIDER_NAME,
providerType: "openai",
credentialEnv: "OPENAI_API_KEY",
endpointUrl: HERMES_INFERENCE_ENDPOINT_URL,
@@ -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/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;
diff --git a/src/lib/readiness/onboard-admission.test.ts b/src/lib/readiness/onboard-admission.test.ts
index 0892a931b61..f17d3c39d4e 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,27 @@ 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],
+ ["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) => {
+ expect(hasExplicitDeferredN1xOnboardingIntent(env)).toBe(expected);
+ });
+});
+
function capability(id: string, state: ReadinessCapability["state"]): ReadinessCapability {
return { id, state };
}
@@ -242,10 +264,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 +305,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..e2c28721a9e 100644
--- a/src/lib/readiness/onboard-admission.ts
+++ b/src/lib/readiness/onboard-admission.ts
@@ -2,6 +2,16 @@
// 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 {
+ 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 = {
blockingFindings: "onboard.readiness.blocking_findings",
@@ -53,7 +63,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/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
diff --git a/test/helpers/installer-express-prompt-pty-harness.ts b/test/helpers/installer-express-prompt-pty-harness.ts
index 3e232eace25..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"; }
@@ -64,11 +147,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 +168,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 5231e31a82a..194ceae1d40 100644
--- a/test/install/install-express-n1x.test.ts
+++ b/test/install/install-express-n1x.test.ts
@@ -30,29 +30,27 @@ 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)", () => {
- const result = runExpressPromptWithTty("", "pipe", "N1x", {
- NEMOCLAW_NO_EXPRESS: "1",
- });
+ it.each([
+ ["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(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/);
+ expect(output).toMatch(/RESULT .*NO_EXPRESS=1/);
+ expect(output).toContain(`PROVIDER=${provider} `);
});
it("allows the N1x prompt bypass with explicit managed-vLLM intent (#8574)", () => {
@@ -96,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"
@@ -131,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/installer-integration/install-express-prompt.test.ts b/test/installer-integration/install-express-prompt.test.ts
index 3c9fcb57509..e87753c1cf3 100644
--- a/test/installer-integration/install-express-prompt.test.ts
+++ b/test/installer-integration/install-express-prompt.test.ts
@@ -11,6 +11,18 @@ 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).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);
+ });
+
it("drains PTY output after the child exits", () => {
const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-pty-tail-"));
try {
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,
}, () => {
diff --git a/test/package-contract/installer-host-preflight.test.ts b/test/package-contract/installer-host-preflight.test.ts
index 95e80aee7b3..ab45864bb07 100644
--- a/test/package-contract/installer-host-preflight.test.ts
+++ b/test/package-contract/installer-host-preflight.test.ts
@@ -27,9 +27,21 @@ 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[];
@@ -42,6 +54,9 @@ 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-"));
@@ -129,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: [] };
@@ -156,6 +200,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,13 +237,21 @@ 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";
`,
- );
+ );
+ 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;
@@ -208,6 +265,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 +291,85 @@ run_installer_host_preflight
}
describe("installer host preflight package contract", () => {
+ it.each([
+ ["NEMOCLAW_NO_EXPRESS", { 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/);
+ },
+ 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"],
+ },
+ 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",