Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions docs/get-started/windows-preparation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,10 @@ 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.
When the installer must read the Docker configuration file to determine the effective Docker context and Node.js is unavailable, it defers provider selection until after it installs Node.js.
It then reads the configuration and 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
52 changes: 47 additions & 5 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3512,6 +3512,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 @@ -4190,6 +4191,48 @@ express_wsl_can_use_windows_host_ollama() {
express_wsl_docker_operating_system | grep -qi 'docker desktop'
}

# True when a readable Docker configuration 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_HOST:-}" ] || return 1
[ -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 @@ -4222,11 +4265,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 @@ -4698,6 +4737,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 @@ -4979,6 +5020,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 @@ -216,6 +216,28 @@ describe("detectInferenceProviderHostState", () => {
expect(deps.getWindowsHostOllamaDockerRequirement).toHaveBeenCalledWith("docker-desktop");
});

it("keeps WSL-local install available when Docker Desktop cannot reach Windows-host Ollama (#8199)", () => {
const deps = buildDeps({
isWsl: vi.fn(() => true),
getContainerRuntime: vi.fn<DetectInferenceProviderHostStateDeps["getContainerRuntime"]>(
() => "docker-desktop",
),
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.windowsHostOllamaDockerRequirement.supported).toBe(true);
expect(state.windowsOllamaReachable).toBe(false);
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
2 changes: 2 additions & 0 deletions src/lib/onboard/provider-host-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ export function detectInferenceProviderHostState(
hasOllama,
ollamaRunning,
hasWindowsOllama,
windowsHostOllamaSupported:
windowsHostOllamaDockerRequirement.supported && windowsOllamaReachable,
ollamaHost,
platform,
isWsl,
Expand Down
21 changes: 21 additions & 0 deletions src/lib/shields/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,27 @@ describe("shields — unit logic", () => {
expect(writeTempPolicy).not.toHaveBeenCalled();
});

it("deadline restore reuses an unchanged snapshot without temporary storage when no managed MCP entries exist (#7952)", async () => {
const snapshotPath = path.join(stateDir(), "policy-snapshot-no-managed-mcp.yaml");
fs.mkdirSync(stateDir(), { recursive: true });
fs.writeFileSync(snapshotPath, "version: 1\nnetwork_policies:\n restrictive_baseline: {}\n");
const createTempDirectory = vi.spyOn(fs, "mkdtempSync").mockImplementation(() => {
throw Object.assign(new Error("ENOSPC: simulated temporary storage full"), {
code: "ENOSPC",
});
});
const { buildDeadlineRuntimeManagedMcpPolicy } = await import("./permissive-runtime");

const result = buildDeadlineRuntimeManagedMcpPolicy(snapshotPath, {
managedMcpPolicies: [],
snapshotManagedPolicyKeys: [],
readBasePolicy: () => fs.readFileSync(snapshotPath, "utf-8"),
});

expect(result).toEqual({ path: snapshotPath, omissions: [] });
expect(createTempDirectory).not.toHaveBeenCalled();
});

it("shieldsStatus warns and stays DOWN when inline recovery fails", async () => {
const sandboxName = "openclaw";
const missingSnapshotPath = path.join(stateDir(), "missing-snapshot.yaml");
Expand Down
Loading
Loading