diff --git a/packages/kilo-vscode/src/services/cli-backend/server-manager.ts b/packages/kilo-vscode/src/services/cli-backend/server-manager.ts index 17b4137d7fd..18c176c389a 100644 --- a/packages/kilo-vscode/src/services/cli-backend/server-manager.ts +++ b/packages/kilo-vscode/src/services/cli-backend/server-manager.ts @@ -104,6 +104,12 @@ export class ServerManager { ...(extraCaCerts && { NODE_EXTRA_CA_CERTS: extraCaCerts }), ...(!proxyStrictSSL && { NODE_TLS_REJECT_UNAUTHORIZED: "0" }), ...process.env, + // VS Code's http.proxy / http.noProxy settings are not reflected in + // process.env, so spawned children bypass the user's configured proxy + // and fail behind corporate firewalls. Forward them as the standard + // HTTP_PROXY / HTTPS_PROXY / NO_PROXY env vars that Bun's fetch and + // most HTTP clients already respect. + ...buildProxyEnv(), // Force mimalloc (the allocator Bun ships with) to return freed pages // to the OS immediately instead of retaining them in its arenas. // Without this, Bun.spawn's piped stdio accumulates ~2 MB of native @@ -257,6 +263,72 @@ function stripAnsi(str: string): string { return str.replace(/\x1b\[[0-9;]*m/g, "") } +/** + * Translate VS Code's `http.proxy` / `http.noProxy` / `http.proxySupport` + * settings into the standard proxy env vars, so the spawned CLI honors the + * user's proxy configuration. Returns an empty object when no override is + * needed, so callers can spread unconditionally. + * + * `http.proxySupport: "off"` is VS Code's opt-in way to disable proxy support + * entirely; when set, we explicitly clear the env vars so ambient shell + * HTTP_PROXY/http_proxy doesn't leak into the spawned child. + */ +export function buildProxyEnv(): Record { + const httpConfig = vscode.workspace.getConfiguration("http") + const proxyInfo = httpConfig.inspect("proxy") + const noProxyInfo = httpConfig.inspect("noProxy") + const proxySupport = httpConfig.get("proxySupport") + + if (proxySupport === "off") { + return { HTTP_PROXY: "", HTTPS_PROXY: "", NO_PROXY: "", http_proxy: "", https_proxy: "", no_proxy: "" } + } + + const proxy = httpConfig.get("proxy") + const noProxy = httpConfig.get("noProxy") + const proxySet = + proxyInfo !== undefined && + [ + proxyInfo.globalValue, + proxyInfo.workspaceValue, + proxyInfo.workspaceFolderValue, + proxyInfo.globalLanguageValue, + proxyInfo.workspaceLanguageValue, + proxyInfo.workspaceFolderLanguageValue, + ].some((value) => value !== undefined) + const noProxySet = + noProxyInfo !== undefined && + [ + noProxyInfo.globalValue, + noProxyInfo.workspaceValue, + noProxyInfo.workspaceFolderValue, + noProxyInfo.globalLanguageValue, + noProxyInfo.workspaceLanguageValue, + noProxyInfo.workspaceFolderLanguageValue, + ].some((value) => value !== undefined) + const env: Record = {} + if (proxy && proxy.trim() !== "") { + env.HTTP_PROXY = proxy + env.HTTPS_PROXY = proxy + env.http_proxy = proxy + env.https_proxy = proxy + } + if (proxySet && proxy !== undefined && proxy.trim() === "") { + env.HTTP_PROXY = "" + env.HTTPS_PROXY = "" + env.http_proxy = "" + env.https_proxy = "" + } + if (Array.isArray(noProxy) && noProxy.length > 0) { + env.NO_PROXY = noProxy.join(",") + env.no_proxy = noProxy.join(",") + } + if (noProxySet && Array.isArray(noProxy) && noProxy.length === 0) { + env.NO_PROXY = "" + env.no_proxy = "" + } + return env +} + export function toErrorMessage( error: string, stderrLines: string[], diff --git a/packages/kilo-vscode/tests/unit/server-manager-proxy-env.test.ts b/packages/kilo-vscode/tests/unit/server-manager-proxy-env.test.ts new file mode 100644 index 00000000000..6aac3ae2bcb --- /dev/null +++ b/packages/kilo-vscode/tests/unit/server-manager-proxy-env.test.ts @@ -0,0 +1,138 @@ +import { describe, it, expect, afterEach } from "bun:test" +import * as vscode from "vscode" +import { buildProxyEnv } from "../../src/services/cli-backend/server-manager" + +type Info = { globalValue?: unknown; workspaceValue?: unknown; workspaceFolderValue?: unknown } +type WorkspaceStub = { + getConfiguration: (section?: string) => { get: (key: string) => unknown; inspect: (key: string) => Info } +} + +const workspace = vscode.workspace as unknown as WorkspaceStub +const originalGetConfiguration = workspace.getConfiguration + +function stubHttpConfig(values: { proxy?: unknown; noProxy?: unknown; proxySupport?: unknown }): void { + workspace.getConfiguration = (section?: string) => { + if (section === "http") { + return { + get: (key: string) => { + if (key === "proxy") return values.proxy + if (key === "noProxy") return values.noProxy + if (key === "proxySupport") return values.proxySupport + return undefined + }, + inspect: (key: string) => { + if (key === "proxy" && values.proxy !== undefined) return { workspaceValue: values.proxy } + if (key === "noProxy" && values.noProxy !== undefined) return { workspaceValue: values.noProxy } + if (key === "proxySupport" && values.proxySupport !== undefined) + return { workspaceValue: values.proxySupport } + return {} + }, + } + } + return { get: () => undefined, inspect: () => ({}) } + } +} + +afterEach(() => { + workspace.getConfiguration = originalGetConfiguration +}) + +describe("buildProxyEnv", () => { + it("returns an empty object when neither proxy nor noProxy is configured", () => { + stubHttpConfig({ proxy: undefined, noProxy: undefined }) + + expect(buildProxyEnv()).toEqual({}) + }) + + it("forwards http.proxy as HTTP_PROXY and HTTPS_PROXY", () => { + stubHttpConfig({ proxy: "http://proxy.corp.example:8080" }) + + expect(buildProxyEnv()).toEqual({ + HTTP_PROXY: "http://proxy.corp.example:8080", + HTTPS_PROXY: "http://proxy.corp.example:8080", + http_proxy: "http://proxy.corp.example:8080", + https_proxy: "http://proxy.corp.example:8080", + }) + }) + + it("joins http.noProxy into a comma-separated NO_PROXY value", () => { + stubHttpConfig({ noProxy: ["localhost", "127.0.0.1", "*.internal"] }) + + expect(buildProxyEnv()).toEqual({ + NO_PROXY: "localhost,127.0.0.1,*.internal", + no_proxy: "localhost,127.0.0.1,*.internal", + }) + }) + + it("forwards both proxy and noProxy when both are configured", () => { + stubHttpConfig({ + proxy: "http://proxy.corp.example:8080", + noProxy: ["localhost", "*.internal"], + }) + + expect(buildProxyEnv()).toEqual({ + HTTP_PROXY: "http://proxy.corp.example:8080", + HTTPS_PROXY: "http://proxy.corp.example:8080", + NO_PROXY: "localhost,*.internal", + http_proxy: "http://proxy.corp.example:8080", + https_proxy: "http://proxy.corp.example:8080", + no_proxy: "localhost,*.internal", + }) + }) + + it("clears env vars when http.proxy is only whitespace", () => { + stubHttpConfig({ proxy: " " }) + + expect(buildProxyEnv()).toEqual({ + HTTP_PROXY: "", + HTTPS_PROXY: "", + http_proxy: "", + https_proxy: "", + }) + }) + + it("clears env var when http.noProxy is an empty array", () => { + stubHttpConfig({ noProxy: [] }) + + expect(buildProxyEnv()).toEqual({ + NO_PROXY: "", + no_proxy: "", + }) + }) + + it("ignores a non-array http.noProxy value", () => { + stubHttpConfig({ noProxy: "localhost" }) + + expect(buildProxyEnv()).toEqual({}) + }) + + it("explicitly clears env vars when http.proxySupport is off", () => { + stubHttpConfig({ proxySupport: "off" }) + + expect(buildProxyEnv()).toEqual({ + HTTP_PROXY: "", + HTTPS_PROXY: "", + NO_PROXY: "", + http_proxy: "", + https_proxy: "", + no_proxy: "", + }) + }) + + it("http.proxySupport=off wins over a configured http.proxy/http.noProxy", () => { + stubHttpConfig({ + proxy: "http://proxy.corp.example:8080", + noProxy: ["localhost"], + proxySupport: "off", + }) + + expect(buildProxyEnv()).toEqual({ + HTTP_PROXY: "", + HTTPS_PROXY: "", + NO_PROXY: "", + http_proxy: "", + https_proxy: "", + no_proxy: "", + }) + }) +})