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
1 change: 0 additions & 1 deletion bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 to use it. Press Enter to keep NVIDIA Endpoints.");
console.log("");
}

Expand Down
2 changes: 0 additions & 2 deletions bin/nemoclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 7 additions & 5 deletions scripts/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -207,18 +207,20 @@ 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.
# Ref: https://github.com/NVIDIA/NemoClaw/issues/626
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"
Expand Down
58 changes: 58 additions & 0 deletions test/cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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").trim().split("\n").filter(Boolean);
expect(calls).toContain("sandbox get alpha");
expect(calls).toContain("sandbox connect 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", () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-stale-connect-"));
const localBin = path.join(home, "bin");
Expand Down
1 change: 0 additions & 1 deletion test/onboard-selection.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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("Press Enter to keep NVIDIA Endpoints")));
assert.ok(payload.lines.some((line) => line.includes("Cloud models:")));
assert.ok(payload.lines.some((line) => line.includes("Responses API available")));
});
Expand Down
18 changes: 9 additions & 9 deletions test/service-env.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -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");
});

Expand Down Expand Up @@ -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 */ }
Expand Down Expand Up @@ -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);
Expand All @@ -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 */ }
}
Expand Down
Loading