-
Notifications
You must be signed in to change notification settings - Fork 3.1k
fix(onboard): probe-and-pick WSL2 host IP candidates for local inference #1864
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
92d111e
5800da8
768b4f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3233,6 +3233,11 @@ async function setupInference( | |
| step(4, 8, "Setting up inference provider"); | ||
| runOpenshell(["gateway", "select", GATEWAY_NAME], { ignoreError: true }); | ||
|
|
||
| // Populated by local-inference branches on WSL2 + Docker Desktop; | ||
| // persisted to the registry so later commands / diagnostics can see | ||
| // which host IP was injected into `host.openshell.internal`. | ||
| let resolvedHostIp = null; | ||
|
|
||
| if ( | ||
| provider === "nvidia-prod" || | ||
| provider === "nvidia-nim" || | ||
|
|
@@ -3311,12 +3316,25 @@ async function setupInference( | |
| process.exit(applyResult.status || 1); | ||
| } | ||
| } else if (provider === "vllm-local") { | ||
| const validation = validateLocalProvider(provider, runCapture); | ||
| const platformOpts = { | ||
| isWsl: isWsl(), | ||
| isDockerDesktop: getContainerRuntime() === "docker-desktop", | ||
| }; | ||
| const validation = validateLocalProvider(provider, runCapture, platformOpts); | ||
| if (!validation.ok) { | ||
| console.error(` ${validation.message}`); | ||
| process.exit(1); | ||
| const answer = (await prompt(" Continue anyway? Inference may fail at runtime. [y/N]: ")) | ||
| .trim() | ||
| .toLowerCase(); | ||
| if (answer !== "y") { | ||
| process.exit(1); | ||
| } | ||
| } | ||
| resolvedHostIp = validation.resolvedHostIp || null; | ||
| if (resolvedHostIp) { | ||
| console.log(` Resolved WSL2 host IP for container access: ${resolvedHostIp}`); | ||
| } | ||
| const baseUrl = getLocalProviderBaseUrl(provider); | ||
| const baseUrl = getLocalProviderBaseUrl(provider, resolvedHostIp ?? undefined); | ||
| const providerResult = upsertProvider("vllm-local", "openai", "OPENAI_API_KEY", baseUrl, { | ||
| OPENAI_API_KEY: "dummy", | ||
| }); | ||
|
|
@@ -3336,13 +3354,26 @@ async function setupInference( | |
| String(LOCAL_INFERENCE_TIMEOUT_SECS), | ||
| ]); | ||
| } else if (provider === "ollama-local") { | ||
| const validation = validateLocalProvider(provider, runCapture); | ||
| const platformOpts = { | ||
| isWsl: isWsl(), | ||
| isDockerDesktop: getContainerRuntime() === "docker-desktop", | ||
| }; | ||
| const validation = validateLocalProvider(provider, runCapture, platformOpts); | ||
| if (!validation.ok) { | ||
| console.error(` ${validation.message}`); | ||
| console.error(" On macOS, local inference also depends on OpenShell host routing support."); | ||
| process.exit(1); | ||
| const answer = (await prompt(" Continue anyway? Inference may fail at runtime. [y/N]: ")) | ||
| .trim() | ||
| .toLowerCase(); | ||
| if (answer !== "y") { | ||
| process.exit(1); | ||
| } | ||
| } | ||
| resolvedHostIp = validation.resolvedHostIp || null; | ||
| if (resolvedHostIp) { | ||
| console.log(` Resolved WSL2 host IP for container access: ${resolvedHostIp}`); | ||
| } | ||
| const baseUrl = getLocalProviderBaseUrl(provider); | ||
| const baseUrl = getLocalProviderBaseUrl(provider, resolvedHostIp ?? undefined); | ||
| const providerResult = upsertProvider("ollama-local", "openai", "OPENAI_API_KEY", baseUrl, { | ||
| OPENAI_API_KEY: "ollama", | ||
| }); | ||
|
|
@@ -3371,7 +3402,11 @@ async function setupInference( | |
| } | ||
|
|
||
| verifyInferenceRoute(provider, model); | ||
| registry.updateSandbox(sandboxName, { model, provider }); | ||
| registry.updateSandbox(sandboxName, { | ||
| model, | ||
| provider, | ||
| resolvedHostIp: resolvedHostIp || null, | ||
| }); | ||
|
Comment on lines
+3405
to
+3409
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This registry write never reaches the real sandbox entry.
Please move this persistence until after the sandbox has been registered, or store it in session state and apply it once the real sandbox entry exists. 🤖 Prompt for AI Agents |
||
| console.log(` ✓ Inference route set: ${provider} / ${model}`); | ||
| return { ok: true }; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -155,6 +155,115 @@ $ nemoclaw onboard | |
| Podman is not a tested runtime. | ||
| If onboarding or sandbox lifecycle fails, switch to a tested runtime (Docker Desktop, Colima, or Docker Engine) and rerun onboarding. | ||
|
|
||
| ### Local inference on WSL2 + Docker Desktop | ||
|
|
||
| On WSL2 with Docker Desktop, the conventional `host.openshell.internal` | ||
| gateway hostname (backed by Docker's `host-gateway`) often resolves to | ||
| an IPv6 ULA or an un-routable gateway IP. That can hang the onboard | ||
| container reachability probe (step 4/8) and break inference routing to | ||
| a host-side Ollama or vLLM. | ||
|
|
||
| Onboarding now detects this combination and probes a list of candidate | ||
| host IPs in order: | ||
|
|
||
| 1. The WSL distro's outbound IPv4 (`ip -4 -o route get 1.1.1.1`) — | ||
| correct when Ollama or vLLM runs **inside WSL**. | ||
| 2. The WSL2 default gateway (`ip -4 -o route show default`) — correct | ||
| when Ollama or vLLM runs on the **Windows host** in NAT networking | ||
| mode. | ||
| 3. Other interface addresses from `hostname -I`. | ||
|
|
||
| The first candidate whose container-side probe succeeds is injected | ||
| into both `OPENAI_BASE_URL` and the reachability check, and persisted | ||
| to the sandbox registry entry as `resolvedHostIp`. No manual override | ||
| is needed for either Ollama placement. | ||
|
Comment on lines
+176
to
+179
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid absolute wording about This states persistence as guaranteed, but current behavior is best-effort in onboarding flow and has a known persistence-ordering follow-up. Please soften this to "attempts to persist" to avoid misleading troubleshooting expectations. 🤖 Prompt for AI Agents |
||
|
|
||
| In WSL **mirrored** networking mode, `host.openshell.internal` already | ||
| reaches the shared network stack directly, so no override is applied. | ||
|
|
||
| #### Host-side prerequisites for Windows-hosted Ollama | ||
|
|
||
| If Ollama runs on the **Windows host** (not inside WSL), NemoClaw's | ||
| detection only helps once the host itself is actually reachable from | ||
| WSL. Run the following checks in the indicated shell. | ||
|
|
||
| **1. Bind Ollama to all interfaces (run in PowerShell, as Administrator):** | ||
|
|
||
| ```powershell | ||
| # Persist across reboots; Machine scope so services also inherit it. | ||
| [System.Environment]::SetEnvironmentVariable('OLLAMA_HOST','0.0.0.0:11434','Machine') | ||
|
|
||
| # Stop Ollama (tray + server) and start it in a new shell so it picks | ||
| # up the new env var. Open a NEW PowerShell window first, then: | ||
| Get-Process | Where-Object { $_.ProcessName -like 'ollama*' } | Stop-Process -Force | ||
| ollama serve | ||
| ``` | ||
|
Comment on lines
+192
to
+200
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use These CLI examples are tagged as Suggested formatting adjustment-```powershell
+```console
+$ [System.Environment]::SetEnvironmentVariable('OLLAMA_HOST','0.0.0.0:11434','Machine')
...
-Get-Process | Where-Object { $_.ProcessName -like 'ollama*' } | Stop-Process -Force
-ollama serve
+$ Get-Process | Where-Object { $_.ProcessName -like 'ollama*' } | Stop-Process -Force
+$ ollama serveVerify each finding against the current code and only fix it if needed. In [System.Environment]::SetEnvironmentVariable('OLLAMA_HOST'...) and the
Get-Process | Where-Object ... | Stop-Process -Force and ollama serve lines with
a ```console block and add `$ ` before each command), and apply the same
transformation to the other referenced blocks (lines 204-207, 211-215, 232-234,
238-241, 255-257) so all CLI examples follow the docs guideline. |
||
|
|
||
| Verify the bind address (run in PowerShell): | ||
|
|
||
| ```powershell | ||
| Get-NetTCPConnection -LocalPort 11434 -State Listen | Select LocalAddress, LocalPort | ||
| # Expect: 0.0.0.0 or [::] — NOT 127.0.0.1 | ||
| ``` | ||
|
|
||
| **2. Allow inbound TCP 11434 in Windows Defender Firewall (PowerShell, Administrator):** | ||
|
|
||
| ```powershell | ||
| New-NetFirewallRule -DisplayName "Ollama 11434 (WSL)" ` | ||
| -Direction Inbound -Protocol TCP -LocalPort 11434 ` | ||
| -Action Allow -Profile Any | ||
| ``` | ||
|
|
||
| **3. Switch WSL2 to mirrored networking mode.** On recent Windows 11, | ||
| WSL2 in NAT mode routes traffic through a separate Hyper-V firewall | ||
| layer that ignores standard inbound rules (you will see | ||
| `NATInboundRuleNotApplicable` on `Get-NetFirewallHyperVRule`). Mirrored | ||
| mode makes WSL share the Windows network stack directly. | ||
|
|
||
| Edit `%USERPROFILE%\.wslconfig` (PowerShell or Notepad on Windows): | ||
|
|
||
| ```ini | ||
| [wsl2] | ||
| networkingMode=mirrored | ||
| ``` | ||
|
|
||
| Then apply (run in PowerShell): | ||
|
|
||
| ```powershell | ||
| wsl --shutdown | ||
| ``` | ||
|
|
||
| Reopen your WSL terminal and verify Ollama is reachable (run in WSL): | ||
|
|
||
| ```bash | ||
| curl --max-time 5 http://127.0.0.1:11434/api/tags | ||
| # Expect: JSON list of installed models. | ||
| ``` | ||
|
|
||
| #### If the container reachability check still fails | ||
|
|
||
| If onboarding still reports that the container reachability check | ||
| failed for `http://host.openshell.internal:11434`: | ||
|
|
||
| - Double-check the bind address (PowerShell): Ollama shows | ||
| `127.0.0.1` in `Get-NetTCPConnection` until the env var reaches the | ||
| process from a fresh shell. | ||
| - Confirm the firewall rule is enabled (PowerShell): | ||
| `Get-NetFirewallRule -DisplayName "Ollama 11434 (WSL)" | Select Enabled, Profile`. | ||
| - If you cannot switch to mirrored mode, manually set the base URL | ||
| using the WSL2 default gateway (run in WSL to find it): | ||
| ```bash | ||
| ip route show default | awk '/default/ {print $3}' | ||
| ``` | ||
|
Comment on lines
+255
to
+257
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add blank lines around the nested fenced code block. This block trips As per coding guidelines, "Follow style guide in 🧰 Tools🪛 markdownlint-cli2 (0.22.0)[warning] 255-255: Fenced code blocks should be surrounded by blank lines (MD031, blanks-around-fences) [warning] 257-257: Fenced code blocks should be surrounded by blank lines (MD031, blanks-around-fences) 🤖 Prompt for AI Agents |
||
| then export that IP as `OPENAI_BASE_URL=http://<gateway-ip>:11434/v1`. | ||
| - Last resort: install Docker Engine directly inside WSL2 instead of | ||
| Docker Desktop — `host-gateway` works reliably there. | ||
|
|
||
| Sandbox pod → host egress is a separate path from the onboard probe. If | ||
| inference calls still fail from inside a running sandbox, verify the | ||
| sandbox's `HTTP_PROXY` / `ALL_PROXY` env vars include the resolved host | ||
| IP in `NO_PROXY`. | ||
|
|
||
| ### Invalid sandbox name | ||
|
|
||
| Sandbox names must follow RFC 1123 subdomain rules: lowercase alphanumeric characters and hyphens only, and must start and end with an alphanumeric character. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep local-provider probe failures non-interactive in
--non-interactivemode.These branches now call
prompt()unconditionally on validation failure. Ifnemoclaw onboard --non-interactiveselectsollamaorvllm, a bad probe turns into a hang instead of the hard failure the rest of the wizard uses.Suggested fix
if (!validation.ok) { console.error(` ${validation.message}`); + if (isNonInteractive()) { + process.exit(1); + } const answer = (await prompt(" Continue anyway? Inference may fail at runtime. [y/N]: ")) .trim() .toLowerCase(); if (answer !== "y") { process.exit(1); } }Apply the same guard in both local-provider branches.
Also applies to: 3362-3369
🤖 Prompt for AI Agents