feat(security): support operator-managed network proxy routing (#70044)

* feat: support operator-managed proxy routing

* docs: add network proxy changelog entry

* fix(proxy): restrict gateway bypass to loopback IPs

* fix(cli): harden container proxy URL checks

* docs(proxy): clarify gateway bypass scope

* docs: remove proxy changelog entry

* fix(proxy): clear startup CI guard failures

* fix(proxy): harden gateway proxy policy parsing

* fix(proxy): honor update shorthand proxy policy

* fix(cli): redact proxy URL suffixes

* test(proxy): keep gateway help off proxy startup

* fix(proxy): keep overlapping lifecycle active

* docs: add proxy changelog entry

---------

Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
This commit is contained in:
Jesse Merhi
2026-04-28 15:20:47 +10:00
committed by GitHub
parent 025081dbc5
commit 2633b14914
36 changed files with 2737 additions and 96 deletions

View File

@@ -575,6 +575,18 @@ describe("buildServiceEnvironment", () => {
expect(env.all_proxy).toBeUndefined();
});
it("forwards proxy URL env fallback for installed gateway services", () => {
const env = buildServiceEnvironment({
env: {
HOME: "/home/user",
OPENCLAW_PROXY_URL: " http://127.0.0.1:3128 ",
},
port: 18789,
});
expect(env.OPENCLAW_PROXY_URL).toBe("http://127.0.0.1:3128");
});
it("omits PATH on Windows so Scheduled Tasks can inherit the current shell path", () => {
const env = buildServiceEnvironment({
env: {
@@ -648,6 +660,17 @@ describe("buildNodeServiceEnvironment", () => {
expect(env.no_proxy).toBeUndefined();
});
it("forwards proxy URL env fallback for installed node services", () => {
const env = buildNodeServiceEnvironment({
env: {
HOME: "/home/user",
OPENCLAW_PROXY_URL: " http://127.0.0.1:3128 ",
},
});
expect(env.OPENCLAW_PROXY_URL).toBe("http://127.0.0.1:3128");
});
it("forwards TMPDIR for node services on Linux", () => {
const env = buildNodeServiceEnvironment({
env: { HOME: "/home/user", TMPDIR: "/tmp/custom" },

View File

@@ -42,11 +42,13 @@ type SharedServiceEnvironmentFields = {
configPath: string | undefined;
tmpDir: string;
minimalPath: string | undefined;
proxyEnv: Record<string, string | undefined>;
nodeCaCerts: string | undefined;
nodeUseSystemCa: string | undefined;
};
export const SERVICE_PROXY_ENV_KEYS = [
"OPENCLAW_PROXY_URL",
"HTTP_PROXY",
"HTTPS_PROXY",
"NO_PROXY",
@@ -57,6 +59,13 @@ export const SERVICE_PROXY_ENV_KEYS = [
"all_proxy",
] as const;
function readServiceProxyEnvironment(
env: Record<string, string | undefined>,
): Record<string, string | undefined> {
const proxyUrl = normalizeOptionalString(env.OPENCLAW_PROXY_URL);
return proxyUrl ? { OPENCLAW_PROXY_URL: proxyUrl } : {};
}
function addNonEmptyDir(dirs: string[], dir: string | undefined): void {
if (dir) {
dirs.push(dir);
@@ -355,6 +364,7 @@ function buildCommonServiceEnvironment(
NODE_USE_SYSTEM_CA: sharedEnv.nodeUseSystemCa,
OPENCLAW_STATE_DIR: sharedEnv.stateDir,
OPENCLAW_CONFIG_PATH: sharedEnv.configPath,
...sharedEnv.proxyEnv,
};
if (sharedEnv.minimalPath) {
serviceEnv.PATH = sharedEnv.minimalPath;
@@ -404,6 +414,7 @@ function resolveSharedServiceEnvironmentFields(
platform === "win32"
? undefined
: buildMinimalServicePath({ env, platform, extraDirs: extraPathDirs }),
proxyEnv: readServiceProxyEnvironment(env),
nodeCaCerts: startupTlsEnv.NODE_EXTRA_CA_CERTS,
nodeUseSystemCa: startupTlsEnv.NODE_USE_SYSTEM_CA,
};