Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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");
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");
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