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
25 changes: 25 additions & 0 deletions nemoclaw/src/lib/subprocess-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,30 @@ const ALLOWED_ENV_PREFIXES = ["LC_", "XDG_", "OPENSHELL_", "GRPC_"];

// ── Public API ─────────────────────────────────────────────────

/**
* When any HTTP proxy is forwarded, ensure localhost and loopback traffic is
* not routed through it. Without this, tools that respect HTTP_PROXY (curl,
* Node.js http, Python requests) will tunnel loopback requests to the user's
* proxy (e.g. Privoxy), which fails with HTTP 500.
* See: #2616
*/
export function withLocalNoProxy(env: Record<string, string>): void {
const hasProxy = env.HTTP_PROXY || env.HTTPS_PROXY || env.http_proxy || env.https_proxy;
if (!hasProxy) return;
for (const key of ["NO_PROXY", "no_proxy"] as const) {
const current = env[key] ?? "";
const parts = current ? current.split(",").map((s) => s.trim()) : [];
let changed = false;
for (const host of ["localhost", "127.0.0.1"]) {
if (!parts.includes(host)) {
parts.push(host);
changed = true;
}
}
if (changed) env[key] = parts.join(",");
}
}

export function buildSubprocessEnv(extra?: Record<string, string>): Record<string, string> {
const env: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
Expand All @@ -59,5 +83,6 @@ export function buildSubprocessEnv(extra?: Record<string, string>): Record<strin
if (extra) {
Object.assign(env, extra);
}
withLocalNoProxy(env);
return env;
}
8 changes: 4 additions & 4 deletions src/lib/onboard-ollama-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const {
getOllamaWarmupCommand,
validateOllamaModel,
} = require("./local-inference");
const { buildSubprocessEnv } = require("./subprocess-env");
const { prompt } = require("./credentials");
const { promptManualModelId } = require("./model-prompts");

Expand Down Expand Up @@ -104,12 +105,11 @@ function spawnOllamaAuthProxy(token: string): number | null {
const child = spawn(process.execPath, [path.join(SCRIPTS, "ollama-auth-proxy.js")], {
detached: true,
stdio: "ignore",
env: {
...process.env,
env: buildSubprocessEnv({
OLLAMA_PROXY_TOKEN: token,
OLLAMA_PROXY_PORT: String(OLLAMA_PROXY_PORT),
OLLAMA_BACKEND_PORT: String(OLLAMA_PORT),
},
}),
});
child.unref();
persistProxyPid(child.pid);
Expand Down Expand Up @@ -275,7 +275,7 @@ function pullOllamaModel(model) {
encoding: "utf8",
stdio: "inherit",
timeout: 600_000,
env: { ...process.env },
env: buildSubprocessEnv(),
});
if (result.signal === "SIGTERM") {
console.error(
Expand Down
127 changes: 127 additions & 0 deletions src/lib/subprocess-env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { withLocalNoProxy } from "../../dist/lib/subprocess-env";

describe("withLocalNoProxy", () => {
it("does nothing when no proxy vars are present", () => {
const env: Record<string, string> = { PATH: "/usr/bin" };
withLocalNoProxy(env);
expect(env).toEqual({ PATH: "/usr/bin" });
});

it("adds localhost and 127.0.0.1 to NO_PROXY and no_proxy when HTTP_PROXY is set and NO_PROXY is absent", () => {
const env: Record<string, string> = { HTTP_PROXY: "http://proxy:8888" };
withLocalNoProxy(env);
expect(env.NO_PROXY).toBe("localhost,127.0.0.1");
expect(env.no_proxy).toBe("localhost,127.0.0.1");
});

it("adds localhost and 127.0.0.1 when HTTPS_PROXY is set", () => {
const env: Record<string, string> = { HTTPS_PROXY: "http://proxy:8888" };
withLocalNoProxy(env);
expect(env.NO_PROXY).toBe("localhost,127.0.0.1");
expect(env.no_proxy).toBe("localhost,127.0.0.1");
});

it("adds localhost and 127.0.0.1 when lowercase http_proxy is set", () => {
const env: Record<string, string> = { http_proxy: "http://proxy:8888" };
withLocalNoProxy(env);
expect(env.NO_PROXY).toBe("localhost,127.0.0.1");
expect(env.no_proxy).toBe("localhost,127.0.0.1");
});

it("appends only the missing loopback entries when NO_PROXY already has localhost", () => {
const env: Record<string, string> = {
HTTP_PROXY: "http://proxy:8888",
NO_PROXY: "example.com,localhost",
no_proxy: "example.com,localhost",
};
withLocalNoProxy(env);
expect(env.NO_PROXY).toBe("example.com,localhost,127.0.0.1");
expect(env.no_proxy).toBe("example.com,localhost,127.0.0.1");
});

it("does not duplicate entries when both loopback hosts are already present", () => {
const env: Record<string, string> = {
HTTP_PROXY: "http://proxy:8888",
NO_PROXY: "localhost,127.0.0.1,corp.internal",
no_proxy: "localhost,127.0.0.1,corp.internal",
};
withLocalNoProxy(env);
expect(env.NO_PROXY).toBe("localhost,127.0.0.1,corp.internal");
expect(env.no_proxy).toBe("localhost,127.0.0.1,corp.internal");
});

it("preserves existing NO_PROXY entries and adds loopback hosts", () => {
const env: Record<string, string> = {
HTTP_PROXY: "http://proxy:8888",
NO_PROXY: "corp.internal,.nvidia.com",
no_proxy: "corp.internal,.nvidia.com",
};
withLocalNoProxy(env);
expect(env.NO_PROXY).toBe("corp.internal,.nvidia.com,localhost,127.0.0.1");
expect(env.no_proxy).toBe("corp.internal,.nvidia.com,localhost,127.0.0.1");
});
});

describe("buildSubprocessEnv NO_PROXY injection", () => {
const originalEnv = process.env;

beforeEach(() => {
vi.resetModules();
process.env = { ...originalEnv };
});

afterEach(() => {
process.env = originalEnv;
});

it("injects NO_PROXY=localhost,127.0.0.1 when HTTP_PROXY is set and NO_PROXY is absent", async () => {
process.env.HTTP_PROXY = "http://proxy.example.com:8888";
delete process.env.NO_PROXY;
delete process.env.no_proxy;

const { buildSubprocessEnv } = await import("../../dist/lib/subprocess-env");
const env = buildSubprocessEnv();
expect(env.NO_PROXY).toBe("localhost,127.0.0.1");
expect(env.no_proxy).toBe("localhost,127.0.0.1");
});

it("augments an existing NO_PROXY to add loopback hosts", async () => {
process.env.HTTP_PROXY = "http://proxy.example.com:8888";
process.env.NO_PROXY = "corp.internal";
process.env.no_proxy = "corp.internal";

const { buildSubprocessEnv } = await import("../../dist/lib/subprocess-env");
const env = buildSubprocessEnv();
expect(env.NO_PROXY).toBe("corp.internal,localhost,127.0.0.1");
expect(env.no_proxy).toBe("corp.internal,localhost,127.0.0.1");
});

it("does not add NO_PROXY when no proxy is set", async () => {
delete process.env.HTTP_PROXY;
delete process.env.HTTPS_PROXY;
delete process.env.http_proxy;
delete process.env.https_proxy;
delete process.env.NO_PROXY;
delete process.env.no_proxy;

const { buildSubprocessEnv } = await import("../../dist/lib/subprocess-env");
const env = buildSubprocessEnv();
expect(env.NO_PROXY).toBeUndefined();
expect(env.no_proxy).toBeUndefined();
});

it("extra vars passed to buildSubprocessEnv override env vars before NO_PROXY injection", async () => {
process.env.HTTP_PROXY = "http://proxy.example.com:8888";
delete process.env.NO_PROXY;
delete process.env.no_proxy;

const { buildSubprocessEnv } = await import("../../dist/lib/subprocess-env");
const env = buildSubprocessEnv({ MY_TOKEN: "abc123" });
expect(env.MY_TOKEN).toBe("abc123");
expect(env.NO_PROXY).toBe("localhost,127.0.0.1");
});
});
25 changes: 25 additions & 0 deletions src/lib/subprocess-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,30 @@ const ALLOWED_ENV_PREFIXES = ["LC_", "XDG_", "OPENSHELL_", "GRPC_"];

// ── Public API ─────────────────────────────────────────────────

/**
* When any HTTP proxy is forwarded, ensure localhost and loopback traffic is
* not routed through it. Without this, tools that respect HTTP_PROXY (curl,
* Node.js http, Python requests) will tunnel loopback requests to the user's
* proxy (e.g. Privoxy), which fails with HTTP 500.
* See: #2616
*/
export function withLocalNoProxy(env: Record<string, string>): void {
const hasProxy = env.HTTP_PROXY || env.HTTPS_PROXY || env.http_proxy || env.https_proxy;
if (!hasProxy) return;
for (const key of ["NO_PROXY", "no_proxy"] as const) {
const current = env[key] ?? "";
const parts = current ? current.split(",").map((s) => s.trim()) : [];
let changed = false;
for (const host of ["localhost", "127.0.0.1"]) {
if (!parts.includes(host)) {
parts.push(host);
changed = true;
}
}
if (changed) env[key] = parts.join(",");
}
}

export function buildSubprocessEnv(extra?: Record<string, string>): Record<string, string> {
const env: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
Expand All @@ -59,5 +83,6 @@ export function buildSubprocessEnv(extra?: Record<string, string>): Record<strin
if (extra) {
Object.assign(env, extra);
}
withLocalNoProxy(env);
return env;
}
Loading