From 9b81b72141f72e70e4b394269865c21c184046b4 Mon Sep 17 00:00:00 2001 From: latenighthackathon Date: Thu, 6 Aug 2026 02:03:55 +0000 Subject: [PATCH 1/9] fix(onboard): prove native Linux ARM64 GPUs with denylisted names Onboarding reported "no NVIDIA GPU detected" on native Linux ARM64 hosts whose GPU reports a denylisted JMJWOA-Generic-* name, even where docker run --gpus all runs a CUDA workload successfully. detectGpu() treats that name as untrusted unless firmware confirms a Spark, Station, or Jetson host, and clears it only through the bounded Docker CUDA proof. The prover returned null unless the host was Docker Desktop-backed WSL, so a native Linux ARM64 host had no way to prove its GPU and onboarding fell through to the remote provider menu. Run the bounded proof on native ARM64 Linux as well. A WSL host that is not Docker Desktop-backed still returns null, so Windows-on-ARM passthrough scope is unchanged. The CUDA workload remains the trust boundary, so the Snapdragon nvidia-smi shim still fails closed wherever the proof runs. Signed-off-by: latenighthackathon --- docs/reference/commands.mdx | 1 + docs/reference/troubleshooting.mdx | 14 +++++- src/lib/inference/gpu-trust.ts | 3 +- src/lib/inference/nim.ts | 18 ++++--- .../onboard/wsl-docker-desktop-gpu.test.ts | 49 +++++++++++++++++-- src/lib/onboard/wsl-docker-desktop-gpu.ts | 32 ++++++------ 6 files changed, 89 insertions(+), 28 deletions(-) diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index e1a5aebfb5f..cb2c64b7e1f 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -796,6 +796,7 @@ $$nemoclaw onboard --from ./Dockerfile.custom When `$$nemoclaw onboard` detects an NVIDIA GPU on the host, it enables OpenShell GPU passthrough at both the gateway and sandbox level by default. Detection proceeds along two paths. The `nvidia-smi`-based paths (the primary `--query-gpu=name,memory.total,memory.free` probe and the unified-memory `--query-gpu=name` fallback) require `nvidia-smi` to succeed and, on hosts whose firmware does not classify as a known NVIDIA platform (DGX Spark, DGX Station, Jetson, or Tegra), additionally require that the GPU name does not match the placeholder family observed on the Windows-on-ARM WSL2 nvidia-smi shim (`JMJWOA-Generic-*`) and that either the host is not ARM64 Linux (the observed shim is Windows-on-ARM only) or the NVIDIA kernel driver is bound (`/proc/driver/nvidia/` present), so that placeholder shims on non-NVIDIA hardware are not mistaken for real GPUs. +An ARM64 Linux host that is native or Docker Desktop-backed WSL clears a `JMJWOA-Generic-*` name only when one bounded `docker run --gpus all nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0` workload succeeds, which may first pull that image; every other host still fails closed. Jetson/Tegra hosts that ship without `nvidia-smi` continue to be detected via the devicetree firmware fallback (`/sys/firmware/devicetree/base/model`) or the Tegra device-node fallback (`/dev/nvhost-gpu`, `/dev/nvhost-ctrl-gpu`, `/dev/nvhost-ctrl`, or `/dev/nvmap`); both bypass the trust-tier gate above. Use `--no-gpu` to opt out when you want host-side inference providers only and do not need direct GPU access inside the sandbox. Use `--gpu` to require GPU passthrough and fail fast if an NVIDIA GPU is not detected. diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index 09ab640bd8d..aafa9b5d9ec 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -802,8 +802,20 @@ The wizard cleans up stale port forwards and waits for gateway readiness automat ### GPU setup fails with a placeholder GPU name -On Windows or WSL hosts, some systems report a placeholder display adapter name even when no NVIDIA GPU firmware is present. +On Windows, WSL, and native Linux ARM64 hosts, some systems report a placeholder display adapter name even when no NVIDIA GPU firmware is present. NVIDIA NIM and GPU-backed sandbox setup require a real NVIDIA GPU. + +On ARM64 Linux hosts that are native or Docker Desktop-backed WSL, onboarding clears a placeholder name with one bounded Docker CUDA workload before rejecting the GPU: + +``` +docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0 +``` + +The run may first pull the CUDA sample image and is bounded to 3 minutes. +Set `NEMOCLAW_WSL_GPU_PROOF_TIMEOUT_MS` to raise that bound on slow links. +A host whose GPU passes the workload keeps GPU passthrough; a host that fails or times out falls back to CPU. +WSL hosts that are not Docker Desktop-backed do not run the workload and continue to report the GPU as unavailable. + If NemoClaw rejects the detected GPU name during preflight, select a CPU or remote inference provider, or move the setup to a host with a supported NVIDIA GPU and current drivers. Jetson/Tegra hosts support sandbox GPU passthrough through the compatibility route. diff --git a/src/lib/inference/gpu-trust.ts b/src/lib/inference/gpu-trust.ts index f91cf27e755..20484399fe2 100644 --- a/src/lib/inference/gpu-trust.ts +++ b/src/lib/inference/gpu-trust.ts @@ -39,7 +39,8 @@ export interface DockerGpuProofResult { // Optional accept-path used by `detectGpu()` when an ARM64 Linux host reports a // denylisted `JMJWOA-Generic-*` placeholder. The prover returns `null` when the -// host is not a proof candidate (not ARM64 WSL Docker Desktop), preserving the +// host is not a proof candidate (not ARM64 Linux that is native or Docker +// Desktop-backed WSL, #8096), preserving the // #3988 fail-closed default; otherwise it returns the bounded Docker GPU proof // outcome so a passing real GPU can be trusted without trusting the name alone. export type Arm64WslDockerDesktopGpuProver = (gpuNames: string[]) => DockerGpuProofResult | null; diff --git a/src/lib/inference/nim.ts b/src/lib/inference/nim.ts index 852bd500dac..07b77e97775 100644 --- a/src/lib/inference/nim.ts +++ b/src/lib/inference/nim.ts @@ -84,12 +84,13 @@ export interface GpuDetection { // skips `computeIntensive` registry entries on these hosts. computeConstrained?: boolean; // Set when a denylisted `JMJWOA-Generic-*` placeholder name was accepted only - // because a bounded Docker `--gpus` CUDA proof passed (Windows-ARM N1X + WSL2 - // + Docker Desktop, #4565). Diagnostic marker that this detection cleared a - // live proof rather than firmware/name trust. The sandbox GPU preflight still - // reaches the Docker Desktop WSL compatibility branch via its own - // `detectWslDockerDesktopStatus()` check (consistent because the proof itself - // requires Docker Desktop WSL); this flag does not gate that branch. + // because a bounded Docker `--gpus` CUDA proof passed, on Windows-ARM N1X + // (WSL2 + Docker Desktop, #4565) or native ARM64 Linux (#8096). Diagnostic + // marker that this detection cleared a live proof rather than firmware/name + // trust. The sandbox GPU preflight reaches the Docker Desktop WSL + // compatibility branch via its own `detectWslDockerDesktopStatus()` check, + // which stays false on a native Linux host; this flag does not gate that + // branch. wslDockerDesktopGpuProofPassed?: boolean; } @@ -510,9 +511,10 @@ export function detectGpu(deps: DetectGpuDeps = {}): GpuDetection | null { nimCapable: canRunNimWithMemory(totalMemoryMB), platform, spark: platform === "spark", - // The proof-passed Windows-ARM N1X iGPU is memory-shared like Jetson - // and cannot serve a computeIntensive model in-loop, so tag it + // The proof-passed ARM64 N1X GPU is memory-shared like Jetson and + // cannot serve a computeIntensive model in-loop, so tag it // computeConstrained to exclude those Ollama bootstrap models (#3707). + // This covers the N1X part on WSL2 and on native Linux (#8096). ...(platform === "jetson" || wslDockerDesktopGpuProofPassed ? { computeConstrained: true } : {}), diff --git a/src/lib/onboard/wsl-docker-desktop-gpu.test.ts b/src/lib/onboard/wsl-docker-desktop-gpu.test.ts index 4ae5344041a..db0d540f89d 100644 --- a/src/lib/onboard/wsl-docker-desktop-gpu.test.ts +++ b/src/lib/onboard/wsl-docker-desktop-gpu.test.ts @@ -85,17 +85,58 @@ describe("createArm64WslDockerDesktopGpuProver (#4565)", () => { expect(runProof).not.toHaveBeenCalled(); }); - it("returns null when the host is not Docker Desktop-backed WSL", () => { + it("proves a denylisted GPU name on native Linux ARM64 (#8096)", () => { + // A native Linux ARM64 host reports a genuine GPU as `JMJWOA-Generic-GPU`. + // Gating the proof on Docker Desktop left no way to verify that GPU, so + // onboarding reported "no NVIDIA GPU detected" on a host where + // `docker run --gpus all` runs a CUDA workload. const runProof = vi.fn(() => passingProof); const prover = createArm64WslDockerDesktopGpuProver({ platform: "linux", arch: "arm64", - detectWslDockerDesktopStatus: () => "not-docker-desktop", + env: {}, + release: "6.17.0-1029-nvidia", + procVersion: "Linux version 6.17.0-1029-nvidia", runProof, log: () => undefined, }); - expect(prover(["JMJWOA-Generic-GPU"])).toBeNull(); - expect(runProof).not.toHaveBeenCalled(); + expect(prover(["JMJWOA-Generic-GPU"])).toEqual(passingProof); + expect(runProof).toHaveBeenCalledTimes(1); + }); + + it("keeps failing closed when the bounded proof does not pass", () => { + // The Snapdragon nvidia-smi shim reaches the same path; only the CUDA + // workload separates it from real hardware (#3988/#4565). + const failingProof = { passed: false, timedOut: false, exitCode: 1, diagnostic: "" }; + const runProof = vi.fn(() => failingProof); + const prover = createArm64WslDockerDesktopGpuProver({ + platform: "linux", + arch: "arm64", + env: {}, + release: "6.17.0-1029-nvidia", + procVersion: "Linux version 6.17.0-1029-nvidia", + runProof, + log: () => undefined, + }); + expect(prover(["JMJWOA-Generic-GPU"])).toEqual(failingProof); + expect(runProof).toHaveBeenCalledTimes(1); + }); + + it("leaves WSL hosts without Docker Desktop unproven (#8096)", () => { + // Windows-on-ARM passthrough scope is unchanged: only native Linux is added. + for (const status of ["not-docker-desktop", "unknown"] as const) { + const runProof = vi.fn(() => passingProof); + const prover = createArm64WslDockerDesktopGpuProver({ + platform: "linux", + arch: "arm64", + env: { WSL_DISTRO_NAME: "Ubuntu" }, + detectWslDockerDesktopStatus: () => status, + runProof, + log: () => undefined, + }); + expect(prover(["JMJWOA-Generic-GPU"])).toBeNull(); + expect(runProof).not.toHaveBeenCalled(); + } }); it("runs the bounded proof and reports the result on ARM64 Docker Desktop WSL", () => { diff --git a/src/lib/onboard/wsl-docker-desktop-gpu.ts b/src/lib/onboard/wsl-docker-desktop-gpu.ts index c69415cbf96..646b3742bdc 100644 --- a/src/lib/onboard/wsl-docker-desktop-gpu.ts +++ b/src/lib/onboard/wsl-docker-desktop-gpu.ts @@ -184,10 +184,10 @@ function runWslDockerDesktopGpuProof(argv: string[], timeoutMs: number): DockerG } } -// Build the ARM64 WSL Docker Desktop GPU prover consumed by `detectGpu()` for -// denylisted `JMJWOA-Generic-*` names (#4565). Returns `null` for any host that -// is not ARM64 Linux on Docker Desktop-backed WSL, so the #3988/#4424 fail- -// closed default is preserved everywhere else. When the host IS a candidate it +// Build the ARM64 GPU prover consumed by `detectGpu()` for denylisted +// `JMJWOA-Generic-*` names (#4565). Returns `null` unless the host is ARM64 +// Linux that is either native or Docker Desktop-backed WSL (#8096), so the +// #3988/#4424 fail-closed default is preserved everywhere else. When the host IS a candidate it // runs one bounded Docker `--gpus` CUDA workload (the aarch64 vectorAdd sample): // a real N1X GPU passes, while the Snapdragon nvidia-smi shim — which has no // usable CUDA device — cannot, so the placeholder name alone is never trusted. @@ -201,20 +201,26 @@ export function createArm64WslDockerDesktopGpuProver( const platform = deps.platform ?? process.platform; const arch = deps.arch ?? process.arch; if (platform !== "linux" || arch !== "arm64") return null; - if (detectStatus(deps) !== "docker-desktop") return null; + // Docker Desktop-backed WSL is not the only host that reports a denylisted + // `JMJWOA-Generic-*` name for a genuine GPU; native Linux ARM64 hosts do + // too (#8096), and previously had no way to prove one. Only that host class + // is added here: a WSL host that is not Docker Desktop-backed still returns + // `null`, so Windows-on-ARM passthrough scope is unchanged. The bounded CUDA + // proof remains the trust boundary, not the container runtime. The Snapdragon + // nvidia-smi shim exposes no usable CUDA device, so it still fails closed + // wherever the proof runs (#3988/#4565). + if (detectWsl(deps) && detectStatus(deps) !== "docker-desktop") return null; const names = gpuNames.filter(Boolean).join(", ") || "generic ARM64 GPU"; - log( - ` Running bounded Docker Desktop WSL GPU proof for ${names} (may pull a CUDA sample image)...`, - ); + log(` Running bounded Docker GPU proof for ${names} (may pull a CUDA sample image)...`); log(` ${WSL_DOCKER_DESKTOP_GPU_PROOF_COMMAND}`); const result = runProof( wslDockerDesktopGpuProofArgv(), wslDockerDesktopGpuProofTimeoutMs(deps.env), ); if (result.passed) { - log(" ✓ Docker Desktop WSL GPU proof passed; trusting the reported GPU."); + log(" ✓ Docker GPU proof passed; trusting the reported GPU."); } else if (result.timedOut) { - log(" ✗ Docker Desktop WSL GPU proof timed out; treating GPU as unproven (CPU fallback)."); + log(" ✗ Docker GPU proof timed out; treating GPU as unproven (CPU fallback)."); log( " Rerun with --no-gpu to skip GPU passthrough, or raise NEMOCLAW_WSL_GPU_PROOF_TIMEOUT_MS.", ); @@ -222,9 +228,7 @@ export function createArm64WslDockerDesktopGpuProver( // The proof binary's architecture did not match the host. This is an image // problem, not a GPU problem, so call it out explicitly rather than letting // the host fall back to CPU as if no GPU were present (#4565). - log( - " ✗ Docker Desktop WSL GPU proof could not run: CUDA sample image architecture does not", - ); + log(" ✗ Docker GPU proof could not run: CUDA sample image architecture does not"); log( " match this host (exec format error). This is a proof-image issue, not a missing GPU.", ); @@ -232,7 +236,7 @@ export function createArm64WslDockerDesktopGpuProver( " Rerun with --no-gpu to skip GPU passthrough, or report this so the proof image can be fixed.", ); } else { - log(" ✗ Docker Desktop WSL GPU proof failed; treating GPU as unproven (CPU fallback)."); + log(" ✗ Docker GPU proof failed; treating GPU as unproven (CPU fallback)."); log(" Rerun with --no-gpu to skip GPU passthrough."); } return result; From 39b5449b4fbba9e1f5e9f96a3b4d71efd0ebd5db Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 6 Aug 2026 04:15:56 -0700 Subject: [PATCH 2/9] test(onboard): prove generic ARM64 GPU failure boundary Signed-off-by: Apurv Kumaria --- docs/reference/commands.mdx | 2 +- docs/reference/troubleshooting.mdx | 2 +- src/lib/inference/nim.test.ts | 8 ++++---- src/lib/onboard/wsl-docker-desktop-gpu.test.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index cb2c64b7e1f..adc20359a5a 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -796,7 +796,7 @@ $$nemoclaw onboard --from ./Dockerfile.custom When `$$nemoclaw onboard` detects an NVIDIA GPU on the host, it enables OpenShell GPU passthrough at both the gateway and sandbox level by default. Detection proceeds along two paths. The `nvidia-smi`-based paths (the primary `--query-gpu=name,memory.total,memory.free` probe and the unified-memory `--query-gpu=name` fallback) require `nvidia-smi` to succeed and, on hosts whose firmware does not classify as a known NVIDIA platform (DGX Spark, DGX Station, Jetson, or Tegra), additionally require that the GPU name does not match the placeholder family observed on the Windows-on-ARM WSL2 nvidia-smi shim (`JMJWOA-Generic-*`) and that either the host is not ARM64 Linux (the observed shim is Windows-on-ARM only) or the NVIDIA kernel driver is bound (`/proc/driver/nvidia/` present), so that placeholder shims on non-NVIDIA hardware are not mistaken for real GPUs. -An ARM64 Linux host that is native or Docker Desktop-backed WSL clears a `JMJWOA-Generic-*` name only when one bounded `docker run --gpus all nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0` workload succeeds, which may first pull that image; every other host still fails closed. +An ARM64 Linux host that is native or Docker Desktop-backed WSL clears a `JMJWOA-Generic-*` name only when one bounded `docker run --gpus all nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0` workload succeeds, which may first pull that image; other non-firmware-vouched hosts still fail closed. Jetson/Tegra hosts that ship without `nvidia-smi` continue to be detected via the devicetree firmware fallback (`/sys/firmware/devicetree/base/model`) or the Tegra device-node fallback (`/dev/nvhost-gpu`, `/dev/nvhost-ctrl-gpu`, `/dev/nvhost-ctrl`, or `/dev/nvmap`); both bypass the trust-tier gate above. Use `--no-gpu` to opt out when you want host-side inference providers only and do not need direct GPU access inside the sandbox. Use `--gpu` to require GPU passthrough and fail fast if an NVIDIA GPU is not detected. diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index aafa9b5d9ec..d85d1f40ff2 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -805,7 +805,7 @@ The wizard cleans up stale port forwards and waits for gateway readiness automat On Windows, WSL, and native Linux ARM64 hosts, some systems report a placeholder display adapter name even when no NVIDIA GPU firmware is present. NVIDIA NIM and GPU-backed sandbox setup require a real NVIDIA GPU. -On ARM64 Linux hosts that are native or Docker Desktop-backed WSL, onboarding clears a placeholder name with one bounded Docker CUDA workload before rejecting the GPU: +On native or Docker Desktop-backed WSL ARM64 Linux hosts without firmware-confirmed NVIDIA platform metadata, onboarding clears a placeholder name with one bounded Docker CUDA workload before rejecting the GPU: ``` docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0 diff --git a/src/lib/inference/nim.test.ts b/src/lib/inference/nim.test.ts index ee2cc913786..eb6f7e5e359 100644 --- a/src/lib/inference/nim.test.ts +++ b/src/lib/inference/nim.test.ts @@ -752,10 +752,10 @@ describe("nim", () => { } }); - // Snapdragon WoA fail-closed: the same placeholder name, but the bounded - // CUDA proof fails because there is no usable NVIDIA device. The detection - // must stay null so #3988/#4424 is not reopened. - it("keeps rejecting a denylisted ARM64 GPU when the Docker GPU proof fails (#4565/#3988)", () => { + // A generic ARM64 host can be native Linux or Windows on ARM with WSL2. + // When the bounded CUDA proof fails, public detection must return null so + // the native Linux path does not reopen #3988/#4424. + it("rejects a denylisted generic ARM64 GPU when the Docker proof fails (#4565/#8096/#3988)", () => { const runCapture = vi.fn((cmd: string | string[]) => { if (!Array.isArray(cmd)) throw new Error("expected argv array"); if (cmd[0] === "nvidia-smi" && cmd.some((a: string) => a.includes("name,memory.total"))) { diff --git a/src/lib/onboard/wsl-docker-desktop-gpu.test.ts b/src/lib/onboard/wsl-docker-desktop-gpu.test.ts index db0d540f89d..2d04f2fdb17 100644 --- a/src/lib/onboard/wsl-docker-desktop-gpu.test.ts +++ b/src/lib/onboard/wsl-docker-desktop-gpu.test.ts @@ -104,7 +104,7 @@ describe("createArm64WslDockerDesktopGpuProver (#4565)", () => { expect(runProof).toHaveBeenCalledTimes(1); }); - it("keeps failing closed when the bounded proof does not pass", () => { + it("returns the failed bounded proof result on native Linux ARM64", () => { // The Snapdragon nvidia-smi shim reaches the same path; only the CUDA // workload separates it from real hardware (#3988/#4565). const failingProof = { passed: false, timedOut: false, exitCode: 1, diagnostic: "" }; @@ -164,7 +164,7 @@ describe("createArm64WslDockerDesktopGpuProver (#4565)", () => { expect(WSL_DOCKER_DESKTOP_GPU_PROOF_COMMAND).not.toContain("nbody"); }); - it("propagates a failing proof so detection stays fail-closed", () => { + it("returns the failed bounded proof result on Docker Desktop WSL", () => { const failing = { passed: false, timedOut: false, exitCode: 1, diagnostic: "no CUDA device" }; const prover = createArm64WslDockerDesktopGpuProver({ platform: "linux", From 03908063ebaa5e20c3baab08064b6f273f1d4bac Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 6 Aug 2026 04:24:07 -0700 Subject: [PATCH 3/9] docs(onboard): scope ARM64 GPU proof guidance Signed-off-by: Apurv Kumaria --- docs/reference/commands.mdx | 5 ++++- docs/reference/troubleshooting.mdx | 8 +++++--- src/lib/inference/nim.ts | 15 +++++++-------- src/lib/onboard/wsl-docker-desktop-gpu.ts | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index 55ab6c31fd9..d05434f1811 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -874,7 +874,10 @@ $$nemoclaw onboard --from ./Dockerfile.custom When `$$nemoclaw onboard` detects an NVIDIA GPU on the host, it enables OpenShell GPU passthrough at both the gateway and sandbox level by default. Detection proceeds along two paths. The `nvidia-smi`-based paths (the primary `--query-gpu=name,memory.total,memory.free` probe and the unified-memory `--query-gpu=name` fallback) require `nvidia-smi` to succeed and, on hosts whose firmware does not classify as a known NVIDIA platform (DGX Spark, DGX Station, Jetson, or Tegra), additionally require that the GPU name does not match the placeholder family observed on the Windows-on-ARM WSL2 nvidia-smi shim (`JMJWOA-Generic-*`) and that either the host is not ARM64 Linux (the observed shim is Windows-on-ARM only) or the NVIDIA kernel driver is bound (`/proc/driver/nvidia/` present), so that placeholder shims on non-NVIDIA hardware are not mistaken for real GPUs. -An ARM64 Linux host that is native or Docker Desktop-backed WSL clears a `JMJWOA-Generic-*` name only when one bounded `docker run --gpus all nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0` workload succeeds, which may first pull that image; other non-firmware-vouched hosts still fail closed. +On a native or Docker Desktop-backed WSL ARM64 Linux host without firmware-confirmed NVIDIA platform metadata, the primary memory-query probe can clear a `JMJWOA-Generic-*` name only after a bounded CUDA workload succeeds. +The workload runs `docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0` and may first pull the image. +The names-only unified-memory fallback does not run this workload and rejects denylisted names. +Other non-firmware-vouched hosts also reject denylisted names. Jetson/Tegra hosts that ship without `nvidia-smi` continue to be detected via the devicetree firmware fallback (`/sys/firmware/devicetree/base/model`) or the Tegra device-node fallback (`/dev/nvhost-gpu`, `/dev/nvhost-ctrl-gpu`, `/dev/nvhost-ctrl`, or `/dev/nvmap`); both bypass the trust-tier gate above. Use `--no-gpu` to opt out when you want host-side inference providers only and do not need direct GPU access inside the sandbox. Use `--gpu` to require GPU passthrough and fail fast if an NVIDIA GPU is not detected. diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index d85d1f40ff2..2cb3ed2eaa2 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -805,15 +805,17 @@ The wizard cleans up stale port forwards and waits for gateway readiness automat On Windows, WSL, and native Linux ARM64 hosts, some systems report a placeholder display adapter name even when no NVIDIA GPU firmware is present. NVIDIA NIM and GPU-backed sandbox setup require a real NVIDIA GPU. -On native or Docker Desktop-backed WSL ARM64 Linux hosts without firmware-confirmed NVIDIA platform metadata, onboarding clears a placeholder name with one bounded Docker CUDA workload before rejecting the GPU: +When the primary memory-query probe reports a placeholder name on a native or Docker Desktop-backed WSL ARM64 Linux host without firmware-confirmed NVIDIA platform metadata, onboarding runs one bounded Docker CUDA workload: -``` +```bash docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0 ``` The run may first pull the CUDA sample image and is bounded to 3 minutes. Set `NEMOCLAW_WSL_GPU_PROOF_TIMEOUT_MS` to raise that bound on slow links. -A host whose GPU passes the workload keeps GPU passthrough; a host that fails or times out falls back to CPU. +A host whose GPU passes the workload keeps GPU passthrough. +A failed or timed-out workload does not enable GPU passthrough. +The names-only unified-memory fallback does not run this workload and rejects denylisted names. WSL hosts that are not Docker Desktop-backed do not run the workload and continue to report the GPU as unavailable. If NemoClaw rejects the detected GPU name during preflight, select a CPU or remote inference provider, or move the setup to a host with a supported NVIDIA GPU and current drivers. diff --git a/src/lib/inference/nim.ts b/src/lib/inference/nim.ts index 07b77e97775..05a34bcc923 100644 --- a/src/lib/inference/nim.ts +++ b/src/lib/inference/nim.ts @@ -95,17 +95,16 @@ export interface GpuDetection { } export interface DetectGpuDeps { - // Optional accept-path for ARM64 WSL Docker Desktop `JMJWOA-Generic-*` GPUs - // (#4565). Injected in tests; in production `detectGpu()` lazily builds the - // default prover from the onboard WSL Docker Desktop module only when it is - // about to reject a denylisted ARM64 name. + // Optional accept-path for native or Docker Desktop-backed WSL ARM64 Linux + // hosts that report a `JMJWOA-Generic-*` GPU (#4565/#8096). Injected in tests; + // in production `detectGpu()` lazily builds the default prover only when it + // is about to reject a denylisted ARM64 name. proveArm64WslDockerDesktopGpu?: Arm64WslDockerDesktopGpuProver | null; } -// Lazily construct the default ARM64 WSL Docker Desktop GPU prover. Kept lazy -// (and behind a require) so the inference layer does not statically depend on -// the onboard layer, and so the bounded Docker proof is only wired when we -// actually reach the denylist-reject path on an ARM64 host. +// Lazily construct the default ARM64 Linux GPU prover. Keep it behind a require +// so the inference layer does not statically depend on the onboard layer. The +// bounded Docker proof is wired only at the denylist-reject path. function defaultArm64WslDockerDesktopGpuProver(): Arm64WslDockerDesktopGpuProver | null { try { return require("../onboard/wsl-docker-desktop-gpu").createArm64WslDockerDesktopGpuProver(); diff --git a/src/lib/onboard/wsl-docker-desktop-gpu.ts b/src/lib/onboard/wsl-docker-desktop-gpu.ts index 646b3742bdc..90155ef6c9c 100644 --- a/src/lib/onboard/wsl-docker-desktop-gpu.ts +++ b/src/lib/onboard/wsl-docker-desktop-gpu.ts @@ -23,7 +23,7 @@ export const WSL_DOCKER_DESKTOP_GPU_PROOF_COMMAND = // so it is bounded generously (3 min) rather than with the 30s detection // timeout. Operators on slow links can override via // NEMOCLAW_WSL_GPU_PROOF_TIMEOUT_MS. The timeout is the safety bound that keeps -// onboarding from hanging if Docker Desktop GPU passthrough stalls. +// onboarding from hanging if Docker GPU passthrough stalls. const WSL_DOCKER_DESKTOP_GPU_PROOF_DEFAULT_TIMEOUT_MS = 180_000; export function wslDockerDesktopGpuProofTimeoutMs(env: NodeJS.ProcessEnv = process.env): number { From 083136537e4fd50169363b24a28f7813a6a33f43 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 6 Aug 2026 04:27:02 -0700 Subject: [PATCH 4/9] docs(onboard): use ARM64 CUDA proof command Signed-off-by: Apurv Kumaria --- docs/reference/troubleshooting.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index 2cb3ed2eaa2..50432dc70e3 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -2687,7 +2687,7 @@ nvidia-ctk cdi list On WSL with Docker Desktop, confirm Docker Desktop WSL integration is enabled for your distro and verify Docker GPU access from WSL: ```bash -docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample:nbody nbody -gpu -benchmark +docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0 ``` If GPU passthrough is not required on this host, rerun onboarding with `--no-gpu` instead. From 16192638a80ea0c698f33fef0b2ec94a485d538a Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 6 Aug 2026 07:41:08 -0700 Subject: [PATCH 5/9] docs(onboard): clarify ARM64 GPU proof effects Signed-off-by: Apurv Kumaria --- docs/reference/commands.mdx | 3 ++- docs/reference/troubleshooting.mdx | 13 ++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index ffcb3954df3..2c4cb8a4895 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -875,7 +875,8 @@ $$nemoclaw onboard --from ./Dockerfile.custom When `$$nemoclaw onboard` detects an NVIDIA GPU on the host, it enables OpenShell GPU passthrough at both the gateway and sandbox level by default. Detection proceeds along two paths. The `nvidia-smi`-based paths (the primary `--query-gpu=name,memory.total,memory.free` probe and the unified-memory `--query-gpu=name` fallback) require `nvidia-smi` to succeed and, on hosts whose firmware does not classify as a known NVIDIA platform (DGX Spark, DGX Station, Jetson, or Tegra), additionally require that the GPU name does not match the placeholder family observed on the Windows-on-ARM WSL2 nvidia-smi shim (`JMJWOA-Generic-*`) and that either the host is not ARM64 Linux (the observed shim is Windows-on-ARM only) or the NVIDIA kernel driver is bound (`/proc/driver/nvidia/` present), so that placeholder shims on non-NVIDIA hardware are not mistaken for real GPUs. On a native or Docker Desktop-backed WSL ARM64 Linux host without firmware-confirmed NVIDIA platform metadata, the primary memory-query probe can clear a `JMJWOA-Generic-*` name only after a bounded CUDA workload succeeds. -The workload runs `docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0` and may first pull the image. +The workload runs `docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0`. +Docker may pull the CUDA sample image from `nvcr.io` and keeps the image in the local cache after the container exits. The names-only unified-memory fallback does not run this workload and rejects denylisted names. Other non-firmware-vouched hosts also reject denylisted names. Jetson/Tegra hosts that ship without `nvidia-smi` continue to be detected via the devicetree firmware fallback (`/sys/firmware/devicetree/base/model`) or the Tegra device-node fallback (`/dev/nvhost-gpu`, `/dev/nvhost-ctrl-gpu`, `/dev/nvhost-ctrl`, or `/dev/nvmap`); both bypass the trust-tier gate above. diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index 50432dc70e3..93606d56910 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -805,16 +805,19 @@ The wizard cleans up stale port forwards and waits for gateway readiness automat On Windows, WSL, and native Linux ARM64 hosts, some systems report a placeholder display adapter name even when no NVIDIA GPU firmware is present. NVIDIA NIM and GPU-backed sandbox setup require a real NVIDIA GPU. -When the primary memory-query probe reports a placeholder name on a native or Docker Desktop-backed WSL ARM64 Linux host without firmware-confirmed NVIDIA platform metadata, onboarding runs one bounded Docker CUDA workload: +When the primary memory-query probe reports a placeholder name on a native or Docker Desktop-backed WSL ARM64 Linux host without firmware-confirmed NVIDIA platform metadata, onboarding runs one bounded Docker CUDA workload. +Docker may pull the CUDA sample image from `nvcr.io` and keeps the image in the local cache after the container exits. +The workload uses this command: ```bash docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0 ``` -The run may first pull the CUDA sample image and is bounded to 3 minutes. -Set `NEMOCLAW_WSL_GPU_PROOF_TIMEOUT_MS` to raise that bound on slow links. -A host whose GPU passes the workload keeps GPU passthrough. -A failed or timed-out workload does not enable GPU passthrough. +The run is bounded to 3 minutes. +Set `NEMOCLAW_WSL_GPU_PROOF_TIMEOUT_MS` to a positive millisecond value to change that bound. +Increase the value on slow links. +A passing workload qualifies the detected GPU for GPU passthrough. +A failed or timed-out workload leaves the GPU unproven and does not enable GPU passthrough. The names-only unified-memory fallback does not run this workload and rejects denylisted names. WSL hosts that are not Docker Desktop-backed do not run the workload and continue to report the GPU as unavailable. From 4c6944a6890b60cefd494b2a6f77b19f41567f3e Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 6 Aug 2026 14:22:30 -0700 Subject: [PATCH 6/9] fix(onboard): trust immutable GPU proof image Signed-off-by: Apurv Kumaria --- docs/reference/commands.mdx | 2 +- docs/reference/troubleshooting.mdx | 4 ++-- src/lib/onboard/wsl-docker-desktop-gpu.test.ts | 7 +++++-- src/lib/onboard/wsl-docker-desktop-gpu.ts | 14 ++++++++------ 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index 2c4cb8a4895..03106ab41d3 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -875,7 +875,7 @@ $$nemoclaw onboard --from ./Dockerfile.custom When `$$nemoclaw onboard` detects an NVIDIA GPU on the host, it enables OpenShell GPU passthrough at both the gateway and sandbox level by default. Detection proceeds along two paths. The `nvidia-smi`-based paths (the primary `--query-gpu=name,memory.total,memory.free` probe and the unified-memory `--query-gpu=name` fallback) require `nvidia-smi` to succeed and, on hosts whose firmware does not classify as a known NVIDIA platform (DGX Spark, DGX Station, Jetson, or Tegra), additionally require that the GPU name does not match the placeholder family observed on the Windows-on-ARM WSL2 nvidia-smi shim (`JMJWOA-Generic-*`) and that either the host is not ARM64 Linux (the observed shim is Windows-on-ARM only) or the NVIDIA kernel driver is bound (`/proc/driver/nvidia/` present), so that placeholder shims on non-NVIDIA hardware are not mistaken for real GPUs. On a native or Docker Desktop-backed WSL ARM64 Linux host without firmware-confirmed NVIDIA platform metadata, the primary memory-query probe can clear a `JMJWOA-Generic-*` name only after a bounded CUDA workload succeeds. -The workload runs `docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0`. +The workload runs `docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample@sha256:7c7540bdf1f942d4fb6db97069fd6c289471b54ac29e3c7fcdf914cf77af7d41`. Docker may pull the CUDA sample image from `nvcr.io` and keeps the image in the local cache after the container exits. The names-only unified-memory fallback does not run this workload and rejects denylisted names. Other non-firmware-vouched hosts also reject denylisted names. diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index 93606d56910..7cbd788414c 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -810,7 +810,7 @@ Docker may pull the CUDA sample image from `nvcr.io` and keeps the image in the The workload uses this command: ```bash -docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0 +docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample@sha256:7c7540bdf1f942d4fb6db97069fd6c289471b54ac29e3c7fcdf914cf77af7d41 ``` The run is bounded to 3 minutes. @@ -2690,7 +2690,7 @@ nvidia-ctk cdi list On WSL with Docker Desktop, confirm Docker Desktop WSL integration is enabled for your distro and verify Docker GPU access from WSL: ```bash -docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0 +docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample@sha256:7c7540bdf1f942d4fb6db97069fd6c289471b54ac29e3c7fcdf914cf77af7d41 ``` If GPU passthrough is not required on this host, rerun onboarding with `--no-gpu` instead. diff --git a/src/lib/onboard/wsl-docker-desktop-gpu.test.ts b/src/lib/onboard/wsl-docker-desktop-gpu.test.ts index 2d04f2fdb17..48a0743294d 100644 --- a/src/lib/onboard/wsl-docker-desktop-gpu.test.ts +++ b/src/lib/onboard/wsl-docker-desktop-gpu.test.ts @@ -155,12 +155,15 @@ describe("createArm64WslDockerDesktopGpuProver (#4565)", () => { expect(argv).toContain("--gpus"); }); - it("uses an arch-correct CUDA sample image (not the amd64-only nbody) on this ARM64 path", () => { + it("uses the approved immutable multi-architecture CUDA sample image on this ARM64 path", () => { // The proof only runs on ARM64, so the image must ship a real aarch64 CUDA // binary. `cuda-sample:nbody` packs an x86-64 binary in its arm64 tag and // fails with `exec format error` on the N1X target (#4565); the chosen // vectorAdd image ships a genuine aarch64 binary. - expect(WSL_DOCKER_DESKTOP_GPU_PROOF_COMMAND).toContain("cuda-sample:vectoradd"); + expect(WSL_DOCKER_DESKTOP_GPU_PROOF_COMMAND).toBe( + "docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample@sha256:7c7540bdf1f942d4fb6db97069fd6c289471b54ac29e3c7fcdf914cf77af7d41", + ); + expect(WSL_DOCKER_DESKTOP_GPU_PROOF_COMMAND).not.toContain("vectoradd-cuda12.5.0"); expect(WSL_DOCKER_DESKTOP_GPU_PROOF_COMMAND).not.toContain("nbody"); }); diff --git a/src/lib/onboard/wsl-docker-desktop-gpu.ts b/src/lib/onboard/wsl-docker-desktop-gpu.ts index 90155ef6c9c..0bebd5c96c4 100644 --- a/src/lib/onboard/wsl-docker-desktop-gpu.ts +++ b/src/lib/onboard/wsl-docker-desktop-gpu.ts @@ -11,13 +11,15 @@ const WSL_DOCKER_DESKTOP_DETECTION_TIMEOUT_MS = 30_000; // so the proof image MUST ship a real aarch64 CUDA binary. The older // `cuda-sample:nbody` image is unusable here: its arm64 manifest entry actually // contains an x86-64 ELF, so on the N1X Windows-ARM target it fails with -// `exec /cuda-samples/sample: exec format error` (#4565). `vectoradd-cuda12.5.0` -// ships a genuine aarch64 binary and runs a real CUDA kernel (device alloc + -// add + result verification), which is a strong usability proof that still -// fails closed on the Snapdragon nvidia-smi shim (no usable CUDA device, #3988). -// The image's entrypoint runs vectorAdd directly, so no trailing args are needed. +// `exec /cuda-samples/sample: exec format error` (#4565). The immutable +// vectorAdd manifest below contains both linux/amd64 and linux/arm64 images; its +// ARM64 image ships a genuine aarch64 binary and runs a real CUDA kernel (device +// alloc + add + result verification), which is a strong usability proof that +// still fails closed on the Snapdragon nvidia-smi shim (no usable CUDA device, +// #3988). The image's entrypoint runs vectorAdd directly, so no trailing args +// are needed. export const WSL_DOCKER_DESKTOP_GPU_PROOF_COMMAND = - "docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0"; + "docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample@sha256:7c7540bdf1f942d4fb6db97069fd6c289471b54ac29e3c7fcdf914cf77af7d41"; // The proof runs a real CUDA workload and may first pull the CUDA sample image, // so it is bounded generously (3 min) rather than with the 30s detection From b547507d893ac6bb6278da73cbf5de5dab5f123c Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 6 Aug 2026 23:16:36 -0700 Subject: [PATCH 7/9] fix(docs): refresh ARM64 support citation Signed-off-by: Apurv Kumaria --- ci/platform-matrix.json | 2 +- docs/reference/platform-support.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/platform-matrix.json b/ci/platform-matrix.json index 271b17923f4..e27672ff29e 100644 --- a/ci/platform-matrix.json +++ b/ci/platform-matrix.json @@ -266,7 +266,7 @@ { "name": "Windows-on-ARM GPU passthrough", "status": "unsupported", - "notes": "Windows-on-ARM CPU paths run under WSL2 'tested with limitations', but GPU passthrough on WOA is denylisted (`src/lib/onboard/wsl-docker-desktop-gpu.ts:188`, `src/lib/inference/gpu-trust.test.ts:70`). See closed issue #4565." + "notes": "Windows-on-ARM CPU paths run under WSL2 'tested with limitations', but GPU passthrough on WOA is denylisted (`src/lib/onboard/wsl-docker-desktop-gpu.ts:214`, `src/lib/inference/gpu-trust.test.ts:70`). See closed issue #4565." }, { "name": "Non-NVIDIA GPUs (AMD/ROCm, Intel Arc, Apple Metal)", diff --git a/docs/reference/platform-support.mdx b/docs/reference/platform-support.mdx index 502553460f7..b090fc7eba2 100644 --- a/docs/reference/platform-support.mdx +++ b/docs/reference/platform-support.mdx @@ -169,7 +169,7 @@ They are listed here so launch material, sales conversations, and support triage | Non-Ubuntu/Debian Linux distros | Unsupported | Installer assumes `apt-get`. Fedora/Rocky/Alma/Arch/NixOS are not validated and the installer's package-manager probes do not cover them. See open issue #899 (Fedora hang). | | Native Kubernetes or OpenShift deployments | Unsupported | NemoClaw runs the sandbox as a Docker container, not a Kubernetes pod. The default Docker-driver topology does not embed k3s. Operator-managed K8s/OpenShift deployments are out of scope; see issue #407 (community OpenShift through agent-sandbox CRD). | | Air-gapped / offline installs | Unsupported | Onboard assumes network reachability for package fetches, container pulls, and provider validation. See open issues #4872 and #2218 (production-deployment epic covering air-gapped support, China network guidance, multi-host topology). | -| Windows-on-ARM GPU passthrough | Unsupported | Windows-on-ARM CPU paths run under WSL2 'tested with limitations', but GPU passthrough on WOA is denylisted (`src/lib/onboard/wsl-docker-desktop-gpu.ts:188`, `src/lib/inference/gpu-trust.test.ts:70`). See closed issue #4565. | +| Windows-on-ARM GPU passthrough | Unsupported | Windows-on-ARM CPU paths run under WSL2 'tested with limitations', but GPU passthrough on WOA is denylisted (`src/lib/onboard/wsl-docker-desktop-gpu.ts:214`, `src/lib/inference/gpu-trust.test.ts:70`). See closed issue #4565. | | Non-NVIDIA GPUs (AMD/ROCm, Intel Arc, Apple Metal) | Unsupported | Local vLLM and NIM paths assert NVIDIA CDI presence with `assertCdiNvidiaGpuSpecPresent` (`src/lib/onboard/fatal-runtime-preflight.ts`). NemoClaw does not install non-NVIDIA accelerator drivers. | | Other LangChain, AutoGen, CrewAI, or non-listed agent harnesses | Unsupported | LangChain Deep Agents Code is the only integrated LangChain-family agent runtime (refer to the Agents section above; status `Tested`). Other LangChain agent runtimes, AutoGen, CrewAI, and any agent runtime not listed in the Agents table are not integrated. Bringing more agent runtimes is tracked as a research epic (refer to open issue #4861) but is not on the current roadmap. | | Multi-user host sharing | Unsupported | Sandboxes are scoped to a single host user. NemoClaw treats multi-user hosts as a risk and warns at onboard; see `docs/security/openclaw-controls.mdx` Multi-user detection. | From 03e3f1872745860826be27de43e335e5aff79455 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Fri, 7 Aug 2026 08:29:45 -0400 Subject: [PATCH 8/9] fix(onboard): harden ARM64 GPU proof inputs Signed-off-by: Julie Yaunches --- docs/reference/commands.mdx | 2 ++ docs/reference/troubleshooting.mdx | 2 ++ src/lib/inference/nim.test.ts | 18 +++++++++--------- src/lib/inference/nim.ts | 14 +++++++------- src/lib/onboard/wsl-docker-desktop-gpu.test.ts | 17 +++++++++++++++++ src/lib/onboard/wsl-docker-desktop-gpu.ts | 16 +++++++++++++++- 6 files changed, 52 insertions(+), 17 deletions(-) diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index f15eab5eac5..e3352911d77 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -887,6 +887,8 @@ $$nemoclaw onboard --from ./Dockerfile.custom When `$$nemoclaw onboard` detects an NVIDIA GPU on the host, it enables OpenShell GPU passthrough at both the gateway and sandbox level by default. Detection proceeds along two paths. The `nvidia-smi`-based paths (the primary `--query-gpu=name,memory.total,memory.free` probe and the unified-memory `--query-gpu=name` fallback) require `nvidia-smi` to succeed and, on hosts whose firmware does not classify as a known NVIDIA platform (DGX Spark, DGX Station, Jetson, or Tegra), additionally require that the GPU name does not match the placeholder family observed on the Windows-on-ARM WSL2 nvidia-smi shim (`JMJWOA-Generic-*`) and that either the host is not ARM64 Linux (the observed shim is Windows-on-ARM only) or the NVIDIA kernel driver is bound (`/proc/driver/nvidia/` present), so that placeholder shims on non-NVIDIA hardware are not mistaken for real GPUs. +NemoClaw treats a recognized NVIDIA product model from `/sys/class/dmi/id/product_name` or `/sys/firmware/devicetree/base/model`, or a known Tegra device node, as authoritative platform identity. +Remove the native ARM64 proof exception when every supported host that uses it exposes a stable value through one of these signals and `detectNvidiaPlatform()` recognizes that value. On a native or Docker Desktop-backed WSL ARM64 Linux host without firmware-confirmed NVIDIA platform metadata, the primary memory-query probe can clear a `JMJWOA-Generic-*` name only after a bounded CUDA workload succeeds. The workload runs `docker run --rm --gpus all nvcr.io/nvidia/k8s/cuda-sample@sha256:7c7540bdf1f942d4fb6db97069fd6c289471b54ac29e3c7fcdf914cf77af7d41`. Docker may pull the CUDA sample image from `nvcr.io` and keeps the image in the local cache after the container exits. diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index 3fc7db70ae1..cb54077961f 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -835,6 +835,8 @@ On Windows, WSL, and native Linux ARM64 hosts, some systems report a placeholder NVIDIA NIM and GPU-backed sandbox setup require a real NVIDIA GPU. When the primary memory-query probe reports a placeholder name on a native or Docker Desktop-backed WSL ARM64 Linux host without firmware-confirmed NVIDIA platform metadata, onboarding runs one bounded Docker CUDA workload. +NemoClaw treats a recognized NVIDIA product model from `/sys/class/dmi/id/product_name` or `/sys/firmware/devicetree/base/model`, or a known Tegra device node, as authoritative platform identity. +Remove the native ARM64 proof exception when every supported host that uses it exposes a stable value through one of these signals and `detectNvidiaPlatform()` recognizes that value. Docker may pull the CUDA sample image from `nvcr.io` and keeps the image in the local cache after the container exits. The workload uses this command: diff --git a/src/lib/inference/nim.test.ts b/src/lib/inference/nim.test.ts index eb6f7e5e359..4704946af25 100644 --- a/src/lib/inference/nim.test.ts +++ b/src/lib/inference/nim.test.ts @@ -434,6 +434,13 @@ describe("nim", () => { }); describe("detectGpu", () => { + const proveArm64WslDockerDesktopGpu = vi.fn(() => ({ + passed: true, + timedOut: false, + exitCode: 0, + diagnostic: "", + })); + function withGenericLinuxFirmware(fn: () => void): void { const fs = require("fs"); const origReadFileSync = fs.readFileSync; @@ -684,7 +691,7 @@ describe("nim", () => { try { withFirmwareModel("Microsoft Corporation Virtual Machine", () => { - expect(nimModule.detectGpu()).toBeNull(); + expect(nimModule.detectGpu({ proveArm64WslDockerDesktopGpu: null })).toBeNull(); }); } finally { restore(); @@ -707,7 +714,7 @@ describe("nim", () => { try { withFirmwareModel("Microsoft Corporation Virtual Machine", () => { - expect(nimModule.detectGpu()).toBeNull(); + expect(nimModule.detectGpu({ proveArm64WslDockerDesktopGpu })).toBeNull(); }); } finally { restore(); @@ -728,13 +735,6 @@ describe("nim", () => { return ""; }); const { nimModule, restore } = loadNimWithMockedRunner(runCapture); - const proveArm64WslDockerDesktopGpu = vi.fn(() => ({ - passed: true, - timedOut: false, - exitCode: 0, - diagnostic: "", - })); - try { withFirmwareModel("Microsoft Corporation Virtual Machine", () => { const result = nimModule.detectGpu({ proveArm64WslDockerDesktopGpu }); diff --git a/src/lib/inference/nim.ts b/src/lib/inference/nim.ts index 69513262432..965e54d02e5 100644 --- a/src/lib/inference/nim.ts +++ b/src/lib/inference/nim.ts @@ -459,6 +459,12 @@ export function detectGpu(deps: DetectGpuDeps = {}): GpuDetection | null { if (firmwareConfirmsNvidia) { trusted = parsed; } else if (parsed.some((p: ParsedGpu) => isDenylistedNvidiaGpuName(p.name))) { + // The all-GPU CUDA workload proves that at least one usable device + // exists. It does not establish which nvidia-smi rows or capacities + // are genuine, so a denylisted multi-row response stays untrusted. + if (parsed.length !== 1) { + return null; + } // A denylisted `JMJWOA-Generic-*` placeholder. Both real Windows-ARM // N1X (WSL2 + Docker Desktop) and the Snapdragon nvidia-smi shim emit // this name, so the name and `/proc/driver/nvidia` are insufficient. @@ -473,13 +479,7 @@ export function detectGpu(deps: DetectGpuDeps = {}): GpuDetection | null { if (!proof || !proof.passed) { return null; } - // The proof confirms a usable GPU, but it does not vouch for every - // row. Keep only the placeholder rows it covers plus any plausibly- - // named NVIDIA rows; drop unrecognized garbage so a mixed-row spoof - // cannot inflate totalMemoryMB with a phantom device. - trusted = parsed.filter( - (p: ParsedGpu) => isDenylistedNvidiaGpuName(p.name) || isPlausibleNvidiaGpuName(p.name), - ); + trusted = parsed; wslDockerDesktopGpuProofPassed = true; } else { if (!nvidiaHostLooksGenuine()) { diff --git a/src/lib/onboard/wsl-docker-desktop-gpu.test.ts b/src/lib/onboard/wsl-docker-desktop-gpu.test.ts index 48a0743294d..3a2b0098bc2 100644 --- a/src/lib/onboard/wsl-docker-desktop-gpu.test.ts +++ b/src/lib/onboard/wsl-docker-desktop-gpu.test.ts @@ -155,6 +155,23 @@ describe("createArm64WslDockerDesktopGpuProver (#4565)", () => { expect(argv).toContain("--gpus"); }); + it("escapes terminal controls in a denylisted GPU name before logging", () => { + const logs: string[] = []; + const prover = createArm64WslDockerDesktopGpuProver({ + platform: "linux", + arch: "arm64", + env: {}, + release: "6.17.0-1029-nvidia", + procVersion: "Linux version 6.17.0-1029-nvidia", + runProof: () => passingProof, + log: (message) => logs.push(message), + }); + + expect(prover(["JMJWOA-Generic-\u001b[2J\nforged status"])).toEqual(passingProof); + expect(logs[0]).toContain("JMJWOA-Generic-\\u{001b}[2J\\u{000a}forged status"); + expect(logs.every((message) => !/[\u0000-\u001f\u007f-\u009f]/u.test(message))).toBe(true); + }); + it("uses the approved immutable multi-architecture CUDA sample image on this ARM64 path", () => { // The proof only runs on ARM64, so the image must ship a real aarch64 CUDA // binary. `cuda-sample:nbody` packs an x86-64 binary in its arm64 tag and diff --git a/src/lib/onboard/wsl-docker-desktop-gpu.ts b/src/lib/onboard/wsl-docker-desktop-gpu.ts index 0bebd5c96c4..e215c881f5f 100644 --- a/src/lib/onboard/wsl-docker-desktop-gpu.ts +++ b/src/lib/onboard/wsl-docker-desktop-gpu.ts @@ -212,7 +212,8 @@ export function createArm64WslDockerDesktopGpuProver( // nvidia-smi shim exposes no usable CUDA device, so it still fails closed // wherever the proof runs (#3988/#4565). if (detectWsl(deps) && detectStatus(deps) !== "docker-desktop") return null; - const names = gpuNames.filter(Boolean).join(", ") || "generic ARM64 GPU"; + const names = + gpuNames.filter(Boolean).map(escapeGpuNameForTerminal).join(", ") || "generic ARM64 GPU"; log(` Running bounded Docker GPU proof for ${names} (may pull a CUDA sample image)...`); log(` ${WSL_DOCKER_DESKTOP_GPU_PROOF_COMMAND}`); const result = runProof( @@ -245,6 +246,19 @@ export function createArm64WslDockerDesktopGpuProver( }; } +function escapeGpuNameForTerminal(value: string): string { + return [...value] + .map((character) => { + const codePoint = character.codePointAt(0) ?? 0; + const isC0 = codePoint <= 0x1f; + const isDeleteOrC1 = codePoint >= 0x7f && codePoint <= 0x9f; + const isLineSeparator = codePoint === 0x2028 || codePoint === 0x2029; + const isFormatControl = /^\p{Cf}$/u.test(character); + if (!isC0 && !isDeleteOrC1 && !isLineSeparator && !isFormatControl) return character; + return "\\u{" + codePoint.toString(16).padStart(4, "0") + "}"; + }) + .join(""); +} export function wslDockerDesktopGpuCompatibilityAction(): WslDockerDesktopGpuCompatibilityAction { return { id: "wsl_docker_desktop_gpu_compatibility", From 456359a23e24a55b79868576c516bde4c09d01a9 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Fri, 7 Aug 2026 08:40:41 -0400 Subject: [PATCH 9/9] fix(onboard): escape GPU names at preflight Signed-off-by: Julie Yaunches --- src/lib/inference/gpu-trust.ts | 14 ++++++++++++++ src/lib/inference/nim.test.ts | 18 +++++++++--------- src/lib/inference/nim.ts | 12 +++++++----- src/lib/onboard/wsl-docker-desktop-gpu.ts | 19 +++++-------------- 4 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/lib/inference/gpu-trust.ts b/src/lib/inference/gpu-trust.ts index 20484399fe2..1df768f0019 100644 --- a/src/lib/inference/gpu-trust.ts +++ b/src/lib/inference/gpu-trust.ts @@ -24,6 +24,20 @@ const NVIDIA_DRIVER_PROC_PATH = "/proc/driver/nvidia"; export function isDenylistedNvidiaGpuName(name: string): boolean { return NVIDIA_GPU_NAME_DENYLIST_PATTERN.test(name); } +/** Escape untrusted GPU names without allowing terminal-control sequences. */ +export function escapeGpuNameForTerminal(value: string): string { + return [...value] + .map((character) => { + const codePoint = character.codePointAt(0) ?? 0; + const isC0 = codePoint <= 0x1f; + const isDeleteOrC1 = codePoint >= 0x7f && codePoint <= 0x9f; + const isLineSeparator = codePoint === 0x2028 || codePoint === 0x2029; + const isFormatControl = /^\p{Cf}$/u.test(character); + if (!isC0 && !isDeleteOrC1 && !isLineSeparator && !isFormatControl) return character; + return "\\u{" + codePoint.toString(16).padStart(4, "0") + "}"; + }) + .join(""); +} // Result of a bounded Docker `--gpus` CUDA proof. `passed` is true only when a // real CUDA workload (not just nvidia-smi) succeeded — that is the signal that diff --git a/src/lib/inference/nim.test.ts b/src/lib/inference/nim.test.ts index 4704946af25..c239278489f 100644 --- a/src/lib/inference/nim.test.ts +++ b/src/lib/inference/nim.test.ts @@ -721,16 +721,13 @@ describe("nim", () => { } }); - // #4565: a real Windows-ARM N1X + WSL2 + Docker Desktop host reports the - // same `JMJWOA-Generic-*` placeholder as the Snapdragon shim, but it can - // pass a bounded Docker `--gpus` CUDA proof. When the injected prover - // confirms the proof, the denylisted name is accepted and the detection is - // tagged so the sandbox preflight reaches the Docker Desktop WSL branch. - it("accepts a denylisted ARM64 GPU when the bounded Docker GPU proof passes (#4565)", () => { + // A passing proof accepts the raw name, but terminal formatting must escape it. + it("escapes terminal controls after a denylisted ARM64 GPU proof passes (#4565)", () => { + const gpuName = "JMJWOA-Generic-\u001b\u0085\u200d\u2028\u2029GPU"; const runCapture = vi.fn((cmd: string | string[]) => { if (!Array.isArray(cmd)) throw new Error("expected argv array"); if (cmd[0] === "nvidia-smi" && cmd.some((a: string) => a.includes("name,memory.total"))) { - return "JMJWOA-Generic-GPU, 65471, 65000\n"; + return `${gpuName}, 65471, 65000\n`; } return ""; }); @@ -740,12 +737,15 @@ describe("nim", () => { const result = nimModule.detectGpu({ proveArm64WslDockerDesktopGpu }); expect(result).toMatchObject({ type: "nvidia", - name: "JMJWOA-Generic-GPU", + name: gpuName, count: 1, totalMemoryMB: 65471, wslDockerDesktopGpuProofPassed: true, }); - expect(proveArm64WslDockerDesktopGpu).toHaveBeenCalledWith(["JMJWOA-Generic-GPU"]); + expect(proveArm64WslDockerDesktopGpu).toHaveBeenCalledWith([gpuName]); + expect(nimModule.formatNvidiaGpuPreflightLines(result)).toEqual([ + "NVIDIA GPU detected (JMJWOA-Generic-\\u{001b}\\u{0085}\\u{200d}\\u{2028}\\u{2029}GPU, 65471 MB)", + ]); }); } finally { restore(); diff --git a/src/lib/inference/nim.ts b/src/lib/inference/nim.ts index 965e54d02e5..0b055dc9aa7 100644 --- a/src/lib/inference/nim.ts +++ b/src/lib/inference/nim.ts @@ -26,6 +26,7 @@ import { isSafeModelId } from "../validation"; import { isDgxStationGb300Product } from "./dgx-station-identity"; import { type Arm64WslDockerDesktopGpuProver, + escapeGpuNameForTerminal, isDenylistedNvidiaGpuName, isPlausibleNvidiaGpuName, nvidiaHostLooksGenuine, @@ -164,7 +165,8 @@ export function groupGpusByName(gpus: readonly NimGpu[]): GpuGroup[] { // See #2669 for the multi-GPU case the previous fix missed. export function formatNvidiaGpuPreflightLines(gpu: GpuDetection): string[] { if (gpu.name) { - const detail = gpu.count > 1 ? `${gpu.count}x ${gpu.name}` : gpu.name; + const name = escapeGpuNameForTerminal(gpu.name); + const detail = gpu.count > 1 ? `${gpu.count}x ${name}` : name; return [`NVIDIA GPU detected (${detail}, ${gpu.totalMemoryMB} MB)`]; } if (gpu.gpus && gpu.gpus.length > 0) { @@ -174,7 +176,7 @@ export function formatNvidiaGpuPreflightLines(gpu: GpuDetection): string[] { const anyDuplicate = groups.some((grp) => grp.count > 1); for (const grp of groups) { const prefix = anyDuplicate ? `${grp.count}x ` : ""; - lines.push(` - ${prefix}${grp.name} (${grp.memoryMB} MB)`); + lines.push(` - ${prefix}${escapeGpuNameForTerminal(grp.name)} (${grp.memoryMB} MB)`); } return lines; } @@ -468,9 +470,9 @@ export function detectGpu(deps: DetectGpuDeps = {}): GpuDetection | null { // A denylisted `JMJWOA-Generic-*` placeholder. Both real Windows-ARM // N1X (WSL2 + Docker Desktop) and the Snapdragon nvidia-smi shim emit // this name, so the name and `/proc/driver/nvidia` are insufficient. - // Give the host one bounded Docker `--gpus` CUDA proof: only the real - // GPU can run the workload, so a pass safely accepts N1X while the - // shim keeps failing closed (#4565 without reopening #3988/#4424). + // A bounded Docker `--gpus` workload proves that the single reported + // row has a usable CUDA device. The Snapdragon shim cannot pass it + // (#4565 without reopening #3988/#4424). const prover = deps.proveArm64WslDockerDesktopGpu === undefined ? defaultArm64WslDockerDesktopGpuProver() diff --git a/src/lib/onboard/wsl-docker-desktop-gpu.ts b/src/lib/onboard/wsl-docker-desktop-gpu.ts index e215c881f5f..930afef8120 100644 --- a/src/lib/onboard/wsl-docker-desktop-gpu.ts +++ b/src/lib/onboard/wsl-docker-desktop-gpu.ts @@ -4,7 +4,11 @@ import fs from "node:fs"; import os from "node:os"; import { dockerInfoFormat as defaultDockerInfoFormat } from "../adapters/docker"; -import type { Arm64WslDockerDesktopGpuProver, DockerGpuProofResult } from "../inference/gpu-trust"; +import { + escapeGpuNameForTerminal, + type Arm64WslDockerDesktopGpuProver, + type DockerGpuProofResult, +} from "../inference/gpu-trust"; const WSL_DOCKER_DESKTOP_DETECTION_TIMEOUT_MS = 30_000; // This prover only ever runs on ARM64 (see `createArm64WslDockerDesktopGpuProver`), @@ -246,19 +250,6 @@ export function createArm64WslDockerDesktopGpuProver( }; } -function escapeGpuNameForTerminal(value: string): string { - return [...value] - .map((character) => { - const codePoint = character.codePointAt(0) ?? 0; - const isC0 = codePoint <= 0x1f; - const isDeleteOrC1 = codePoint >= 0x7f && codePoint <= 0x9f; - const isLineSeparator = codePoint === 0x2028 || codePoint === 0x2029; - const isFormatControl = /^\p{Cf}$/u.test(character); - if (!isC0 && !isDeleteOrC1 && !isLineSeparator && !isFormatControl) return character; - return "\\u{" + codePoint.toString(16).padStart(4, "0") + "}"; - }) - .join(""); -} export function wslDockerDesktopGpuCompatibilityAction(): WslDockerDesktopGpuCompatibilityAction { return { id: "wsl_docker_desktop_gpu_compatibility",