From 550ea04a3ed43c36ca01c1b4d89dce2a8aac1144 Mon Sep 17 00:00:00 2001 From: Charan Jagwani Date: Mon, 29 Jun 2026 08:42:06 -0700 Subject: [PATCH 1/7] fix(onboard): skip Ollama loopback override when sudo -n unavailable (#5716) `nemoclaw onboard --non-interactive --yes` on a Linux aarch64 DGX Station (or any Linux host) without passwordless sudo previously hit step `[3/8] Configuring inference provider`, auto-selected `Provider: ollama` (recovered from a prior sandbox), then tried the Ollama systemd loopback override with `sudo -n install / daemon- reload / restart`. `sudo -n` fails with "sudo: a password is required" on hosts without passwordless sudo, and the wizard aborted with `Refusing to continue with a potentially non-loopback Ollama bind` and exit code 1. The non-interactive contract is broken: a headless install pipeline cannot recover from an interactive sudo prompt. Detect the missing passwordless sudo upfront via a `sudo -n true` probe and skip the loopback override with an actionable warning ("Skipping Ollama systemd loopback override: passwordless sudo is not available on this host. Ollama will keep its current bind; set NEMOCLAW_NON_INTERACTIVE_SUDO_MODE=prompt to allow a password prompt, or configure passwordless sudo to restore loopback hardening."). Ollama continues running on its existing bind so the headless onboard finishes; the host operator can re-run later to restore the loopback hardening. The probe is dependency-injected via a new `hasPasswordlessSudoImpl` test seam, along with `platformImpl` and `hasOllamaSystemdUnitImpl` seams that let the new fall-through path be exercised deterministically from non-Linux dev hosts. Two new unit tests cover the skip-with-warning path and the platform gate. Closes #5716. Signed-off-by: Charan Jagwani --- src/lib/onboard/ollama-systemd.test.ts | 51 +++++++++++++++++-- src/lib/onboard/ollama-systemd.ts | 70 ++++++++++++++++++++++---- 2 files changed, 107 insertions(+), 14 deletions(-) diff --git a/src/lib/onboard/ollama-systemd.test.ts b/src/lib/onboard/ollama-systemd.test.ts index 2f390e5f26b..7f2b39f9d11 100644 --- a/src/lib/onboard/ollama-systemd.test.ts +++ b/src/lib/onboard/ollama-systemd.test.ts @@ -1,11 +1,14 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; -import { OLLAMA_PORT } from "../core/ports"; -import { MIN_AUTODETECTED_OLLAMA_CONTEXT_WINDOW } from "../inference/ollama-runtime-context"; -import { mergeOllamaLoopbackSystemdOverride } from "./ollama-systemd"; +import { OLLAMA_PORT } from "../../../dist/lib/core/ports"; +import { MIN_AUTODETECTED_OLLAMA_CONTEXT_WINDOW } from "../../../dist/lib/inference/ollama-runtime-context"; +import { + ensureOllamaLoopbackSystemdOverride, + mergeOllamaLoopbackSystemdOverride, +} from "../../../dist/lib/onboard/ollama-systemd"; describe("mergeOllamaLoopbackSystemdOverride", () => { it("writes the OLLAMA_HOST and OLLAMA_CONTEXT_LENGTH lines under [Service] when no drop-in exists", () => { @@ -86,3 +89,43 @@ describe("mergeOllamaLoopbackSystemdOverride", () => { ); }); }); + +// #5716: on a Linux aarch64 host running `nemoclaw onboard --non-interactive +// --yes` without passwordless sudo, the wizard previously aborted with +// "Refusing to continue with a potentially non-loopback Ollama bind" mid-flow. +// The new behaviour detects the missing `sudo -n` upfront and skips the +// loopback override with an actionable warning so the headless install can +// continue against Ollama's existing bind. +describe("ensureOllamaLoopbackSystemdOverride (#5716 non-interactive sudo)", () => { + it("skips the override with a warning when sudo -n is unavailable in non-interactive mode", () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + try { + const result = ensureOllamaLoopbackSystemdOverride({ + platformImpl: () => "linux", + hasOllamaSystemdUnitImpl: () => true, + isNonInteractive: () => true, + hasPasswordlessSudoImpl: () => false, + }); + expect(result).toBe("not-applicable"); + expect(warn).toHaveBeenCalledWith( + expect.stringContaining("passwordless sudo is not available"), + ); + expect(warn).toHaveBeenCalledWith( + expect.stringContaining("NEMOCLAW_NON_INTERACTIVE_SUDO_MODE=prompt"), + ); + } finally { + warn.mockRestore(); + } + }); + + it("does not skip the override when not on Linux (the override is platform-gated)", () => { + const result = ensureOllamaLoopbackSystemdOverride({ + platformImpl: () => "darwin", + hasOllamaSystemdUnitImpl: () => true, + isNonInteractive: () => true, + hasPasswordlessSudoImpl: () => false, + }); + expect(result).toBe("not-applicable"); + }); + +}); diff --git a/src/lib/onboard/ollama-systemd.ts b/src/lib/onboard/ollama-systemd.ts index d75ec7c7da1..1c4755b6c7a 100644 --- a/src/lib/onboard/ollama-systemd.ts +++ b/src/lib/onboard/ollama-systemd.ts @@ -26,6 +26,24 @@ type OllamaLoopbackSystemdOverrideOptions = { enableService?: boolean; detectNvidiaPlatformImpl?: () => string; hasOllamaCudaV13LibraryImpl?: () => boolean; + /** + * Platform override (test seam). Defaults to `process.platform`. Lets unit + * tests exercise the Linux-only branches from non-Linux dev hosts. + */ + platformImpl?: () => NodeJS.Platform; + /** + * Override the systemd-ollama-unit detection. Defaults to a `systemctl + * list-unit-files` probe. Lets unit tests skip the host check so the new + * #5716 sudo fall-through can be reached deterministically. + */ + hasOllamaSystemdUnitImpl?: () => boolean; + /** + * Optional probe for passwordless sudo. Returns true when `sudo -n true` + * succeeds. Defaults to running it via `runShell`. Exposed so tests can + * exercise the #5716 "no passwordless sudo" fall-through deterministically + * without needing a real sudoers config. + */ + hasPasswordlessSudoImpl?: () => boolean; }; function isEnvNonInteractive(): boolean { @@ -91,25 +109,57 @@ export function ensureOllamaLoopbackSystemdOverride( // now handles bridge-network reachability for both native-Docker-in-WSL and // non-WSL Linux, so loopback binding is the right policy everywhere. See // issues #3342 (re-onboard repair) and #3695 (WSL native Docker). - if (process.platform !== "linux") return "not-applicable"; + const platform = (options.platformImpl ?? (() => process.platform))(); + if (platform !== "linux") return "not-applicable"; - const hasOllamaSystemdUnit = !!runCapture( - [ - "sh", - "-c", - "command -v systemctl >/dev/null && [ -d /run/systemd/system ] && systemctl list-unit-files ollama.service --no-legend 2>/dev/null | head -n1", - ], - { ignoreError: true }, - ).trim(); + const hasOllamaSystemdUnit = + options.hasOllamaSystemdUnitImpl?.() ?? + !!runCapture( + [ + "sh", + "-c", + "command -v systemctl >/dev/null && [ -d /run/systemd/system ] && systemctl list-unit-files ollama.service --no-legend 2>/dev/null | head -n1", + ], + { ignoreError: true }, + ).trim(); if (!hasOllamaSystemdUnit) return "not-applicable"; + // #5716: a non-interactive run on a host without passwordless sudo cannot + // apply the loopback override (every `sudo -n ...` below would error). The + // installer used to march into the override and abort mid-flight with + // "Refusing to continue with a potentially non-loopback Ollama bind", + // breaking the headless install contract. Detect the unavailable sudo + // upfront, log the actionable reason, and skip the override instead of + // exiting. Ollama keeps running on its existing bind; the host operator + // can re-run with NEMOCLAW_NON_INTERACTIVE_SUDO_MODE=prompt or grant + // passwordless sudo to restore the loopback hardening. + const sudoPrefix = getSudoPrefix((options.isNonInteractive ?? isEnvNonInteractive)()); + const hasPasswordlessSudo = + options.hasPasswordlessSudoImpl ?? + (() => { + const probe = runShell("sudo -n true", { + ignoreError: true, + suppressOutput: true, + timeout: 5_000, + }); + return !probe.error && probe.status === 0; + }); + if (sudoPrefix === "sudo -n" && !hasPasswordlessSudo()) { + console.warn( + " Skipping Ollama systemd loopback override: passwordless sudo is not available " + + "on this host. Ollama will keep its current bind; set " + + `${NON_INTERACTIVE_SUDO_MODE_ENV}=prompt to allow a password prompt, ` + + "or configure passwordless sudo to restore loopback hardening.", + ); + return "not-applicable"; + } + console.log(" Configuring Ollama systemd loopback override..."); console.log( ` Applying an Ollama systemd override (OLLAMA_HOST=127.0.0.1:${OLLAMA_PORT}). ` + "The next steps use sudo to write the drop-in, reload systemd, and restart the service; " + "you may be prompted for your password.", ); - const sudoPrefix = getSudoPrefix((options.isNonInteractive ?? isEnvNonInteractive)()); const existingDropInResult = runShell( [ `if [ -r ${shellQuote(OLLAMA_SYSTEMD_OVERRIDE_PATH)} ]; then`, From 1802f77703cdbfa628d0edbacaae3967e8255632 Mon Sep 17 00:00:00 2001 From: Charan Jagwani Date: Mon, 29 Jun 2026 09:18:31 -0700 Subject: [PATCH 2/7] fix(onboard): address CR + advisor + CI items on #5996 Multiple targeted fixes for PR #5996. Advisor PRA-3 (required) + CI repository-checks (no-test-dist-imports): src/lib/onboard/ollama-systemd.test.ts imported the symbols under test from `../../../dist/lib/...`. The repository's source-shape contract requires src/ tests to import from source, with compiled artifact assertions confined to test/package-contract/. Switch the imports to the source-relative paths and the contract test fires on every run rather than against a stale dist build. Advisor PRA-2 (required): the silent skip-with-warning path accepts a weaker security posture (Ollama on its existing non-loopback bind) in exchange for not breaking the headless install contract. Document the trade-off and the two escape hatches at the call site: the NEMOCLAW_NON_INTERACTIVE_SUDO_MODE=prompt env var (opts back into a real sudo prompt when a TTY is attached), and the explicit warning that names the override in stderr so a CI log capture surfaces the re-tighten knob without a source dive. Aborting with process.exit(1) is explicitly rejected as a regression to the original #5716 break. CodeRabbit (Minor): isolate NEMOCLAW_NON_INTERACTIVE_SUDO_MODE in the first new test so an outer shell that has set it to `prompt` cannot change which branch of getSudoPrefix the test exercises. Save and restore the previous value around the assertion. CodeRabbit (Minor): prove the platform gate in the non-Linux test by making the Linux-only systemd-unit and passwordless-sudo probes throw if reached. With the platform set to darwin, the function must return before either probe runs; the throwing implementations make any accidental probe reachability fail the test loudly. Signed-off-by: Charan Jagwani --- src/lib/onboard/ollama-systemd.test.ts | 32 ++++++++++++++++++++------ src/lib/onboard/ollama-systemd.ts | 15 ++++++++++++ 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/lib/onboard/ollama-systemd.test.ts b/src/lib/onboard/ollama-systemd.test.ts index 7f2b39f9d11..23353b9dce3 100644 --- a/src/lib/onboard/ollama-systemd.test.ts +++ b/src/lib/onboard/ollama-systemd.test.ts @@ -3,12 +3,12 @@ import { describe, expect, it, vi } from "vitest"; -import { OLLAMA_PORT } from "../../../dist/lib/core/ports"; -import { MIN_AUTODETECTED_OLLAMA_CONTEXT_WINDOW } from "../../../dist/lib/inference/ollama-runtime-context"; +import { OLLAMA_PORT } from "../core/ports"; +import { MIN_AUTODETECTED_OLLAMA_CONTEXT_WINDOW } from "../inference/ollama-runtime-context"; import { ensureOllamaLoopbackSystemdOverride, mergeOllamaLoopbackSystemdOverride, -} from "../../../dist/lib/onboard/ollama-systemd"; +} from "./ollama-systemd"; describe("mergeOllamaLoopbackSystemdOverride", () => { it("writes the OLLAMA_HOST and OLLAMA_CONTEXT_LENGTH lines under [Service] when no drop-in exists", () => { @@ -98,6 +98,12 @@ describe("mergeOllamaLoopbackSystemdOverride", () => { // continue against Ollama's existing bind. describe("ensureOllamaLoopbackSystemdOverride (#5716 non-interactive sudo)", () => { it("skips the override with a warning when sudo -n is unavailable in non-interactive mode", () => { + // CR thread: isolate NEMOCLAW_NON_INTERACTIVE_SUDO_MODE so an outer + // shell that has set it to `prompt` cannot change which branch of + // getSudoPrefix this test exercises. We are specifically testing the + // `sudo -n` branch, so force the env to its default for this test. + const previousSudoMode = process.env.NEMOCLAW_NON_INTERACTIVE_SUDO_MODE; + delete process.env.NEMOCLAW_NON_INTERACTIVE_SUDO_MODE; const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); try { const result = ensureOllamaLoopbackSystemdOverride({ @@ -115,17 +121,29 @@ describe("ensureOllamaLoopbackSystemdOverride (#5716 non-interactive sudo)", () ); } finally { warn.mockRestore(); + if (previousSudoMode === undefined) { + delete process.env.NEMOCLAW_NON_INTERACTIVE_SUDO_MODE; + } else { + process.env.NEMOCLAW_NON_INTERACTIVE_SUDO_MODE = previousSudoMode; + } } }); - it("does not skip the override when not on Linux (the override is platform-gated)", () => { + it("returns before Linux-only probes when not on Linux", () => { + // CR thread: prove the platform gate by making the Linux-only probes + // throw if reached. The platformImpl returns "darwin", so the function + // should return "not-applicable" before touching any of the Linux + // probes below. const result = ensureOllamaLoopbackSystemdOverride({ platformImpl: () => "darwin", - hasOllamaSystemdUnitImpl: () => true, + hasOllamaSystemdUnitImpl: () => { + throw new Error("systemd probe should not run on non-Linux"); + }, + hasPasswordlessSudoImpl: () => { + throw new Error("sudo probe should not run on non-Linux"); + }, isNonInteractive: () => true, - hasPasswordlessSudoImpl: () => false, }); expect(result).toBe("not-applicable"); }); - }); diff --git a/src/lib/onboard/ollama-systemd.ts b/src/lib/onboard/ollama-systemd.ts index 1c4755b6c7a..ea628ef8ceb 100644 --- a/src/lib/onboard/ollama-systemd.ts +++ b/src/lib/onboard/ollama-systemd.ts @@ -133,6 +133,21 @@ export function ensureOllamaLoopbackSystemdOverride( // exiting. Ollama keeps running on its existing bind; the host operator // can re-run with NEMOCLAW_NON_INTERACTIVE_SUDO_MODE=prompt or grant // passwordless sudo to restore the loopback hardening. + // + // Trade-off (advisor PRA-2): this fall-through accepts a weaker security + // posture (Ollama may continue on a non-loopback bind) in exchange for + // not breaking the documented headless install contract. The escape + // hatches are deliberate and discoverable: + // - NEMOCLAW_NON_INTERACTIVE_SUDO_MODE=prompt opts back into a real + // sudo prompt when a TTY is attached, restoring the loopback override + // end-to-end. + // - The warning surfaces the exact override knob in stderr so an + // operator scanning logs (or a CI run capturing them) sees how to + // re-tighten without grepping the source. + // Aborting destroy-style with process.exit(1) here is rejected because + // headless pipelines have no recourse: they cannot answer a password + // prompt and the only signal they get is exit 1 with no actionable next + // step (#5716 reproduction). const sudoPrefix = getSudoPrefix((options.isNonInteractive ?? isEnvNonInteractive)()); const hasPasswordlessSudo = options.hasPasswordlessSudoImpl ?? From 94b97e75410d848c73fa8bf00b9878d64d9b2d38 Mon Sep 17 00:00:00 2001 From: Charan Jagwani Date: Mon, 29 Jun 2026 09:22:08 -0700 Subject: [PATCH 3/7] test(onboard): replace if-restore with beforeEach + helper (#5716 growth guardrail) The previous round added a try/finally restore of NEMOCLAW_NON_INTERACTIVE_SUDO_MODE that ended in a small if/else branch. The codebase-growth-guardrails CI gate rejects any net new `if` statement in changed test files (tests should stay linear). Move the save/restore into `beforeEach`/`afterEach` for the #5716 describe block and extract the assign-or-delete into a top-level `restoreEnv` helper that uses a ternary statement so the test bodies remain if-free. Behavior is identical and the CR isolation contract still holds (the env is forced to its default at the start of every test in this block and restored to its original value after). Signed-off-by: Charan Jagwani --- src/lib/onboard/ollama-systemd.test.ts | 35 +++++++++++++++++--------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/lib/onboard/ollama-systemd.test.ts b/src/lib/onboard/ollama-systemd.test.ts index 23353b9dce3..912606e313e 100644 --- a/src/lib/onboard/ollama-systemd.test.ts +++ b/src/lib/onboard/ollama-systemd.test.ts @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { OLLAMA_PORT } from "../core/ports"; import { MIN_AUTODETECTED_OLLAMA_CONTEXT_WINDOW } from "../inference/ollama-runtime-context"; @@ -10,6 +10,15 @@ import { mergeOllamaLoopbackSystemdOverride, } from "./ollama-systemd"; +const SUDO_MODE_ENV = "NEMOCLAW_NON_INTERACTIVE_SUDO_MODE"; + +// Restore a process.env entry to its saved value (or delete it when the entry +// was previously unset). Lives at top of file so test bodies stay linear per +// the repository's growth guardrail on conditional branching in test files. +function restoreEnv(key: string, previous: string | undefined): void { + previous === undefined ? delete process.env[key] : (process.env[key] = previous); +} + describe("mergeOllamaLoopbackSystemdOverride", () => { it("writes the OLLAMA_HOST and OLLAMA_CONTEXT_LENGTH lines under [Service] when no drop-in exists", () => { const out = mergeOllamaLoopbackSystemdOverride(""); @@ -97,13 +106,20 @@ describe("mergeOllamaLoopbackSystemdOverride", () => { // loopback override with an actionable warning so the headless install can // continue against Ollama's existing bind. describe("ensureOllamaLoopbackSystemdOverride (#5716 non-interactive sudo)", () => { + // CR thread: isolate NEMOCLAW_NON_INTERACTIVE_SUDO_MODE so an outer shell + // that has set it to `prompt` cannot change which branch of getSudoPrefix + // these tests exercise. Each test in this block targets the `sudo -n` + // branch and must see the env at its default. + let savedSudoMode: string | undefined; + beforeEach(() => { + savedSudoMode = process.env[SUDO_MODE_ENV]; + delete process.env[SUDO_MODE_ENV]; + }); + afterEach(() => { + restoreEnv(SUDO_MODE_ENV, savedSudoMode); + }); + it("skips the override with a warning when sudo -n is unavailable in non-interactive mode", () => { - // CR thread: isolate NEMOCLAW_NON_INTERACTIVE_SUDO_MODE so an outer - // shell that has set it to `prompt` cannot change which branch of - // getSudoPrefix this test exercises. We are specifically testing the - // `sudo -n` branch, so force the env to its default for this test. - const previousSudoMode = process.env.NEMOCLAW_NON_INTERACTIVE_SUDO_MODE; - delete process.env.NEMOCLAW_NON_INTERACTIVE_SUDO_MODE; const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); try { const result = ensureOllamaLoopbackSystemdOverride({ @@ -121,11 +137,6 @@ describe("ensureOllamaLoopbackSystemdOverride (#5716 non-interactive sudo)", () ); } finally { warn.mockRestore(); - if (previousSudoMode === undefined) { - delete process.env.NEMOCLAW_NON_INTERACTIVE_SUDO_MODE; - } else { - process.env.NEMOCLAW_NON_INTERACTIVE_SUDO_MODE = previousSudoMode; - } } }); From 2a59f5c7aaba8e0df84b8ce4ffd16268cae1d0e7 Mon Sep 17 00:00:00 2001 From: Charan Jagwani Date: Mon, 29 Jun 2026 12:10:44 -0700 Subject: [PATCH 4/7] test(onboard): add happy-path sudo test + rename describe (#5716) Ultra advisor PRA-4 (required) called out the missing happy-path test. Every existing case in the #5716 block exercises either the new skip-with-warning gate or an early return on a non-Linux platform. A regression that inverted the new gate's condition (`!hasPasswordlessSudo()` -> `hasPasswordlessSudo()`) would still pass every prior test in the block while silently skipping the loopback override on hosts that have passwordless sudo. Add an explicit happy-path test that pins the gate's behaviour when sudo IS available: the function must NOT emit the "passwordless sudo is not available" warning and must NOT short-circuit via the new skip path. Downstream of the gate, the function continues into the live override path (real runShell against systemd) which then fails because the test host has no real Ollama systemd unit. The test tolerates that downstream failure via a process.exit stub that re-raises a controlled exception; the assertion is scoped to the gate behaviour, not the downstream side effects, which is the ONLY thing PRA-4 asks for. Also rename the describe title from "ensureOllamaLoopbackSystemdOverride (#5716 non-interactive sudo)" to "ensureOllamaLoopbackSystemdOverride non-interactive sudo (#5716)" to satisfy the repository test-title-style check (issue references must land as a final '(#1234)' suffix). This was the sole repository-checks failure on the prior commit. Signed-off-by: Charan Jagwani --- src/lib/onboard/ollama-systemd.test.ts | 43 +++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/lib/onboard/ollama-systemd.test.ts b/src/lib/onboard/ollama-systemd.test.ts index 912606e313e..cacc79bf809 100644 --- a/src/lib/onboard/ollama-systemd.test.ts +++ b/src/lib/onboard/ollama-systemd.test.ts @@ -105,7 +105,7 @@ describe("mergeOllamaLoopbackSystemdOverride", () => { // The new behaviour detects the missing `sudo -n` upfront and skips the // loopback override with an actionable warning so the headless install can // continue against Ollama's existing bind. -describe("ensureOllamaLoopbackSystemdOverride (#5716 non-interactive sudo)", () => { +describe("ensureOllamaLoopbackSystemdOverride non-interactive sudo (#5716)", () => { // CR thread: isolate NEMOCLAW_NON_INTERACTIVE_SUDO_MODE so an outer shell // that has set it to `prompt` cannot change which branch of getSudoPrefix // these tests exercise. Each test in this block targets the `sudo -n` @@ -140,6 +140,47 @@ describe("ensureOllamaLoopbackSystemdOverride (#5716 non-interactive sudo)", () } }); + // Ultra advisor PRA-4: prove the happy path. When passwordless sudo IS + // available, the new skip-with-warning gate must NOT fire and must not + // emit the "passwordless sudo is not available" warning. Without this + // assertion, a regression that inverted the gate condition would still + // pass every other test in this block (they all hit the skip path or + // an early return). The function continues into the live override path + // afterwards, which then fails downstream because we are not on a real + // Linux+systemd+Ollama host; we tolerate that downstream failure (via + // a process.exit stub) because it is OUT OF SCOPE for this test, which + // is solely about the new gate not skipping when sudo IS available. + it("does NOT skip the override when passwordless sudo IS available", () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + const exit = vi.spyOn(process, "exit").mockImplementation((() => { + throw new Error("exit-stub"); + }) as never); + const error = vi.spyOn(console, "error").mockImplementation(() => {}); + try { + try { + ensureOllamaLoopbackSystemdOverride({ + platformImpl: () => "linux", + hasOllamaSystemdUnitImpl: () => true, + isNonInteractive: () => true, + hasPasswordlessSudoImpl: () => true, + }); + } catch (err) { + // Downstream override (real runShell against a non-existent + // systemd unit) throws via our process.exit stub. Anything else + // re-raises so the assertion sees the real failure. + expect((err as Error).message).toBe("exit-stub"); + } + const passwordlessSudoWarnings = warn.mock.calls + .map((c) => c.join(" ")) + .filter((line) => line.includes("passwordless sudo is not available")); + expect(passwordlessSudoWarnings).toHaveLength(0); + } finally { + warn.mockRestore(); + exit.mockRestore(); + error.mockRestore(); + } + }); + it("returns before Linux-only probes when not on Linux", () => { // CR thread: prove the platform gate by making the Linux-only probes // throw if reached. The platformImpl returns "darwin", so the function From 2ba3fd5abe9632de5a6b288369fe66fd5763e4a8 Mon Sep 17 00:00:00 2001 From: Charan Jagwani Date: Mon, 29 Jun 2026 12:16:03 -0700 Subject: [PATCH 5/7] refactor(onboard): extract sudo-skip gate to pure helper (#5716 CR follow-up) CodeRabbit flagged the prior happy-path test as non-hermetic. With hasPasswordlessSudoImpl returning true, the test fell through into the real systemd override path. On a Linux CI runner that has passwordless sudo, the test would write a real /etc/systemd/system drop-in and restart Ollama -- a host-boundary side effect under what is meant to be a pure unit test of the new gate. Extract the gate's decision into a pure exported helper `shouldSkipOllamaLoopbackForMissingSudo(sudoPrefix, hasPasswordlessSudo)` that returns true exactly when the override must be skipped (the sudoPrefix is "sudo -n" AND the passwordless-sudo probe is false). The call site in `ensureOllamaLoopbackSystemdOverride` now reads as a single boolean predicate, the probe creation goes through a small named `defaultHasPasswordlessSudo` helper, and the prior bigger inline lambda is gone. Replace the one happy-path test (which had the process.exit stub gymnastics) with four small predicate tests over the pure helper: the skip branch, the happy-path branch with sudo available, both "sudo" (interactive) cases, and a call-counting test that proves the probe is short-circuited away when sudoPrefix is "sudo". Every branch the production code can take is now covered by an assertion that does not touch the filesystem, systemd, or `runShell`. 12/12 tests pass, Biome clean, no if statements added, typecheck clean. Signed-off-by: Charan Jagwani --- src/lib/onboard/ollama-systemd.test.ts | 67 +++++++++++--------------- src/lib/onboard/ollama-systemd.ts | 39 ++++++++++----- 2 files changed, 56 insertions(+), 50 deletions(-) diff --git a/src/lib/onboard/ollama-systemd.test.ts b/src/lib/onboard/ollama-systemd.test.ts index cacc79bf809..4f332f18804 100644 --- a/src/lib/onboard/ollama-systemd.test.ts +++ b/src/lib/onboard/ollama-systemd.test.ts @@ -8,6 +8,7 @@ import { MIN_AUTODETECTED_OLLAMA_CONTEXT_WINDOW } from "../inference/ollama-runt import { ensureOllamaLoopbackSystemdOverride, mergeOllamaLoopbackSystemdOverride, + shouldSkipOllamaLoopbackForMissingSudo, } from "./ollama-systemd"; const SUDO_MODE_ENV = "NEMOCLAW_NON_INTERACTIVE_SUDO_MODE"; @@ -140,45 +141,33 @@ describe("ensureOllamaLoopbackSystemdOverride non-interactive sudo (#5716)", () } }); - // Ultra advisor PRA-4: prove the happy path. When passwordless sudo IS - // available, the new skip-with-warning gate must NOT fire and must not - // emit the "passwordless sudo is not available" warning. Without this - // assertion, a regression that inverted the gate condition would still - // pass every other test in this block (they all hit the skip path or - // an early return). The function continues into the live override path - // afterwards, which then fails downstream because we are not on a real - // Linux+systemd+Ollama host; we tolerate that downstream failure (via - // a process.exit stub) because it is OUT OF SCOPE for this test, which - // is solely about the new gate not skipping when sudo IS available. - it("does NOT skip the override when passwordless sudo IS available", () => { - const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); - const exit = vi.spyOn(process, "exit").mockImplementation((() => { - throw new Error("exit-stub"); - }) as never); - const error = vi.spyOn(console, "error").mockImplementation(() => {}); - try { - try { - ensureOllamaLoopbackSystemdOverride({ - platformImpl: () => "linux", - hasOllamaSystemdUnitImpl: () => true, - isNonInteractive: () => true, - hasPasswordlessSudoImpl: () => true, - }); - } catch (err) { - // Downstream override (real runShell against a non-existent - // systemd unit) throws via our process.exit stub. Anything else - // re-raises so the assertion sees the real failure. - expect((err as Error).message).toBe("exit-stub"); - } - const passwordlessSudoWarnings = warn.mock.calls - .map((c) => c.join(" ")) - .filter((line) => line.includes("passwordless sudo is not available")); - expect(passwordlessSudoWarnings).toHaveLength(0); - } finally { - warn.mockRestore(); - exit.mockRestore(); - error.mockRestore(); - } + // Ultra advisor PRA-4 / CR follow-up: pin every branch of the new gate + // via the pure `shouldSkipOllamaLoopbackForMissingSudo` decision so the + // happy path is covered without falling through into the real systemd + // override. Earlier round of this test called + // `ensureOllamaLoopbackSystemdOverride` with + // `hasPasswordlessSudoImpl: () => true`; on a Linux CI runner with + // passwordless sudo, that would exec the real `sudo install / daemon- + // reload / restart` host-boundary commands. CodeRabbit flagged that as + // a non-hermetic unit test. The pure helper covers the contract without + // any host-boundary touch. + it("skips when getSudoPrefix is 'sudo -n' AND passwordless sudo is unavailable", () => { + expect(shouldSkipOllamaLoopbackForMissingSudo("sudo -n", () => false)).toBe(true); + }); + + it("does NOT skip when getSudoPrefix is 'sudo -n' but passwordless sudo IS available", () => { + expect(shouldSkipOllamaLoopbackForMissingSudo("sudo -n", () => true)).toBe(false); + }); + + it("does NOT skip when getSudoPrefix is 'sudo' (interactive), regardless of probe", () => { + expect(shouldSkipOllamaLoopbackForMissingSudo("sudo", () => false)).toBe(false); + expect(shouldSkipOllamaLoopbackForMissingSudo("sudo", () => true)).toBe(false); + }); + + it("does not invoke the passwordless-sudo probe when sudoPrefix is 'sudo'", () => { + const probe = vi.fn(() => false); + shouldSkipOllamaLoopbackForMissingSudo("sudo", probe); + expect(probe).not.toHaveBeenCalled(); }); it("returns before Linux-only probes when not on Linux", () => { diff --git a/src/lib/onboard/ollama-systemd.ts b/src/lib/onboard/ollama-systemd.ts index ea628ef8ceb..fd9e0109d6b 100644 --- a/src/lib/onboard/ollama-systemd.ts +++ b/src/lib/onboard/ollama-systemd.ts @@ -64,6 +64,32 @@ function getSudoPrefix(isNonInteractive: boolean): "sudo" | "sudo -n" { return process.stdin.isTTY ? "sudo" : "sudo -n"; } +/** + * Pure decision for the #5716 sudo-skip gate. The override step requires + * root via `sudo -n install / daemon-reload / restart`, so a non-interactive + * run that picked the `sudo -n` prefix and has no passwordless sudo cannot + * apply it. Returns true when the override MUST be skipped. + * + * Exposed so tests can pin both branches (skip + happy path) without falling + * through into the real systemd code and triggering host-boundary side + * effects on a Linux CI runner that happens to have passwordless sudo. + */ +export function shouldSkipOllamaLoopbackForMissingSudo( + sudoPrefix: "sudo" | "sudo -n", + hasPasswordlessSudo: () => boolean, +): boolean { + return sudoPrefix === "sudo -n" && !hasPasswordlessSudo(); +} + +function defaultHasPasswordlessSudo(): boolean { + const probe = runShell("sudo -n true", { + ignoreError: true, + suppressOutput: true, + timeout: 5_000, + }); + return !probe.error && probe.status === 0; +} + function hasOllamaCudaV13Library(): boolean { const ollamaPath = runCapture(["sh", "-c", "command -v ollama"], { ignoreError: true }).trim(); const candidates = [ @@ -149,17 +175,8 @@ export function ensureOllamaLoopbackSystemdOverride( // prompt and the only signal they get is exit 1 with no actionable next // step (#5716 reproduction). const sudoPrefix = getSudoPrefix((options.isNonInteractive ?? isEnvNonInteractive)()); - const hasPasswordlessSudo = - options.hasPasswordlessSudoImpl ?? - (() => { - const probe = runShell("sudo -n true", { - ignoreError: true, - suppressOutput: true, - timeout: 5_000, - }); - return !probe.error && probe.status === 0; - }); - if (sudoPrefix === "sudo -n" && !hasPasswordlessSudo()) { + const hasPasswordlessSudo = options.hasPasswordlessSudoImpl ?? defaultHasPasswordlessSudo; + if (shouldSkipOllamaLoopbackForMissingSudo(sudoPrefix, hasPasswordlessSudo)) { console.warn( " Skipping Ollama systemd loopback override: passwordless sudo is not available " + "on this host. Ollama will keep its current bind; set " + From 890c8870f25c92ca0744c5e9eb964f747ca9287b Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Tue, 30 Jun 2026 01:25:24 -0700 Subject: [PATCH 6/7] fix(onboard): verify Ollama loopback before sudo fallback --- docs/inference/use-local-inference.mdx | 2 + src/lib/onboard/ollama-systemd.test.ts | 60 +++++++++++++--- src/lib/onboard/ollama-systemd.ts | 97 ++++++++++++++++++-------- 3 files changed, 121 insertions(+), 38 deletions(-) diff --git a/docs/inference/use-local-inference.mdx b/docs/inference/use-local-inference.mdx index 415e3dd29a4..406a6937061 100644 --- a/docs/inference/use-local-inference.mdx +++ b/docs/inference/use-local-inference.mdx @@ -125,6 +125,8 @@ Refer to [Tool-Calling Reliability](tool-calling-reliability). On non-WSL hosts, NemoClaw keeps Ollama bound to `127.0.0.1:11434` and starts a token-gated reverse proxy on `0.0.0.0:11435`. The native install/start paths also reset NemoClaw-managed systemd launches to the loopback binding. +When non-interactive Linux onboarding finds an existing systemd Ollama service but cannot use passwordless sudo, it inspects the active listener before continuing. +If every Ollama listener is already loopback-only, onboarding continues without rewriting the systemd drop-in; otherwise it exits before configuring the proxy and asks you to rerun with `NEMOCLAW_NON_INTERACTIVE_SUDO_MODE=prompt` from a terminal or configure passwordless sudo. Containers and other hosts on the local network reach Ollama only through the proxy, which validates a Bearer token before forwarding requests. On that native path, NemoClaw never exposes Ollama without authentication. diff --git a/src/lib/onboard/ollama-systemd.test.ts b/src/lib/onboard/ollama-systemd.test.ts index 4f332f18804..34394a83f06 100644 --- a/src/lib/onboard/ollama-systemd.test.ts +++ b/src/lib/onboard/ollama-systemd.test.ts @@ -8,6 +8,7 @@ import { MIN_AUTODETECTED_OLLAMA_CONTEXT_WINDOW } from "../inference/ollama-runt import { ensureOllamaLoopbackSystemdOverride, mergeOllamaLoopbackSystemdOverride, + ollamaListenersAreLoopbackOnly, shouldSkipOllamaLoopbackForMissingSudo, } from "./ollama-systemd"; @@ -100,12 +101,33 @@ describe("mergeOllamaLoopbackSystemdOverride", () => { }); }); +describe("ollamaListenersAreLoopbackOnly", () => { + it("accepts IPv4, IPv6, and IPv4-mapped loopback listeners", () => { + const output = [ + "LISTEN 0 4096 127.0.0.1:11434 0.0.0.0:*", + "LISTEN 0 4096 [::1]:11434 [::]:*", + "LISTEN 0 4096 [::ffff:127.0.0.2]:11434 [::]:*", + ].join("\n"); + expect(ollamaListenersAreLoopbackOnly(output)).toBe(true); + }); + + it("rejects wildcard or non-loopback Ollama listeners", () => { + expect(ollamaListenersAreLoopbackOnly("LISTEN 0 4096 0.0.0.0:11434 0.0.0.0:*")).toBe(false); + expect(ollamaListenersAreLoopbackOnly("LISTEN 0 4096 [::]:11434 [::]:*")).toBe(false); + expect(ollamaListenersAreLoopbackOnly("LISTEN 0 4096 192.168.1.8:11434 0.0.0.0:*")).toBe(false); + }); + + it("rejects missing or unrelated listeners because no positive proof exists", () => { + expect(ollamaListenersAreLoopbackOnly("")).toBe(false); + expect(ollamaListenersAreLoopbackOnly("LISTEN 0 4096 127.0.0.1:11435 0.0.0.0:*")).toBe(false); + }); +}); + // #5716: on a Linux aarch64 host running `nemoclaw onboard --non-interactive // --yes` without passwordless sudo, the wizard previously aborted with // "Refusing to continue with a potentially non-loopback Ollama bind" mid-flow. -// The new behaviour detects the missing `sudo -n` upfront and skips the -// loopback override with an actionable warning so the headless install can -// continue against Ollama's existing bind. +// The new behaviour detects the missing `sudo -n` upfront and only skips the +// override after positively verifying the active listener is loopback-only. describe("ensureOllamaLoopbackSystemdOverride non-interactive sudo (#5716)", () => { // CR thread: isolate NEMOCLAW_NON_INTERACTIVE_SUDO_MODE so an outer shell // that has set it to `prompt` cannot change which branch of getSudoPrefix @@ -120,7 +142,7 @@ describe("ensureOllamaLoopbackSystemdOverride non-interactive sudo (#5716)", () restoreEnv(SUDO_MODE_ENV, savedSudoMode); }); - it("skips the override with a warning when sudo -n is unavailable in non-interactive mode", () => { + it("continues with a warning when sudo -n is unavailable but the active listener is loopback-only", () => { const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); try { const result = ensureOllamaLoopbackSystemdOverride({ @@ -128,11 +150,10 @@ describe("ensureOllamaLoopbackSystemdOverride non-interactive sudo (#5716)", () hasOllamaSystemdUnitImpl: () => true, isNonInteractive: () => true, hasPasswordlessSudoImpl: () => false, + isOllamaLoopbackOnlyImpl: () => true, }); - expect(result).toBe("not-applicable"); - expect(warn).toHaveBeenCalledWith( - expect.stringContaining("passwordless sudo is not available"), - ); + expect(result).toBe("ready"); + expect(warn).toHaveBeenCalledWith(expect.stringContaining("already loopback-only")); expect(warn).toHaveBeenCalledWith( expect.stringContaining("NEMOCLAW_NON_INTERACTIVE_SUDO_MODE=prompt"), ); @@ -141,6 +162,29 @@ describe("ensureOllamaLoopbackSystemdOverride non-interactive sudo (#5716)", () } }); + it("fails before override commands when sudo is unavailable and loopback-only binding is unverified", () => { + const error = vi.spyOn(console, "error").mockImplementation(() => {}); + const exit = vi.spyOn(process, "exit").mockImplementation(((code?: number) => { + throw new Error(`exit ${code}`); + }) as never); + try { + expect(() => + ensureOllamaLoopbackSystemdOverride({ + platformImpl: () => "linux", + hasOllamaSystemdUnitImpl: () => true, + isNonInteractive: () => true, + hasPasswordlessSudoImpl: () => false, + isOllamaLoopbackOnlyImpl: () => false, + }), + ).toThrow("exit 1"); + expect(error).toHaveBeenCalledWith(expect.stringContaining("could not be verified")); + expect(error).toHaveBeenCalledWith(expect.stringContaining("potentially exposed")); + } finally { + exit.mockRestore(); + error.mockRestore(); + } + }); + // Ultra advisor PRA-4 / CR follow-up: pin every branch of the new gate // via the pure `shouldSkipOllamaLoopbackForMissingSudo` decision so the // happy path is covered without falling through into the real systemd diff --git a/src/lib/onboard/ollama-systemd.ts b/src/lib/onboard/ollama-systemd.ts index fd9e0109d6b..cdff805291d 100644 --- a/src/lib/onboard/ollama-systemd.ts +++ b/src/lib/onboard/ollama-systemd.ts @@ -44,6 +44,11 @@ type OllamaLoopbackSystemdOverrideOptions = { * without needing a real sudoers config. */ hasPasswordlessSudoImpl?: () => boolean; + /** + * Positive runtime proof that the active systemd Ollama listener is bound + * only to loopback. Defaults to a non-privileged `systemctl` + `ss` probe. + */ + isOllamaLoopbackOnlyImpl?: () => boolean; }; function isEnvNonInteractive(): boolean { @@ -90,6 +95,46 @@ function defaultHasPasswordlessSudo(): boolean { return !probe.error && probe.status === 0; } +function parseListenerEndpoint(token: string): { host: string; port: number } | null { + const match = token.match(/^(?:\[([^\]]+)\]|(.+)):(\d+)$/u); + if (!match) return null; + return { + host: String(match[1] ?? match[2]) + .replace(/%[^%]+$/u, "") + .toLowerCase(), + port: Number(match[3]), + }; +} + +/** Return true only when at least one Ollama listener exists and all are loopback-only. */ +export function ollamaListenersAreLoopbackOnly(output: string): boolean { + const hosts = output.split(/\r?\n/u).flatMap((line) => { + const endpoint = parseListenerEndpoint(line.trim().split(/\s+/u)[3] ?? ""); + return endpoint?.port === OLLAMA_PORT ? [endpoint.host] : []; + }); + return ( + hosts.length > 0 && + hosts.every( + (host) => + host === "::1" || + /^127(?:\.\d{1,3}){3}$/u.test(host) || + /^::ffff:127(?:\.\d{1,3}){3}$/u.test(host), + ) + ); +} + +function isActiveOllamaListenerLoopbackOnly(): boolean { + const listeners = runCapture( + [ + "sh", + "-c", + "command -v systemctl >/dev/null && command -v ss >/dev/null && systemctl is-active --quiet ollama.service && ss -H -ltn 2>/dev/null", + ], + { ignoreError: true }, + ); + return ollamaListenersAreLoopbackOnly(listeners); +} + function hasOllamaCudaV13Library(): boolean { const ollamaPath = runCapture(["sh", "-c", "command -v ollama"], { ignoreError: true }).trim(); const candidates = [ @@ -150,40 +195,32 @@ export function ensureOllamaLoopbackSystemdOverride( ).trim(); if (!hasOllamaSystemdUnit) return "not-applicable"; - // #5716: a non-interactive run on a host without passwordless sudo cannot - // apply the loopback override (every `sudo -n ...` below would error). The - // installer used to march into the override and abort mid-flight with - // "Refusing to continue with a potentially non-loopback Ollama bind", - // breaking the headless install contract. Detect the unavailable sudo - // upfront, log the actionable reason, and skip the override instead of - // exiting. Ollama keeps running on its existing bind; the host operator - // can re-run with NEMOCLAW_NON_INTERACTIVE_SUDO_MODE=prompt or grant - // passwordless sudo to restore the loopback hardening. - // - // Trade-off (advisor PRA-2): this fall-through accepts a weaker security - // posture (Ollama may continue on a non-loopback bind) in exchange for - // not breaking the documented headless install contract. The escape - // hatches are deliberate and discoverable: - // - NEMOCLAW_NON_INTERACTIVE_SUDO_MODE=prompt opts back into a real - // sudo prompt when a TTY is attached, restoring the loopback override - // end-to-end. - // - The warning surfaces the exact override knob in stderr so an - // operator scanning logs (or a CI run capturing them) sees how to - // re-tighten without grepping the source. - // Aborting destroy-style with process.exit(1) here is rejected because - // headless pipelines have no recourse: they cannot answer a password - // prompt and the only signal they get is exit 1 with no actionable next - // step (#5716 reproduction). + // #5716: detect missing non-interactive sudo before attempting any override + // command. Continuing is safe only when runtime listener evidence proves + // the active systemd service is already loopback-only. A loopback HTTP + // response is not enough because a wildcard bind responds there too. const sudoPrefix = getSudoPrefix((options.isNonInteractive ?? isEnvNonInteractive)()); const hasPasswordlessSudo = options.hasPasswordlessSudoImpl ?? defaultHasPasswordlessSudo; if (shouldSkipOllamaLoopbackForMissingSudo(sudoPrefix, hasPasswordlessSudo)) { - console.warn( - " Skipping Ollama systemd loopback override: passwordless sudo is not available " + - "on this host. Ollama will keep its current bind; set " + - `${NON_INTERACTIVE_SUDO_MODE_ENV}=prompt to allow a password prompt, ` + - "or configure passwordless sudo to restore loopback hardening.", + const loopbackOnly = + options.isOllamaLoopbackOnlyImpl?.() ?? isActiveOllamaListenerLoopbackOnly(); + if (loopbackOnly) { + console.warn( + " Passwordless sudo is not available; verified that the active Ollama service " + + "is already loopback-only, so onboarding will continue without rewriting its " + + `systemd drop-in. Set ${NON_INTERACTIVE_SUDO_MODE_ENV}=prompt to apply the managed override.`, + ); + return "ready"; + } + console.error( + " Passwordless sudo is not available, and the active Ollama listener could not be " + + "verified as loopback-only.", + ); + console.error( + ` Refusing to continue with a potentially exposed Ollama bind. Set ${NON_INTERACTIVE_SUDO_MODE_ENV}=prompt ` + + "with a terminal, or configure passwordless sudo and rerun onboarding.", ); - return "not-applicable"; + process.exit(1); } console.log(" Configuring Ollama systemd loopback override..."); From 77f30eb92ee078e4d81fb87a0d7f3e6c42748492 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Tue, 30 Jun 2026 01:33:23 -0700 Subject: [PATCH 7/7] docs(inference): clarify Ollama listener proof Signed-off-by: Carlos Villela --- docs/inference/use-local-inference.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/inference/use-local-inference.mdx b/docs/inference/use-local-inference.mdx index 406a6937061..dae9e19fbbd 100644 --- a/docs/inference/use-local-inference.mdx +++ b/docs/inference/use-local-inference.mdx @@ -125,8 +125,10 @@ Refer to [Tool-Calling Reliability](tool-calling-reliability). On non-WSL hosts, NemoClaw keeps Ollama bound to `127.0.0.1:11434` and starts a token-gated reverse proxy on `0.0.0.0:11435`. The native install/start paths also reset NemoClaw-managed systemd launches to the loopback binding. -When non-interactive Linux onboarding finds an existing systemd Ollama service but cannot use passwordless sudo, it inspects the active listener before continuing. -If every Ollama listener is already loopback-only, onboarding continues without rewriting the systemd drop-in; otherwise it exits before configuring the proxy and asks you to rerun with `NEMOCLAW_NON_INTERACTIVE_SUDO_MODE=prompt` from a terminal or configure passwordless sudo. +When non-interactive Linux onboarding finds an existing systemd Ollama service but cannot use passwordless sudo, it checks the service state and active TCP listeners before continuing. +It continues without rewriting the systemd drop-in only when the service is active, `ss` reports at least one listener on port `11434`, and every reported address is loopback-only. +Wildcard and non-loopback addresses, as well as missing or unreadable service or listener evidence, are treated as unverified. +In those cases, onboarding exits before configuring the proxy and asks you to rerun from a terminal with `NEMOCLAW_NON_INTERACTIVE_SUDO_MODE=prompt` or configure passwordless sudo. Containers and other hosts on the local network reach Ollama only through the proxy, which validates a Bearer token before forwarding requests. On that native path, NemoClaw never exposes Ollama without authentication.