diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 672122da..042da954 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- Added a global `httpProxy` setting that applies as `HTTP_PROXY` and `HTTPS_PROXY` for Pi-managed HTTP clients ([#5790](https://github.com/earendil-works/pi/issues/5790)). - Added `auth.json` API key `env` values so provider-specific environment overrides can be scoped to Pi and propagated to inherited provider configuration ([#5728](https://github.com/earendil-works/pi/issues/5728)). ### Changed diff --git a/packages/coding-agent/docs/settings.md b/packages/coding-agent/docs/settings.md index fccfab06..18c9ad97 100644 --- a/packages/coding-agent/docs/settings.md +++ b/packages/coding-agent/docs/settings.md @@ -69,6 +69,18 @@ Use `/trust` in interactive mode to save a project trust decision for future ses Set `PI_SKIP_VERSION_CHECK=1` to disable the Pi version update check. Use `--offline` or `PI_OFFLINE=1` to disable all startup network operations described here, including update checks, package update checks, and install/update telemetry. +### Network + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `httpProxy` | string | - | HTTP proxy URL applied as `HTTP_PROXY` and `HTTPS_PROXY`. Global setting only. | + +```json +{ + "httpProxy": "http://127.0.0.1:7890" +} +``` + ### Warnings | Setting | Type | Default | Description | diff --git a/packages/coding-agent/src/core/http-dispatcher.ts b/packages/coding-agent/src/core/http-dispatcher.ts index 12ce9fb0..2ec8144e 100644 --- a/packages/coding-agent/src/core/http-dispatcher.ts +++ b/packages/coding-agent/src/core/http-dispatcher.ts @@ -36,6 +36,13 @@ export function formatHttpIdleTimeoutMs(timeoutMs: number): string { return `${timeoutMs / 1000} sec`; } +export function applyHttpProxySettings(httpProxy: string | undefined): void { + const proxy = httpProxy?.trim(); + if (!proxy) return; + process.env.HTTP_PROXY ??= proxy; + process.env.HTTPS_PROXY ??= proxy; +} + export function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS): void { const normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs); if (normalizedTimeoutMs === undefined) { diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index 8acd5998..6b54a187 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -117,6 +117,7 @@ export interface Settings { markdown?: MarkdownSettings; warnings?: WarningSettings; sessionDir?: string; // Custom session storage directory (same format as --session-dir CLI flag) + httpProxy?: string; // Proxy URL applied as HTTP_PROXY and HTTPS_PROXY for Pi-managed HTTP clients httpIdleTimeoutMs?: number; // HTTP header/body idle timeout in milliseconds; 0 disables it websocketConnectTimeoutMs?: number; // WebSocket connect/open handshake timeout in milliseconds; 0 disables it } diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 42ce1ce5..f66040bb 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -26,7 +26,7 @@ import { formatNoModelsAvailableMessage } from "./core/auth-guidance.ts"; import { AuthStorage } from "./core/auth-storage.ts"; import { exportFromFile } from "./core/export-html/index.ts"; import type { ExtensionFactory } from "./core/extensions/types.ts"; -import { configureHttpDispatcher } from "./core/http-dispatcher.ts"; +import { applyHttpProxySettings, configureHttpDispatcher } from "./core/http-dispatcher.ts"; import type { ModelRegistry } from "./core/model-registry.ts"; import { resolveCliModel, resolveModelScope, type ScopedModel } from "./core/model-resolver.ts"; import { restoreStdout, takeOverStdout } from "./core/output-guard.ts"; @@ -466,6 +466,12 @@ export async function main(args: string[], options?: MainOptions) { cleanupWindowsSelfUpdateQuarantine(getPackageDir()); } + const cwd = process.cwd(); + const agentDir = getAgentDir(); + const bootstrapSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false }); + applyHttpProxySettings(bootstrapSettingsManager.getGlobalSettings().httpProxy); + configureHttpDispatcher(); + if (await handlePackageCommand(args, { extensionFactories: options?.extensionFactories })) { const exitCode = process.exitCode ?? 0; if (process.platform === "win32" && exitCode === 0 && args[0] === "update") { @@ -529,11 +535,9 @@ export async function main(args: string[], options?: MainOptions) { validateSessionIdFlags(parsed); // Run migrations (pass cwd for project-local migrations) - const { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(process.cwd()); + const { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(cwd); time("runMigrations"); - const cwd = process.cwd(); - const agentDir = getAgentDir(); const startupSettingsManager = SettingsManager.create(cwd, agentDir); reportDiagnostics(collectSettingsDiagnostics(startupSettingsManager, "startup session lookup")); @@ -726,6 +730,7 @@ export async function main(args: string[], options?: MainOptions) { time("createAgentSessionRuntime"); const { services, session, modelFallbackMessage } = runtime; const { settingsManager, modelRegistry, resourceLoader } = services; + applyHttpProxySettings(settingsManager.getGlobalSettings().httpProxy); configureHttpDispatcher(settingsManager.getHttpIdleTimeoutMs()); if (parsed.help) { diff --git a/packages/coding-agent/test/http-dispatcher.test.ts b/packages/coding-agent/test/http-dispatcher.test.ts new file mode 100644 index 00000000..f70022f1 --- /dev/null +++ b/packages/coding-agent/test/http-dispatcher.test.ts @@ -0,0 +1,53 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { applyHttpProxySettings } from "../src/core/http-dispatcher.ts"; + +const PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY"] as const; + +describe("http proxy settings", () => { + let savedEnv: Record<(typeof PROXY_ENV_KEYS)[number], string | undefined>; + + beforeEach(() => { + savedEnv = Object.fromEntries(PROXY_ENV_KEYS.map((key) => [key, process.env[key]])) as Record< + (typeof PROXY_ENV_KEYS)[number], + string | undefined + >; + for (const key of PROXY_ENV_KEYS) { + delete process.env[key]; + } + }); + + afterEach(() => { + for (const key of PROXY_ENV_KEYS) { + const value = savedEnv[key]; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + }); + + it("applies httpProxy to HTTP_PROXY and HTTPS_PROXY", () => { + applyHttpProxySettings("http://127.0.0.1:7890"); + + expect(process.env.HTTP_PROXY).toBe("http://127.0.0.1:7890"); + expect(process.env.HTTPS_PROXY).toBe("http://127.0.0.1:7890"); + }); + + it("does not override existing proxy env vars", () => { + process.env.HTTP_PROXY = "http://env-http:8080"; + process.env.HTTPS_PROXY = "http://env-https:8080"; + + applyHttpProxySettings("http://settings:7890"); + + expect(process.env.HTTP_PROXY).toBe("http://env-http:8080"); + expect(process.env.HTTPS_PROXY).toBe("http://env-https:8080"); + }); + + it("ignores empty values", () => { + applyHttpProxySettings(" "); + + expect(process.env.HTTP_PROXY).toBeUndefined(); + expect(process.env.HTTPS_PROXY).toBeUndefined(); + }); +});