From 11b62d55e7e384cdceffa654f72a74e997cab81c Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 27 Mar 2026 20:13:12 -0400 Subject: [PATCH 1/4] fix: restore routed inference and connect UX --- bin/nemoclaw.js | 2 -- scripts/nemoclaw-start.sh | 12 ++++---- test/cli.test.js | 58 +++++++++++++++++++++++++++++++++++++++ test/service-env.test.js | 18 ++++++------ 4 files changed, 74 insertions(+), 16 deletions(-) diff --git a/bin/nemoclaw.js b/bin/nemoclaw.js index 00a430b1d04..76e9512f5a3 100755 --- a/bin/nemoclaw.js +++ b/bin/nemoclaw.js @@ -562,8 +562,6 @@ function listSandboxes() { async function sandboxConnect(sandboxName) { await ensureLiveSandboxOrExit(sandboxName); - // Ensure port forward is alive before connecting - runOpenshell(["forward", "start", "--background", "18789", sandboxName], { ignoreError: true }); const result = spawnSync(getOpenshellBinary(), ["sandbox", "connect", sandboxName], { stdio: "inherit", cwd: ROOT, diff --git a/scripts/nemoclaw-start.sh b/scripts/nemoclaw-start.sh index e3a3dc21116..9e316b39592 100755 --- a/scripts/nemoclaw-start.sh +++ b/scripts/nemoclaw-start.sh @@ -207,10 +207,12 @@ PYAUTOPAIR # ── Proxy environment ──────────────────────────────────────────── # OpenShell injects HTTP_PROXY/HTTPS_PROXY/NO_PROXY into the sandbox, but its -# NO_PROXY is limited to 127.0.0.1,localhost,::1 — missing inference.local and -# the gateway IP. Without these entries, LLM inference requests are routed -# through the egress proxy instead of going direct, and the proxy gateway IP -# itself gets proxied (potential infinite loop). +# NO_PROXY is limited to 127.0.0.1,localhost,::1 — missing the gateway IP. +# The gateway IP itself must bypass the proxy to avoid proxy loops. +# +# Do NOT add inference.local here. OpenShell intentionally routes that hostname +# through the proxy path; bypassing the proxy forces a direct DNS lookup inside +# the sandbox, which breaks inference.local resolution. # # NEMOCLAW_PROXY_HOST / NEMOCLAW_PROXY_PORT can be overridden at sandbox # creation time if the gateway IP or port changes in a future OpenShell release. @@ -218,7 +220,7 @@ PYAUTOPAIR PROXY_HOST="${NEMOCLAW_PROXY_HOST:-10.200.0.1}" PROXY_PORT="${NEMOCLAW_PROXY_PORT:-3128}" _PROXY_URL="http://${PROXY_HOST}:${PROXY_PORT}" -_NO_PROXY_VAL="localhost,127.0.0.1,::1,inference.local,${PROXY_HOST}" +_NO_PROXY_VAL="localhost,127.0.0.1,::1,${PROXY_HOST}" export HTTP_PROXY="$_PROXY_URL" export HTTPS_PROXY="$_PROXY_URL" export NO_PROXY="$_NO_PROXY_VAL" diff --git a/test/cli.test.js b/test/cli.test.js index aba5f865c95..12064cfcd1b 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -147,6 +147,64 @@ describe("CLI dispatch", () => { expect(fs.readFileSync(markerFile, "utf8")).toContain("logs alpha --follow"); }); + it("connect does not pre-start a duplicate port forward", () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-connect-forward-")); + const localBin = path.join(home, "bin"); + const registryDir = path.join(home, ".nemoclaw"); + const markerFile = path.join(home, "openshell-calls"); + fs.mkdirSync(localBin, { recursive: true }); + fs.mkdirSync(registryDir, { recursive: true }); + fs.writeFileSync( + path.join(registryDir, "sandboxes.json"), + JSON.stringify({ + sandboxes: { + alpha: { + name: "alpha", + model: "test-model", + provider: "nvidia-prod", + gpuEnabled: false, + policies: [], + }, + }, + defaultSandbox: "alpha", + }), + { mode: 0o600 } + ); + fs.writeFileSync( + path.join(localBin, "openshell"), + [ + "#!/usr/bin/env bash", + `marker_file=${JSON.stringify(markerFile)}`, + "printf '%s\\n' \"$*\" >> \"$marker_file\"", + "if [ \"$1\" = \"sandbox\" ] && [ \"$2\" = \"get\" ] && [ \"$3\" = \"alpha\" ]; then", + " echo 'Sandbox:'", + " echo", + " echo ' Id: abc'", + " echo ' Name: alpha'", + " echo ' Namespace: openshell'", + " echo ' Phase: Ready'", + " exit 0", + "fi", + "if [ \"$1\" = \"sandbox\" ] && [ \"$2\" = \"connect\" ] && [ \"$3\" = \"alpha\" ]; then", + " exit 0", + "fi", + "exit 0", + ].join("\n"), + { mode: 0o755 } + ); + + const r = runWithEnv("alpha connect", { + HOME: home, + PATH: `${localBin}:${process.env.PATH || ""}`, + }); + + expect(r.code).toBe(0); + const calls = fs.readFileSync(markerFile, "utf8"); + expect(calls).toContain("sandbox get alpha"); + expect(calls).toContain("sandbox connect alpha"); + expect(calls).not.toContain("forward start --background 18789 alpha"); + }); + it("removes stale registry entries when connect targets a missing live sandbox", () => { const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-stale-connect-")); const localBin = path.join(home, "bin"); diff --git a/test/service-env.test.js b/test/service-env.test.js index 1edd7b15ecf..3d9e39c752d 100644 --- a/test/service-env.test.js +++ b/test/service-env.test.js @@ -161,13 +161,13 @@ describe("service environment", () => { expect(vars.HTTPS_PROXY).toBe("http://10.200.0.1:8080"); }); - it("NO_PROXY includes loopback and inference.local", () => { + it("NO_PROXY includes loopback only, not inference.local", () => { const vars = extractProxyVars(); const noProxy = vars.NO_PROXY.split(","); expect(noProxy).toContain("localhost"); expect(noProxy).toContain("127.0.0.1"); expect(noProxy).toContain("::1"); - expect(noProxy).toContain("inference.local"); + expect(noProxy).not.toContain("inference.local"); }); it("NO_PROXY includes OpenShell gateway IP", () => { @@ -180,7 +180,7 @@ describe("service environment", () => { expect(vars.http_proxy).toBe("http://10.200.0.1:3128"); expect(vars.https_proxy).toBe("http://10.200.0.1:3128"); const noProxy = vars.no_proxy.split(","); - expect(noProxy).toContain("inference.local"); + expect(noProxy).not.toContain("inference.local"); expect(noProxy).toContain("10.200.0.1"); }); @@ -211,11 +211,11 @@ describe("service environment", () => { expect(bashrc).toContain("export HTTP_PROXY="); expect(bashrc).toContain("export HTTPS_PROXY="); expect(bashrc).toContain("export NO_PROXY="); - expect(bashrc).toContain("inference.local"); + expect(bashrc).not.toContain("inference.local"); expect(bashrc).toContain("10.200.0.1"); const profile = readFileSync(join(fakeHome, ".profile"), "utf-8"); - expect(profile).toContain("inference.local"); + expect(profile).not.toContain("inference.local"); } finally { try { unlinkSync(tmpFile); } catch { /* ignore */ } try { execFileSync("rm", ["-rf", fakeHome]); } catch { /* ignore */ } @@ -306,10 +306,10 @@ describe("service environment", () => { "# nemoclaw-proxy-config begin", 'export HTTP_PROXY="http://10.200.0.1:3128"', 'export HTTPS_PROXY="http://10.200.0.1:3128"', - 'export NO_PROXY="localhost,127.0.0.1,::1,inference.local,10.200.0.1"', + 'export NO_PROXY="localhost,127.0.0.1,::1,10.200.0.1"', 'export http_proxy="http://10.200.0.1:3128"', 'export https_proxy="http://10.200.0.1:3128"', - 'export no_proxy="localhost,127.0.0.1,::1,inference.local,10.200.0.1"', + 'export no_proxy="localhost,127.0.0.1,::1,10.200.0.1"', "# nemoclaw-proxy-config end", ].join("\n"); writeFileSync(join(fakeHome, ".bashrc"), bashrcContent); @@ -323,8 +323,8 @@ describe("service environment", () => { 'echo "no_proxy=$no_proxy"', ].join("; ")], { encoding: "utf-8" }).trim(); - expect(out).toContain("NO_PROXY=localhost,127.0.0.1,::1,inference.local,10.200.0.1"); - expect(out).toContain("no_proxy=localhost,127.0.0.1,::1,inference.local,10.200.0.1"); + expect(out).toContain("NO_PROXY=localhost,127.0.0.1,::1,10.200.0.1"); + expect(out).toContain("no_proxy=localhost,127.0.0.1,::1,10.200.0.1"); } finally { try { execFileSync("rm", ["-rf", fakeHome]); } catch { /* ignore */ } } From aa08a9137ea090403a968917cebef970150d7bc9 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 27 Mar 2026 20:16:35 -0400 Subject: [PATCH 2/4] fix: simplify detected local inference hint --- bin/lib/onboard.js | 2 +- test/onboard-selection.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/lib/onboard.js b/bin/lib/onboard.js index e58c64502dc..31cf220e177 100644 --- a/bin/lib/onboard.js +++ b/bin/lib/onboard.js @@ -1955,7 +1955,7 @@ async function setupNim(gpu) { if (ollamaRunning) suggestions.push("Ollama"); if (suggestions.length > 0) { console.log(` Detected local inference option${suggestions.length > 1 ? "s" : ""}: ${suggestions.join(", ")}`); - console.log(" Select one explicitly to use it. Press Enter to keep NVIDIA Endpoints."); + console.log(" Select one explicitly if you want to use it."); console.log(""); } diff --git a/test/onboard-selection.test.js b/test/onboard-selection.test.js index 8fceee219f0..5202bb2136a 100644 --- a/test/onboard-selection.test.js +++ b/test/onboard-selection.test.js @@ -100,7 +100,7 @@ const { setupNim } = require(${onboardPath}); assert.match(payload.messages[0], /Choose \[/); assert.match(payload.messages[1], /Choose model \[1\]/); assert.ok(payload.lines.some((line) => line.includes("Detected local inference option"))); - assert.ok(payload.lines.some((line) => line.includes("Press Enter to keep NVIDIA Endpoints"))); + assert.ok(payload.lines.some((line) => line.includes("Select one explicitly if you want to use it."))); assert.ok(payload.lines.some((line) => line.includes("Cloud models:"))); assert.ok(payload.lines.some((line) => line.includes("Responses API available"))); }); From 8c2749266660528d790ce7f937f634680fd84fc8 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 27 Mar 2026 20:20:36 -0400 Subject: [PATCH 3/4] fix: remove stale local inference hint --- bin/lib/onboard.js | 1 - test/onboard-selection.test.js | 1 - 2 files changed, 2 deletions(-) diff --git a/bin/lib/onboard.js b/bin/lib/onboard.js index 31cf220e177..53069e339ec 100644 --- a/bin/lib/onboard.js +++ b/bin/lib/onboard.js @@ -1955,7 +1955,6 @@ async function setupNim(gpu) { if (ollamaRunning) suggestions.push("Ollama"); if (suggestions.length > 0) { console.log(` Detected local inference option${suggestions.length > 1 ? "s" : ""}: ${suggestions.join(", ")}`); - console.log(" Select one explicitly if you want to use it."); console.log(""); } diff --git a/test/onboard-selection.test.js b/test/onboard-selection.test.js index 5202bb2136a..6cf6e436af2 100644 --- a/test/onboard-selection.test.js +++ b/test/onboard-selection.test.js @@ -100,7 +100,6 @@ const { setupNim } = require(${onboardPath}); assert.match(payload.messages[0], /Choose \[/); assert.match(payload.messages[1], /Choose model \[1\]/); assert.ok(payload.lines.some((line) => line.includes("Detected local inference option"))); - assert.ok(payload.lines.some((line) => line.includes("Select one explicitly if you want to use it."))); assert.ok(payload.lines.some((line) => line.includes("Cloud models:"))); assert.ok(payload.lines.some((line) => line.includes("Responses API available"))); }); From 472a4a8baefe60a7683b39319e1a15e41c56feae Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 27 Mar 2026 20:23:53 -0400 Subject: [PATCH 4/4] test: relax connect forward assertion --- test/cli.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/cli.test.js b/test/cli.test.js index 12064cfcd1b..f255c678113 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -199,10 +199,10 @@ describe("CLI dispatch", () => { }); expect(r.code).toBe(0); - const calls = fs.readFileSync(markerFile, "utf8"); + const calls = fs.readFileSync(markerFile, "utf8").trim().split("\n").filter(Boolean); expect(calls).toContain("sandbox get alpha"); expect(calls).toContain("sandbox connect alpha"); - expect(calls).not.toContain("forward start --background 18789 alpha"); + expect(calls.some((call) => call.startsWith("forward start --background 18789"))).toBe(false); }); it("removes stale registry entries when connect targets a missing live sandbox", () => {