Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/get-started/windows-preparation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,9 @@ You can also use Ollama for Windows.
During onboarding, NemoClaw can use an already-running Windows-host daemon, start or restart an installed daemon, or install Ollama on the Windows host.
If the installer offers express install on WSL, accepting it selects a local Ollama path automatically based on the container runtime: the Windows-host Ollama path only when it detects Docker Desktop WSL integration, and WSL-local Ollama otherwise.
The Windows-host Ollama path requires Docker Desktop WSL integration; the express prompt appears on WSL regardless of the container runtime.
On a host without Node.js, the installer cannot read the Docker configuration at the prompt, so it reports that it selects the local Ollama path after installing Node.js and then applies the same runtime check.
With native Docker Engine inside WSL, or when the Docker runtime is unavailable or cannot be probed, express install configures WSL-local Ollama instead of aborting.
An installed Windows-host Ollama that the container runtime cannot reach leaves the WSL-local install available, in both the onboarding menu and a requested `install-ollama` provider.
When containers cannot reach host loopback, onboarding fronts that WSL-local daemon with the sandbox auth proxy.
You can still decline the express prompt (or set `NEMOCLAW_NO_EXPRESS=1`) to choose a provider manually; the onboarding menu labels the Windows-host actions as requiring Docker Desktop integration.
When Ollama runs on the Windows host, NemoClaw detects it from WSL through `host.docker.internal` and pulls missing models through the Ollama HTTP API.
Expand Down
51 changes: 46 additions & 5 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3381,6 +3381,7 @@ STATION_ULTRA_LEGACY_VLLM_IMAGE="vllm/vllm-openai@sha256:0fec7ec5f3e6bc168e54899
STATION_DEEPSEEK_VLLM_MODEL="deepseek-v4-flash"
STATION_DEEPSEEK_SERVED_MODEL="deepseek-ai/DeepSeek-V4-Flash"
_SELECTED_EXPRESS_PLATFORM=""
_EXPRESS_WSL_PROVIDER_PENDING=""
_STATION_EXPRESS_RESUME_REVISION=""
_STATION_EXPRESS_MODEL_WAS_EXPLICIT=0
_STATION_EXPRESS_DEFERRED_MANAGED_PAIR=0
Expand Down Expand Up @@ -4059,6 +4060,47 @@ express_wsl_can_use_windows_host_ollama() {
express_wsl_docker_operating_system | grep -qi 'docker desktop'
}

# True when a readable Docker config decides the context but no Node.js can
# parse it yet. The express prompt runs before install_nodejs, so treating that
# window as non-local pinned WSL-local Ollama on hosts whose Docker Desktop
# topology supports Windows-host Ollama, and onboarding then rejected the
# preselected provider (#8199). Selection waits for the runtime instead.
express_wsl_docker_context_needs_node() {
[ -z "${DOCKER_CONTEXT:-}" ] || return 1
local cfg="${DOCKER_CONFIG:-${HOME:-}/.docker}/config.json"
[ -e "$cfg" ] && [ -r "$cfg" ] || return 1
! command_exists node
}

# Choose between Windows-host and WSL-local Ollama, or defer when only the
# missing Node.js runtime blocks the decision.
select_express_wsl_ollama_provider() {
_EXPRESS_WSL_PROVIDER_PENDING=""
if express_wsl_can_use_windows_host_ollama; then
export NEMOCLAW_PROVIDER=install-windows-ollama
return 0
fi
if express_wsl_docker_context_needs_node; then
_EXPRESS_WSL_PROVIDER_PENDING=1
return 0
fi
export NEMOCLAW_PROVIDER=install-ollama
}

# Finish a deferred Windows WSL selection once install_nodejs has provided the
# runtime that reads the Docker configuration.
resolve_pending_express_wsl_provider() {
[ "${_EXPRESS_WSL_PROVIDER_PENDING:-}" = "1" ] || return 0
_EXPRESS_WSL_PROVIDER_PENDING=""
if express_wsl_can_use_windows_host_ollama; then
export NEMOCLAW_PROVIDER=install-windows-ollama
info "Express install will configure Windows-host Ollama through host.docker.internal."
else
export NEMOCLAW_PROVIDER=install-ollama
info "Express install will configure WSL-local Ollama."
fi
}

activate_express_install() {
local platform="$1"
_SELECTED_EXPRESS_PLATFORM="$platform"
Expand Down Expand Up @@ -4091,11 +4133,7 @@ activate_express_install() {
configure_station_express_model
;;
"Windows WSL")
if express_wsl_can_use_windows_host_ollama; then
export NEMOCLAW_PROVIDER=install-windows-ollama
else
export NEMOCLAW_PROVIDER=install-ollama
fi
select_express_wsl_ollama_provider
;;
esac
}
Expand Down Expand Up @@ -4566,6 +4604,8 @@ describe_express_install() {
"Windows WSL")
if express_wsl_can_use_windows_host_ollama; then
inference_summary="Windows-host Ollama through host.docker.internal"
elif express_wsl_docker_context_needs_node; then
inference_summary="local Ollama, selected once the installed Node.js runtime reads the Docker configuration"
else
inference_summary="WSL-local Ollama, with a sandbox auth proxy when containers cannot reach host loopback"
fi
Expand Down Expand Up @@ -4847,6 +4887,7 @@ main() {
step 1 "Node.js"
install_nodejs
ensure_supported_runtime
resolve_pending_express_wsl_provider
ensure_station_express_pair

step 2 "${_CLI_DISPLAY} CLI"
Expand Down
26 changes: 26 additions & 0 deletions src/lib/onboard/ollama-install-menu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ describe("resolveOllamaInstallMenuEntry", () => {
hasOllama: false,
ollamaRunning: false,
hasWindowsOllama: false,
windowsHostOllamaSupported: true,
installedOllamaVersion: null,
...LINUX_NON_WSL,
});
Expand All @@ -73,6 +74,7 @@ describe("resolveOllamaInstallMenuEntry", () => {
hasOllama: true,
ollamaRunning: true,
hasWindowsOllama: false,
windowsHostOllamaSupported: true,
installedOllamaVersion: "0.6.2",
runningOllamaVersion: "0.6.2",
...LINUX_NON_WSL,
Expand All @@ -89,6 +91,7 @@ describe("resolveOllamaInstallMenuEntry", () => {
hasOllama: true,
ollamaRunning: true,
hasWindowsOllama: false,
windowsHostOllamaSupported: true,
installedOllamaVersion: "0.24.0",
runningOllamaVersion: "0.24.0",
...LINUX_NON_WSL,
Expand All @@ -102,6 +105,7 @@ describe("resolveOllamaInstallMenuEntry", () => {
hasOllama: true,
ollamaRunning: true,
hasWindowsOllama: false,
windowsHostOllamaSupported: true,
ollamaHost: "127.0.0.1",
installedOllamaVersion: "0.24.0",
runningOllamaVersion: "0.6.2",
Expand All @@ -119,6 +123,7 @@ describe("resolveOllamaInstallMenuEntry", () => {
hasOllama: true,
ollamaRunning: true,
hasWindowsOllama: false,
windowsHostOllamaSupported: true,
ollamaHost: "127.0.0.1",
installedOllamaVersion: "0.6.2",
runningOllamaVersion: "0.24.0",
Expand All @@ -136,6 +141,7 @@ describe("resolveOllamaInstallMenuEntry", () => {
hasOllama: false,
ollamaRunning: true,
hasWindowsOllama: true,
windowsHostOllamaSupported: true,
ollamaHost: "host.docker.internal",
// Pretend the local-loopback probe would have returned a stale version
// if it were applied. The Windows-host case must short-circuit and not
Expand All @@ -153,17 +159,33 @@ describe("resolveOllamaInstallMenuEntry", () => {
hasOllama: false,
ollamaRunning: false,
hasWindowsOllama: true,
windowsHostOllamaSupported: true,
installedOllamaVersion: null,
...LINUX_NON_WSL,
});
expect(result.entry).toBeNull();
});

it("offers a WSL-local install when the sandbox cannot reach the Windows-host Ollama (#8199)", () => {
const result = resolveOllamaInstallMenuEntry({
hasOllama: false,
ollamaRunning: false,
hasWindowsOllama: true,
windowsHostOllamaSupported: false,
installedOllamaVersion: null,
platform: "linux",
isWsl: true,
});
expect(result.entry?.key).toBe("install-ollama");
expect(result.entry?.label).toBe("Install Ollama (WSL Linux)");
});

it("treats null versions as below the minimum to recover stale installs", () => {
const result = resolveOllamaInstallMenuEntry({
hasOllama: true,
ollamaRunning: true,
hasWindowsOllama: false,
windowsHostOllamaSupported: true,
installedOllamaVersion: null,
...LINUX_NON_WSL,
});
Expand All @@ -178,6 +200,7 @@ describe("resolveOllamaInstallMenuEntry", () => {
hasOllama: false,
ollamaRunning: false,
hasWindowsOllama: false,
windowsHostOllamaSupported: true,
installedOllamaVersion: null,
platform: "linux",
isWsl: true,
Expand All @@ -190,6 +213,7 @@ describe("resolveOllamaInstallMenuEntry", () => {
hasOllama: false,
ollamaRunning: false,
hasWindowsOllama: false,
windowsHostOllamaSupported: true,
installedOllamaVersion: null,
platform: "darwin",
isWsl: false,
Expand All @@ -202,6 +226,7 @@ describe("resolveOllamaInstallMenuEntry", () => {
hasOllama: true,
ollamaRunning: true,
hasWindowsOllama: false,
windowsHostOllamaSupported: true,
installedOllamaVersion: "0.6.2",
runningOllamaVersion: "0.6.2",
platform: "darwin",
Expand Down Expand Up @@ -259,6 +284,7 @@ describe("resolveOllamaInstallMenuEntry", () => {
hasOllama: false,
ollamaRunning: false,
hasWindowsOllama: false,
windowsHostOllamaSupported: true,
installedOllamaVersion: null,
platform: "win32",
isWsl: false,
Expand Down
18 changes: 14 additions & 4 deletions src/lib/onboard/ollama-install-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ export interface OllamaInstallMenuInput {
hasOllama: boolean;
ollamaRunning: boolean;
hasWindowsOllama: boolean;
/** Whether the sandbox can reach a Windows-host Ollama daemon at all. A
* Windows install behind a container runtime without `host.docker.internal`
* routing covers nothing, so the WSL-local install entry stays on offer.
* Only read when `hasWindowsOllama` is set; defaults to reachable. */
windowsHostOllamaSupported?: boolean;
platform: NodeJS.Platform;
isWsl: boolean;
/** Resolved host for the running Ollama daemon. `host.docker.internal`
Expand Down Expand Up @@ -110,9 +115,9 @@ function osTagFor(platform: NodeJS.Platform, isWsl: boolean): string | null {
* Decide whether the onboard provider menu should expose an `install-ollama`
* entry, and which label to render. Two cases:
*
* 1. No Ollama anywhere (host, running, or Windows) — offer a fresh install
* as a fallback (e.g. when the NVIDIA API server is down and cloud keys
* are unavailable).
* 1. No usable Ollama anywhere (host, running, or a Windows install the
* sandbox can reach) — offer a fresh install as a fallback (e.g. when the
* NVIDIA API server is down and cloud keys are unavailable).
* 2. Host Ollama exists but its version is below `MIN_OLLAMA_VERSION` —
* offer an explicit upgrade so the express setup path doesn't reuse a
* daemon that crashes loading newer starter models.
Expand Down Expand Up @@ -147,8 +152,13 @@ export function resolveOllamaInstallMenuEntry(
const daemonNeedsUpgrade =
daemonProbeApplies && !isOllamaVersionAtLeast(runningOllamaVersion, MIN_OLLAMA_VERSION);
const hasUpgradableOllama = binaryNeedsUpgrade || daemonNeedsUpgrade;
// A Windows-host install only covers the local-inference need when the
// sandbox can route to it. Under a container runtime without that routing,
// WSL-local Ollama is the only workable local provider, and suppressing its
// entry left a requested `install-ollama` with nothing to select (#8199).
const usableWindowsOllama = input.hasWindowsOllama && (input.windowsHostOllamaSupported ?? true);
const showEntry =
(!input.hasOllama && !input.ollamaRunning && !input.hasWindowsOllama) || hasUpgradableOllama;
(!input.hasOllama && !input.ollamaRunning && !usableWindowsOllama) || hasUpgradableOllama;
if (!showEntry) {
return { entry: null, hasUpgradableOllama };
}
Expand Down
22 changes: 22 additions & 0 deletions src/lib/onboard/provider-host-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

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

import { getWindowsHostOllamaDockerRequirement } from "./local-inference-topology";
import {
type DetectInferenceProviderHostStateDeps,
detectInferenceProviderHostState,
Expand Down Expand Up @@ -197,6 +198,27 @@ describe("detectInferenceProviderHostState", () => {
expect(deps.getWindowsHostOllamaDockerRequirement).toHaveBeenCalledWith("docker-desktop");
});

it("keeps the WSL-local install entry when the container runtime cannot reach the Windows host (#8199)", () => {
const deps = buildDeps({
isWsl: vi.fn(() => true),
getContainerRuntime: vi.fn<DetectInferenceProviderHostStateDeps["getContainerRuntime"]>(
() => "docker",
),
getWindowsHostOllamaDockerRequirement: vi.fn(getWindowsHostOllamaDockerRequirement),
detectWindowsHostOllama: vi.fn(() => ({
installed: true,
installedPath: "C:\\Users\\me\\AppData\\Local\\Programs\\Ollama\\ollama.exe",
loopbackOnly: false,
})),
});

const state = detectWithDeps(deps);

expect(state.hasWindowsOllama).toBe(true);
expect(state.ollamaInstallMenu.entry?.key).toBe("install-ollama");
expect(state.ollamaInstallMenu.entry?.label).toBe("Install Ollama (WSL Linux)");
});

it("passes injected platform and env through WSL detection", () => {
const env = { WSL_DISTRO_NAME: "Ubuntu" } as NodeJS.ProcessEnv;
const isWsl = vi.fn<DetectInferenceProviderHostStateDeps["isWsl"]>(() => true);
Expand Down
1 change: 1 addition & 0 deletions src/lib/onboard/provider-host-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ export function detectInferenceProviderHostState(
hasOllama,
ollamaRunning,
hasWindowsOllama,
windowsHostOllamaSupported: windowsHostOllamaDockerRequirement.supported,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
ollamaHost,
platform,
isWsl,
Expand Down
54 changes: 53 additions & 1 deletion test/install-express-wsl-ollama.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ sys.exit(exit_code)
`express_wsl_docker_operating_system() { printf 'Docker Desktop\\n'; }\n` +
`activate_express_install "Windows WSL"\n` +
`printf 'PROVIDER=%s\\n' "$NEMOCLAW_PROVIDER"\n`,
{ PATH: `${path.dirname(process.execPath)}:${TEST_SYSTEM_PATH}` },
);
expect(result.status, output).toBe(0);
expect(output).toContain("PROVIDER=install-ollama");
Expand Down Expand Up @@ -285,18 +286,69 @@ sys.exit(exit_code)
expect(output).toContain("PROVIDER=install-ollama");
});

it("activate_express_install fails closed on malformed Docker config when Node is unavailable", () => {
it("activate_express_install fails closed on malformed Docker config once Node is installed", () => {
const { result, output } = runInstallerSourced(
`mkdir -p "$HOME/.docker"\n` +
`printf '%s' 'not-json {"currentContext":"default"}' > "$HOME/.docker/config.json"\n` +
`express_wsl_docker_operating_system() { printf 'Docker Desktop\\n'; }\n` +
`activate_express_install "Windows WSL"\n` +
`printf 'DEFERRED=%s PROVIDER=%s\\n' "\${_EXPRESS_WSL_PROVIDER_PENDING:-}" "\${NEMOCLAW_PROVIDER:-}"\n` +
`PATH="$NODE_BIN_DIR:$PATH"\n` +
`resolve_pending_express_wsl_provider\n` +
`printf 'PROVIDER=%s\\n' "$NEMOCLAW_PROVIDER"\n`,
{ NODE_BIN_DIR: path.dirname(process.execPath) },
);
expect(result.status, output).toBe(0);
expect(output).toContain("DEFERRED=1 PROVIDER=");
expect(output).toContain("PROVIDER=install-ollama");
});

it("defers Windows WSL selection until Node.js can read the Docker configuration (#8199)", () => {
const { result, output } = runInstallerSourced(
`mkdir -p "$HOME/.docker"\n` +
`printf '%s' '{}' > "$HOME/.docker/config.json"\n` +
`express_wsl_docker_operating_system() { printf 'Docker Desktop\\n'; }\n` +
`activate_express_install "Windows WSL"\n` +
`printf 'DEFERRED=%s PROVIDER=%s\\n' "\${_EXPRESS_WSL_PROVIDER_PENDING:-}" "\${NEMOCLAW_PROVIDER:-}"\n` +
`PATH="$NODE_BIN_DIR:$PATH"\n` +
`resolve_pending_express_wsl_provider\n` +
`printf 'PROVIDER=%s\\n' "$NEMOCLAW_PROVIDER"\n`,
{ NODE_BIN_DIR: path.dirname(process.execPath) },
);
expect(result.status, output).toBe(0);
expect(output).toContain("DEFERRED=1 PROVIDER=");
expect(output).toContain("PROVIDER=install-windows-ollama");
});

it("describes a deferred Windows WSL selection before Node.js is installed (#8199)", () => {
const { result, output } = runInstallerSourced(
`mkdir -p "$HOME/.docker"\n` +
`printf '%s' '{}' > "$HOME/.docker/config.json"\n` +
`express_wsl_docker_operating_system() { printf 'Docker Desktop\\n'; }\n` +
`describe_express_install "Windows WSL"\n`,
);
expect(result.status, output).toBe(0);
expect(output).toMatch(
/Express install will configure local Ollama, selected once the installed Node\.js runtime reads the Docker configuration/,
);
});

it("keeps a resolved Windows WSL selection out of the deferred path", () => {
const dockerBin = dockerStubBin("Docker Desktop");
const { result, output } = runInstallerSourced(
`mkdir -p "$HOME/.docker"\n` +
`printf '%s' '{"currentContext":"default"}' > "$HOME/.docker/config.json"\n` +
`activate_express_install "Windows WSL"\n` +
`printf 'DEFERRED=%s PROVIDER=%s\\n' "\${_EXPRESS_WSL_PROVIDER_PENDING:-}" "\${NEMOCLAW_PROVIDER:-}"\n` +
`resolve_pending_express_wsl_provider\n` +
`printf 'PROVIDER=%s\\n' "$NEMOCLAW_PROVIDER"\n`,
{ PATH: `${dockerBin}:${path.dirname(process.execPath)}:${TEST_SYSTEM_PATH}` },
);
expect(result.status, output).toBe(0);
expect(output).toContain("DEFERRED= PROVIDER=install-windows-ollama");
expect(output).toContain("PROVIDER=install-windows-ollama");
});

it("activate_express_install fails closed on an unreadable Docker config", () => {
const { result, output } = runInstallerSourced(
`mkdir -p "$HOME/.docker"\n` +
Expand Down
Loading