From 847768ccca5201c49ed80dd5a8d1f50f9e27d971 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 8 Aug 2026 18:48:53 +0000
Subject: [PATCH 1/4] Initial plan
From a1bf7fdeab882db96e50592326931ce8cf9e4783 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 8 Aug 2026 19:20:45 +0000
Subject: [PATCH 2/4] feat: add main-process HTTP proxy support and settings UI
Co-authored-by: Power-Maverick <36135520+Power-Maverick@users.noreply.github.com>
---
docs/PROXY_TESTING.md | 62 ++
package.json | 2 +
pnpm-lock.yaml | 6 +
src/common/ipc/channels.ts | 1 +
src/common/types/api.ts | 3 +-
src/common/types/settings.ts | 9 +
src/main/index.ts | 184 +++++-
src/main/managers/proxyManager.ts | 538 ++++++++++++++++++
src/main/managers/settingsManager.ts | 31 +-
src/main/managers/toolRegistryManager.ts | 32 +-
src/main/managers/toolsManager.ts | 7 +-
src/main/preload.ts | 3 +-
src/renderer/modules/settingsManagement.ts | 212 ++++++-
src/renderer/types/index.ts | 3 +-
.../main/managers/settingsManager.test.ts | 24 +
15 files changed, 1075 insertions(+), 42 deletions(-)
create mode 100644 docs/PROXY_TESTING.md
create mode 100644 src/main/managers/proxyManager.ts
diff --git a/docs/PROXY_TESTING.md b/docs/PROXY_TESTING.md
new file mode 100644
index 00000000..6b8b9975
--- /dev/null
+++ b/docs/PROXY_TESTING.md
@@ -0,0 +1,62 @@
+# Proxy Feature Local Testing Guide
+
+## 1) Manual proxy routing (local proxy)
+
+1. Start a local proxy (for example `mitmproxy` or a simple Node proxy) on `127.0.0.1:8080`.
+2. Open ToolBox settings and go to **Network / Proxy**.
+3. Set mode to **Manual proxy** and enter `http://127.0.0.1:8080`.
+4. Click **Test Connection**.
+5. Trigger other Node-side flows (marketplace fetch, tool download).
+6. Verify requests appear in the proxy logs to confirm traffic is routed through the proxy.
+
+## 2) PAC / WPAD auto-detection
+
+1. Serve a local PAC file containing `FindProxyForURL`.
+2. Configure OS proxy auto-config:
+ - **Windows**: Settings → Network & Internet → Proxy → Use setup script.
+ - **macOS**: System Settings → Network → active adapter → Details → Proxies → Automatic Proxy Configuration.
+3. In ToolBox, set mode to **Auto-detect system proxy**.
+4. Restart the app.
+5. Use **Test Connection** and verify traffic routes according to PAC rules.
+6. Confirm behavior in proxy/PAC server logs.
+
+## 3) TLS interception simulation (custom CA bundle)
+
+1. Use `mitmproxy` with TLS interception enabled.
+2. Do **not** rely on OS trust store alone for Node-side calls.
+3. Export mitmproxy CA certificate as PEM (bundle file).
+4. In ToolBox settings (Manual proxy mode), set **Custom CA Bundle** to that PEM file.
+5. Run **Test Connection** and marketplace/tool download operations.
+6. Confirm requests succeed without `UNABLE_TO_VERIFY_LEAF_SIGNATURE`.
+
+## 4) Proxy authentication challenge (407)
+
+1. Start `mitmproxy` with proxy authentication enabled (for example `--proxyauth user:pass`).
+2. Set ToolBox proxy mode to **Manual proxy** with that endpoint.
+3. Trigger a Node-side network call.
+4. Confirm a proxy credentials prompt appears only when the 407 challenge occurs.
+5. Enter credentials and verify the request succeeds.
+
+## 5) NO_PROXY exclusions
+
+1. In manual mode, set a proxy and add exclusions in **No Proxy List** (for example `localhost,127.0.0.1,.internal`).
+2. Run operations that target excluded hosts.
+3. Confirm excluded hosts bypass the proxy (no entries in proxy logs for those hosts).
+
+## 6) Troubleshooting
+
+Check these log areas when routing is not as expected:
+
+- `src/main/managers/proxyManager.ts` log entries:
+ - `[ProxyManager] System proxy auto-detection completed`
+ - `[ProxyManager] System proxy auto-detection failed`
+ - `[ProxyManager] Failed to read custom CA bundle`
+- `src/main/managers/toolRegistryManager.ts` log entries for marketplace/download requests
+- Settings validation via **Test Connection** result text in the settings UI
+
+If requests are still direct:
+
+1. Verify proxy mode and manual URL format.
+2. Verify no-proxy host patterns are not unintentionally matching the target.
+3. Verify CA bundle path points to a readable PEM file.
+4. Restart the app after changing auto-detect mode to refresh resolved proxy state.
diff --git a/package.json b/package.json
index 01d1ad68..c2619db1 100644
--- a/package.json
+++ b/package.json
@@ -81,6 +81,8 @@
"ansi-to-html": "^0.7.2",
"electron-store": "^8.1.0",
"electron-updater": "^6.6.2",
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.1",
"marked": "17.0.3",
"pino": "^9.14.0",
"pino-pretty": "^13.1.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index eb29bf95..c70dfff3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -38,6 +38,12 @@ importers:
electron-updater:
specifier: ^6.6.2
version: 6.7.3
+ http-proxy-agent:
+ specifier: ^5.0.0
+ version: 5.0.0
+ https-proxy-agent:
+ specifier: ^5.0.1
+ version: 5.0.1
marked:
specifier: 17.0.3
version: 17.0.3
diff --git a/src/common/ipc/channels.ts b/src/common/ipc/channels.ts
index 6013f6f8..80cfd094 100644
--- a/src/common/ipc/channels.ts
+++ b/src/common/ipc/channels.ts
@@ -157,6 +157,7 @@ export const UTIL_CHANNELS = {
CHECK_CONNECTIONS: "check-connections",
CHECK_TOOL_DOWNLOAD: "check-tool-download",
CHECK_INTERNET_CONNECTIVITY: "check-internet-connectivity",
+ TEST_PROXY_CONNECTION: "test-proxy-connection",
FETCH_FAVICON: "fetch-favicon",
OPEN_IN_CONNECTION_BROWSER: "open-in-connection-browser",
RESTART_APP: "restart-app",
diff --git a/src/common/types/api.ts b/src/common/types/api.ts
index 0ca69ff8..e0336250 100644
--- a/src/common/types/api.ts
+++ b/src/common/types/api.ts
@@ -7,7 +7,7 @@ import { FileDialogFilter, ModalWindowMessagePayload, ModalWindowOptions, Native
import { CommunityLinksCollection } from "./communityLinks";
import { Connection } from "./connection";
import { DataverseExecuteRequest } from "./dataverse";
-import { CspConsentRecord, LastUsedToolEntry, LastUsedToolUpdate, UserSettings } from "./settings";
+import { CspConsentRecord, LastUsedToolEntry, LastUsedToolUpdate, ProxySettings, UserSettings } from "./settings";
import { Terminal, TerminalOptions } from "./terminal";
import { CapabilityTagEntry, Tool, ToolContext, ToolSettings } from "./tool";
@@ -141,6 +141,7 @@ export interface TroubleshootingAPI {
checkConnections: () => Promise<{ success: boolean; message?: string; connectionCount?: number }>;
checkToolDownload: () => Promise<{ success: boolean; message?: string }>;
checkInternetConnectivity: () => Promise<{ success: boolean; message?: string }>;
+ testProxyConnection: (settings?: ProxySettings) => Promise<{ success: boolean; message?: string }>;
}
/**
diff --git a/src/common/types/settings.ts b/src/common/types/settings.ts
index e4e51900..01e8f732 100644
--- a/src/common/types/settings.ts
+++ b/src/common/types/settings.ts
@@ -29,6 +29,14 @@ export type DeprecatedToolsVisibility = "hide-all" | "show-all" | "show-installe
* Tool display mode options
*/
export type ToolDisplayMode = "standard" | "compact";
+export type ProxyMode = "auto" | "manual" | "none";
+
+export interface ProxySettings {
+ mode: ProxyMode;
+ manualProxyUrl?: string;
+ noProxyList?: string[];
+ caBundlePath?: string;
+}
export const PREVIEW_FEATURE_IDS = {
MCP_SERVER: "mcp-server",
@@ -146,4 +154,5 @@ export interface UserSettings {
previewFeatures?: PreviewFeatureFlags; // Per-feature preview toggles keyed by preview feature ID
marketplaceSources?: MarketplaceSource[]; // Marketplace sources configured for the app
sentryTelemetryConsent?: TelemetryConsentChoice | null; // User consent choice for Sentry warning/error telemetry
+ proxy?: ProxySettings; // Node-side proxy settings for network calls in the main process
}
diff --git a/src/main/index.ts b/src/main/index.ts
index 03904685..7fac2ad3 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -42,6 +42,7 @@ import { InstallIdManager } from "./managers/installIdManager";
import { ModalWindowManager } from "./managers/modalWindowManager";
import { NotificationHistoryWindowManager, NotificationWindowManager } from "./managers/notificationWindowManager";
import { PowerPlatformManager } from "./managers/powerplatformManager";
+import { applyProxyEnvironmentBootstrap, ProxyManager } from "./managers/proxyManager";
import { ProtocolHandlerManager } from "./managers/protocolHandlerManager";
import { SettingsManager } from "./managers/settingsManager";
import { SplitLayoutManager } from "./managers/splitLayoutManager";
@@ -57,6 +58,10 @@ import { McpServerManager } from "./mcp/mcpServer";
import { ActiveToolInfo, buildToolBoxFeedbackUrl, buildToolFeedbackUrl, getEnvironmentDiagnostics, resolveActiveToolInfo } from "./utilities";
import { applyMainSentryConsent } from "./sentryRuntime";
+// Proxy environment variables must be set before any HTTP-capable manager is instantiated.
+// This bootstrap reads persisted settings and applies proxy env vars early in process startup.
+applyProxyEnvironmentBootstrap();
+
// Constants
const MENU_CREATION_DEBOUNCE_MS = 150; // Debounce delay for menu recreation during rapid tool switches
@@ -75,6 +80,7 @@ const isFaviconAllowedHost = (hostname: string): boolean => {
class ToolBoxApp {
private mainWindow: BrowserWindow | null = null;
private settingsManager: SettingsManager;
+ private proxyManager: ProxyManager;
private installIdManager: InstallIdManager;
private connectionsManager: ConnectionsManager;
private toolManager: ToolManager;
@@ -122,6 +128,7 @@ class ToolBoxApp {
try {
this.settingsManager = new SettingsManager();
+ this.proxyManager = new ProxyManager(this.settingsManager);
this.installIdManager = new InstallIdManager(this.settingsManager);
void applyMainSentryConsent(this.settingsManager.getSentryTelemetryConsent(), this.settingsManager.getSentryTelemetryConsent() === "yes" ? this.installIdManager.getInstallId() : undefined);
@@ -135,6 +142,7 @@ class ToolBoxApp {
this.installIdManager,
process.env.AZURE_BLOB_BASE_URL,
this.settingsManager,
+ this.proxyManager,
);
this.browserviewProtocolManager = new BrowserviewProtocolManager(this.toolManager, this.settingsManager);
this.protocolHandlerManager = new ProtocolHandlerManager();
@@ -147,6 +155,7 @@ class ToolBoxApp {
this.toolFilesystemAccessManager = new ToolFileSystemAccessManager();
this.mcpServerManager = new McpServerManager(7339, "127.0.0.1", this.settingsManager, this.toolManager.getRegistryManager(), this.toolManager);
this.mcpServerManager.setConnectionAuthManagers(this.connectionsManager, this.authManager);
+ this.proxyManager.setCredentialPromptHandler((authInfo) => this.promptForProxyCredentials(authInfo.host || "proxy", authInfo.port));
this.trayManager = new TrayManager(
() => this.mainWindow,
() => this.createWindow(),
@@ -184,6 +193,25 @@ class ToolBoxApp {
* Set up event listeners
*/
private setupEventListeners(): void {
+ app.on("login", (event, _webContents, _request, authInfo, callback) => {
+ if (!authInfo.isProxy) {
+ return;
+ }
+
+ event.preventDefault();
+ void this.proxyManager
+ .handleProxyAuthChallenge(authInfo, callback)
+ .then((handled) => {
+ if (!handled) {
+ callback("", "");
+ }
+ })
+ .catch((error) => {
+ logError(error instanceof Error ? error : new Error(String(error)));
+ callback("", "");
+ });
+ });
+
// Listen to tool manager events
this.toolManager.on("tool:loaded", (tool) => {
this.api.emitEvent(ToolBoxEvent.TOOL_LOADED, tool);
@@ -384,6 +412,7 @@ class ToolBoxApp {
ipcMain.removeHandler(UTIL_CHANNELS.OPEN_EXTERNAL);
ipcMain.removeHandler(UTIL_CHANNELS.OPEN_IN_CONNECTION_BROWSER);
ipcMain.removeHandler(UTIL_CHANNELS.RESTART_APP);
+ ipcMain.removeHandler(UTIL_CHANNELS.TEST_PROXY_CONNECTION);
// Filesystem handlers
ipcMain.removeHandler(FILESYSTEM_CHANNELS.READ_TEXT);
@@ -508,6 +537,9 @@ class ToolBoxApp {
ipcMain.handle(SETTINGS_CHANNELS.UPDATE_USER_SETTINGS, async (_, settings) => {
this.settingsManager.updateUserSettings(settings);
+ if (Object.prototype.hasOwnProperty.call(settings, "proxy")) {
+ this.proxyManager.applyProxyEnvironment();
+ }
if (Object.prototype.hasOwnProperty.call(settings, "sentryTelemetryConsent")) {
await applyMainSentryConsent(this.settingsManager.getSentryTelemetryConsent(), this.settingsManager.getSentryTelemetryConsent() === "yes" ? this.installIdManager.getInstallId() : undefined);
}
@@ -520,6 +552,9 @@ class ToolBoxApp {
ipcMain.handle(SETTINGS_CHANNELS.SET_SETTING, async (_, key, value) => {
this.settingsManager.setSetting(key, value);
+ if (key === "proxy") {
+ this.proxyManager.applyProxyEnvironment();
+ }
if (key === "sentryTelemetryConsent") {
await applyMainSentryConsent(this.settingsManager.getSentryTelemetryConsent(), this.settingsManager.getSentryTelemetryConsent() === "yes" ? this.installIdManager.getInstallId() : undefined);
}
@@ -1272,6 +1307,10 @@ class ToolBoxApp {
return await this.checkInternetConnectivity();
});
+ ipcMain.handle(UTIL_CHANNELS.TEST_PROXY_CONNECTION, async (_, settings) => {
+ return await this.proxyManager.testConnection(settings);
+ });
+
// Event history handler
ipcMain.handle(UTIL_CHANNELS.GET_EVENT_HISTORY, (_, limit) => {
return this.api.getEventHistory(limit);
@@ -3074,6 +3113,111 @@ class ToolBoxApp {
this.sendWhatsNewRequest("auto-update", currentVersion);
}
+ private async promptForProxyCredentials(proxyHost: string, proxyPort?: number): Promise<{ username: string; password: string } | null> {
+ if (!this.modalWindowManager) {
+ return null;
+ }
+
+ const modalId = `proxy-auth-${Date.now()}`;
+ const submitChannel = `${modalId}:submit`;
+ const cancelChannel = `${modalId}:cancel`;
+ const escapedHost = proxyHost.replace(/[<>"'&]/g, "");
+ const escapedPort = typeof proxyPort === "number" ? String(proxyPort) : "";
+ const subtitle = escapedPort ? `${escapedHost}:${escapedPort}` : escapedHost;
+
+ const html = `
+
+`;
+
+ return await new Promise((resolve) => {
+ let settled = false;
+ const timeout = setTimeout(() => {
+ if (!settled) {
+ settled = true;
+ ipcMain.removeListener(MODAL_WINDOW_CHANNELS.MESSAGE, onModalMessage);
+ this.modalWindowManager?.hideModal();
+ resolve(null);
+ }
+ }, 120000);
+
+ const finish = (value: { username: string; password: string } | null): void => {
+ if (settled) return;
+ settled = true;
+ clearTimeout(timeout);
+ ipcMain.removeListener(MODAL_WINDOW_CHANNELS.MESSAGE, onModalMessage);
+ this.modalWindowManager?.hideModal();
+ resolve(value);
+ };
+
+ const onModalMessage = (_event: unknown, payload: { channel?: string; data?: unknown }) => {
+ if (!payload || typeof payload.channel !== "string") {
+ return;
+ }
+
+ if (payload.channel === cancelChannel) {
+ finish(null);
+ return;
+ }
+
+ if (payload.channel === submitChannel && payload.data && typeof payload.data === "object") {
+ const data = payload.data as { username?: unknown; password?: unknown };
+ const username = typeof data.username === "string" ? data.username.trim() : "";
+ const password = typeof data.password === "string" ? data.password : "";
+ if (!username || !password) {
+ return;
+ }
+ finish({ username, password });
+ }
+ };
+
+ ipcMain.on(MODAL_WINDOW_CHANNELS.MESSAGE, onModalMessage);
+ this.modalWindowManager?.showModal({
+ id: modalId,
+ html,
+ width: 460,
+ height: 280,
+ resizable: false,
+ alwaysOnTop: true,
+ });
+ });
+ }
+
/**
* Check Supabase connectivity
* Tests if the Supabase API is accessible
@@ -3143,33 +3287,7 @@ class ToolBoxApp {
* Check baseline internet connectivity by reaching GitHub
*/
private async checkInternetConnectivity(): Promise<{ success: boolean; message?: string }> {
- const INTERNET_CHECK_URL = "https://api.github.com/zen";
-
- try {
- const response = await fetch(INTERNET_CHECK_URL, {
- method: "GET",
- headers: { "User-Agent": "PowerPlatformToolBox" },
- });
-
- if (response.ok) {
- logInfo(`[Troubleshooting] Internet connectivity check passed: HTTP ${response.status}`);
- return {
- success: true,
- message: "Internet connectivity verified via GitHub",
- };
- }
- logWarn("[Troubleshooting] Internet connectivity check returned non-OK status");
- return {
- success: false,
- message: `Internet connectivity check failed: HTTP ${response.status}`,
- };
- } catch (error) {
- logError(error as Error);
- return {
- success: false,
- message: error instanceof Error ? error.message : "Network error during internet connectivity check",
- };
- }
+ return this.proxyManager.testConnection();
}
/**
@@ -3196,7 +3314,12 @@ class ToolBoxApp {
await new Promise((resolve, reject) => {
const download = (url: string, redirectDepth = 0) => {
const protocol = url.startsWith("https") ? https : http;
- const request = protocol.get(url, (res) => {
+ const request = protocol.get(
+ url,
+ {
+ agent: this.proxyManager.getAgentForUrl(url),
+ },
+ (res) => {
if ((res.statusCode === 302 || res.statusCode === 301) && res.headers.location) {
if (redirectDepth > 5) {
reject(new Error("Too many redirects while downloading test tool"));
@@ -3226,7 +3349,8 @@ class ToolBoxApp {
}
reject(err);
});
- });
+ },
+ );
request.on("error", (err) => {
reject(new Error(`Network error: ${err.message}`));
@@ -3426,6 +3550,8 @@ class ToolBoxApp {
await app.whenReady();
logCheckpoint("Electron app ready");
+ await this.proxyManager.initialize();
+
// Register protocol handler after app is ready
this.browserviewProtocolManager.registerHandler();
diff --git a/src/main/managers/proxyManager.ts b/src/main/managers/proxyManager.ts
new file mode 100644
index 00000000..fac09265
--- /dev/null
+++ b/src/main/managers/proxyManager.ts
@@ -0,0 +1,538 @@
+import * as http from "http";
+import * as https from "https";
+import * as fs from "fs";
+import Store from "electron-store";
+import { app, session, type AuthInfo } from "electron";
+import { HttpProxyAgent } from "http-proxy-agent";
+import { HttpsProxyAgent } from "https-proxy-agent";
+import { rootCertificates } from "tls";
+import { logInfo, logWarn } from "../../common/logger";
+import type { ProxySettings } from "../../common/types";
+import { EncryptionManager } from "./encryptionManager";
+import { SettingsManager } from "./settingsManager";
+
+interface ProxyAuthEntry {
+ username: string;
+ encryptedPassword: string;
+}
+
+type ProxyAuthStore = Record;
+
+interface EffectiveProxySettings {
+ mode: "manual" | "auto" | "none";
+ proxyUrl?: string;
+ noProxyList: string[];
+ caBundlePath?: string;
+}
+
+type CredentialPromptHandler = (authInfo: AuthInfo) => Promise<{ username: string; password: string } | null>;
+
+function normalizeProxySettings(input?: ProxySettings): ProxySettings {
+ const mode = input?.mode === "manual" || input?.mode === "none" ? input.mode : "auto";
+ const noProxyList = Array.isArray(input?.noProxyList) ? input.noProxyList.filter((entry): entry is string => typeof entry === "string").map((entry) => entry.trim()).filter((entry) => entry.length > 0) : [];
+
+ return {
+ mode,
+ manualProxyUrl: typeof input?.manualProxyUrl === "string" ? input.manualProxyUrl.trim() : "",
+ noProxyList,
+ caBundlePath: typeof input?.caBundlePath === "string" ? input.caBundlePath.trim() : "",
+ };
+}
+
+function normalizeProxyUrl(urlValue: string): string | undefined {
+ if (!urlValue || typeof urlValue !== "string") {
+ return undefined;
+ }
+
+ const trimmedValue = urlValue.trim();
+ if (!trimmedValue) {
+ return undefined;
+ }
+
+ try {
+ const withProtocol = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(trimmedValue) ? trimmedValue : `http://${trimmedValue}`;
+ const parsed = new URL(withProtocol);
+ if (!parsed.hostname) {
+ return undefined;
+ }
+ return parsed.toString();
+ } catch {
+ return undefined;
+ }
+}
+
+function parseResolvedProxyResult(value: string): string | undefined {
+ if (!value || typeof value !== "string") {
+ return undefined;
+ }
+
+ const entries = value
+ .split(";")
+ .map((entry) => entry.trim())
+ .filter((entry) => entry.length > 0);
+
+ for (const entry of entries) {
+ if (/^DIRECT$/i.test(entry)) {
+ continue;
+ }
+
+ const match = entry.match(/^(PROXY|HTTP|HTTPS)\s+(.+)$/i);
+ if (!match) {
+ if (/^SOCKS/i.test(entry)) {
+ logWarn("[ProxyManager] SOCKS proxy detected from system settings, which is not supported in this phase");
+ }
+ continue;
+ }
+
+ const hostPort = match[2].trim();
+ const normalized = normalizeProxyUrl(`http://${hostPort}`);
+ if (normalized) {
+ return normalized;
+ }
+ }
+
+ return undefined;
+}
+
+function matchesNoProxy(hostname: string, noProxyList: string[]): boolean {
+ if (!hostname || noProxyList.length === 0) {
+ return false;
+ }
+
+ return noProxyList.some((rawEntry) => {
+ const entry = rawEntry.trim();
+ if (!entry) {
+ return false;
+ }
+
+ if (entry === "*") {
+ return true;
+ }
+
+ const normalized = entry.startsWith(".") ? entry.slice(1) : entry;
+ return hostname === normalized || hostname.endsWith(`.${normalized}`);
+ });
+}
+
+export function applyProxyEnvironmentBootstrap(): void {
+ try {
+ const store = new Store<{ proxy?: ProxySettings }>({ name: "user-settings" });
+ const proxySettings = normalizeProxySettings(store.get("proxy"));
+ const proxyUrl = proxySettings.mode === "manual" ? normalizeProxyUrl(proxySettings.manualProxyUrl || "") : undefined;
+
+ if (proxySettings.mode === "none" || !proxyUrl) {
+ delete process.env.HTTP_PROXY;
+ delete process.env.HTTPS_PROXY;
+ } else {
+ process.env.HTTP_PROXY = proxyUrl;
+ process.env.HTTPS_PROXY = proxyUrl;
+ }
+
+ if (proxySettings.noProxyList && proxySettings.noProxyList.length > 0) {
+ process.env.NO_PROXY = proxySettings.noProxyList.join(",");
+ } else {
+ delete process.env.NO_PROXY;
+ }
+ } catch (error) {
+ logWarn("[ProxyManager] Failed to apply proxy environment bootstrap", {
+ error: error instanceof Error ? error.message : String(error),
+ });
+ }
+}
+
+export class ProxyManager {
+ private readonly settingsManager: SettingsManager;
+ private readonly encryptionManager: EncryptionManager;
+ private readonly authStore: Store;
+ private readonly agentCache = new Map();
+ private readonly fetchDispatcherCache = new Map();
+ private systemProxyUrl?: string;
+ private credentialPromptHandler?: CredentialPromptHandler;
+
+ constructor(settingsManager: SettingsManager) {
+ this.settingsManager = settingsManager;
+ this.encryptionManager = new EncryptionManager();
+ this.authStore = new Store({
+ name: "proxy-auth",
+ defaults: {},
+ });
+ }
+
+ async initialize(): Promise {
+ const probeTarget = process.env.SUPABASE_URL || "https://api.github.com";
+ await this.detectSystemProxy(probeTarget);
+ this.applyProxyEnvironment();
+ }
+
+ setCredentialPromptHandler(handler: CredentialPromptHandler): void {
+ this.credentialPromptHandler = handler;
+ }
+
+ async detectSystemProxy(targetUrl: string): Promise {
+ try {
+ if (!app.isReady()) {
+ return;
+ }
+
+ const resolved = await session.defaultSession.resolveProxy(targetUrl);
+ const detectedProxyUrl = parseResolvedProxyResult(resolved);
+ this.systemProxyUrl = detectedProxyUrl;
+
+ logInfo("[ProxyManager] System proxy auto-detection completed", {
+ hasProxy: Boolean(detectedProxyUrl),
+ mode: detectedProxyUrl ? "proxy" : "direct",
+ });
+ } catch (error) {
+ logWarn("[ProxyManager] System proxy auto-detection failed", {
+ error: error instanceof Error ? error.message : String(error),
+ });
+ this.systemProxyUrl = undefined;
+ }
+ }
+
+ getEffectiveProxySettings(override?: ProxySettings): EffectiveProxySettings {
+ const proxySettings = normalizeProxySettings(override || this.settingsManager.getSetting("proxy"));
+ const normalizedNoProxyList = proxySettings.noProxyList || [];
+
+ if (proxySettings.mode === "none") {
+ return { mode: "none", noProxyList: normalizedNoProxyList };
+ }
+
+ if (proxySettings.mode === "manual") {
+ const manualProxyUrl = normalizeProxyUrl(proxySettings.manualProxyUrl || "");
+ if (manualProxyUrl) {
+ return {
+ mode: "manual",
+ proxyUrl: manualProxyUrl,
+ noProxyList: normalizedNoProxyList,
+ caBundlePath: proxySettings.caBundlePath || undefined,
+ };
+ }
+
+ return {
+ mode: "none",
+ noProxyList: normalizedNoProxyList,
+ };
+ }
+
+ if (this.systemProxyUrl) {
+ return {
+ mode: "auto",
+ proxyUrl: this.systemProxyUrl,
+ noProxyList: normalizedNoProxyList,
+ caBundlePath: proxySettings.caBundlePath || undefined,
+ };
+ }
+
+ return {
+ mode: "none",
+ noProxyList: normalizedNoProxyList,
+ caBundlePath: proxySettings.caBundlePath || undefined,
+ };
+ }
+
+ applyProxyEnvironment(override?: ProxySettings): void {
+ const effective = this.getEffectiveProxySettings(override);
+
+ if (effective.mode === "none" || !effective.proxyUrl) {
+ delete process.env.HTTP_PROXY;
+ delete process.env.HTTPS_PROXY;
+ } else {
+ process.env.HTTP_PROXY = effective.proxyUrl;
+ process.env.HTTPS_PROXY = effective.proxyUrl;
+ }
+
+ if (effective.noProxyList.length > 0) {
+ process.env.NO_PROXY = effective.noProxyList.join(",");
+ } else {
+ delete process.env.NO_PROXY;
+ }
+
+ this.agentCache.clear();
+ this.fetchDispatcherCache.clear();
+ }
+
+ getProxyEnvironmentVariables(): Record {
+ const output: Record = {};
+ if (process.env.HTTP_PROXY) output.HTTP_PROXY = process.env.HTTP_PROXY;
+ if (process.env.HTTPS_PROXY) output.HTTPS_PROXY = process.env.HTTPS_PROXY;
+ if (process.env.NO_PROXY) output.NO_PROXY = process.env.NO_PROXY;
+ return output;
+ }
+
+ getAgentForUrl(targetUrl: string, override?: ProxySettings): http.RequestOptions["agent"] | undefined {
+ const effective = this.getEffectiveProxySettings(override);
+ if (!effective.proxyUrl) {
+ return this.createDirectAgent(targetUrl, effective.caBundlePath);
+ }
+
+ try {
+ const parsedTarget = new URL(targetUrl);
+ if (matchesNoProxy(parsedTarget.hostname, effective.noProxyList)) {
+ return this.createDirectAgent(targetUrl, effective.caBundlePath);
+ }
+
+ const cacheKey = `${parsedTarget.protocol}:${effective.proxyUrl}:${effective.caBundlePath || ""}`;
+ const cached = this.agentCache.get(cacheKey);
+ if (cached) {
+ return cached;
+ }
+
+ const ca = this.resolveCombinedCaBundle(effective.caBundlePath);
+ const proxyUrlObject = new URL(effective.proxyUrl);
+ const proxyAgentOptions = {
+ protocol: proxyUrlObject.protocol,
+ host: proxyUrlObject.hostname,
+ port: proxyUrlObject.port || undefined,
+ auth: proxyUrlObject.username ? `${decodeURIComponent(proxyUrlObject.username)}:${decodeURIComponent(proxyUrlObject.password)}` : undefined,
+ ca: ca || undefined,
+ };
+ const agent = (parsedTarget.protocol === "http:" ? new HttpProxyAgent(proxyAgentOptions) : new HttpsProxyAgent(proxyAgentOptions)) as unknown as http.Agent;
+
+ this.agentCache.set(cacheKey, agent);
+ return agent;
+ } catch (error) {
+ logWarn("[ProxyManager] Failed to resolve proxy agent for URL", {
+ error: error instanceof Error ? error.message : String(error),
+ });
+ return undefined;
+ }
+ }
+
+ createProxyAwareFetch(override?: ProxySettings): typeof fetch {
+ return (input: Parameters[0], init?: Parameters[1]) => {
+ const targetUrl = this.resolveFetchTargetUrl(input);
+ const requestInit = { ...(init || {}) } as RequestInit & {
+ agent?: http.RequestOptions["agent"];
+ dispatcher?: RequestInit["dispatcher"];
+ };
+ if (!targetUrl) {
+ return fetch(input, requestInit as Parameters[1]);
+ }
+
+ const effective = this.getEffectiveProxySettings(override);
+ const dispatcher = this.getFetchDispatcher(targetUrl, effective);
+ if (dispatcher) {
+ requestInit.dispatcher = dispatcher;
+ return fetch(input, requestInit as Parameters[1]);
+ }
+
+ const fallbackAgent = this.getAgentForUrl(targetUrl, override);
+ if (fallbackAgent) {
+ requestInit.agent = fallbackAgent;
+ }
+ return fetch(input, requestInit as Parameters[1]);
+ };
+ }
+
+ async testConnection(override?: ProxySettings): Promise<{ success: boolean; message: string }> {
+ const testUrl = process.env.SUPABASE_URL || "https://api.github.com/zen";
+ const effective = this.getEffectiveProxySettings(override);
+
+ return new Promise((resolve) => {
+ const protocol = testUrl.startsWith("https:") ? https : http;
+ const agent = this.getAgentForUrl(testUrl, override);
+ const ca = this.resolveCombinedCaBundle(effective.caBundlePath);
+ const request = protocol.request(
+ testUrl,
+ {
+ method: "GET",
+ timeout: 10000,
+ headers: { "User-Agent": "PowerPlatformToolBox" },
+ agent,
+ ca: ca || undefined,
+ },
+ (response) => {
+ response.resume();
+ if (response.statusCode && response.statusCode >= 200 && response.statusCode < 400) {
+ resolve({ success: true, message: `Connection successful (HTTP ${response.statusCode})` });
+ return;
+ }
+
+ resolve({
+ success: false,
+ message: `Connection failed with HTTP ${response.statusCode || "unknown"}`,
+ });
+ },
+ );
+
+ request.on("error", (error) => {
+ resolve({
+ success: false,
+ message: error.message,
+ });
+ });
+
+ request.on("timeout", () => {
+ request.destroy();
+ resolve({
+ success: false,
+ message: "Connection test timed out after 10 seconds",
+ });
+ });
+
+ request.end();
+ });
+ }
+
+ async handleProxyAuthChallenge(authInfo: AuthInfo, callback: (username: string, password: string) => void): Promise {
+ if (!authInfo.isProxy) {
+ return false;
+ }
+
+ const credentialsKey = this.getProxyCredentialKey(authInfo.host, authInfo.port);
+ const existingCredentials = this.authStore.get(credentialsKey);
+ if (existingCredentials?.username && existingCredentials.encryptedPassword) {
+ callback(existingCredentials.username, this.encryptionManager.decrypt(existingCredentials.encryptedPassword));
+ return true;
+ }
+
+ if (!this.credentialPromptHandler) {
+ return false;
+ }
+
+ const prompted = await this.credentialPromptHandler(authInfo);
+ if (!prompted || !prompted.username || !prompted.password) {
+ return false;
+ }
+
+ this.authStore.set(credentialsKey, {
+ username: prompted.username,
+ encryptedPassword: this.encryptionManager.encrypt(prompted.password),
+ });
+
+ callback(prompted.username, prompted.password);
+ return true;
+ }
+
+ private createDirectAgent(targetUrl: string, caBundlePath?: string): http.RequestOptions["agent"] | undefined {
+ try {
+ const parsedUrl = new URL(targetUrl);
+ if (parsedUrl.protocol !== "https:") {
+ return undefined;
+ }
+
+ const ca = this.resolveCombinedCaBundle(caBundlePath);
+ if (!ca) {
+ return undefined;
+ }
+
+ const cacheKey = `direct:${caBundlePath || ""}`;
+ const cached = this.agentCache.get(cacheKey);
+ if (cached) {
+ return cached;
+ }
+
+ const agent = new https.Agent({ ca });
+ this.agentCache.set(cacheKey, agent);
+ return agent;
+ } catch {
+ return undefined;
+ }
+ }
+
+ private resolveCombinedCaBundle(caBundlePath?: string): string | undefined {
+ if (!caBundlePath) {
+ return undefined;
+ }
+
+ try {
+ if (!fs.existsSync(caBundlePath)) {
+ return undefined;
+ }
+
+ const customCa = fs.readFileSync(caBundlePath, "utf-8");
+ if (!customCa.trim()) {
+ return undefined;
+ }
+
+ return `${rootCertificates.join("\n")}\n${customCa}`;
+ } catch (error) {
+ logWarn("[ProxyManager] Failed to read custom CA bundle", {
+ error: error instanceof Error ? error.message : String(error),
+ });
+ return undefined;
+ }
+ }
+
+ private getProxyCredentialKey(host?: string, port?: number): string {
+ return `${host || "unknown"}:${typeof port === "number" ? port : "0"}`;
+ }
+
+ private resolveFetchTargetUrl(input: Parameters[0]): string | undefined {
+ if (typeof input === "string") {
+ return input;
+ }
+
+ if (input instanceof URL) {
+ return input.toString();
+ }
+
+ if ("url" in input && typeof input.url === "string") {
+ return input.url;
+ }
+
+ return undefined;
+ }
+
+ private getFetchDispatcher(targetUrl: string, effective: EffectiveProxySettings): RequestInit["dispatcher"] | undefined {
+ let targetHost = "";
+ try {
+ targetHost = new URL(targetUrl).hostname;
+ } catch {
+ return undefined;
+ }
+
+ if (!effective.proxyUrl && !effective.caBundlePath) {
+ return undefined;
+ }
+
+ if (matchesNoProxy(targetHost, effective.noProxyList)) {
+ return undefined;
+ }
+
+ let undici:
+ | {
+ ProxyAgent?: new (options: { uri: string; requestTls?: { ca?: string } }) => unknown;
+ Agent?: new (options: { connect?: { ca?: string } }) => unknown;
+ }
+ | undefined;
+ try {
+ // undici is used by Node fetch; configure a dispatcher so fetch traffic (Supabase)
+ // follows proxy settings and optional CA bundle.
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ undici = require("undici");
+ } catch {
+ undici = undefined;
+ }
+
+ if (!undici) {
+ return undefined;
+ }
+
+ const ca = this.resolveCombinedCaBundle(effective.caBundlePath);
+ const cacheKey = `${effective.proxyUrl || "direct"}|${effective.caBundlePath || ""}`;
+ const cached = this.fetchDispatcherCache.get(cacheKey);
+ if (cached) {
+ return cached;
+ }
+
+ let dispatcher: RequestInit["dispatcher"] | undefined;
+ if (effective.proxyUrl && undici.ProxyAgent) {
+ dispatcher = new undici.ProxyAgent({
+ uri: effective.proxyUrl,
+ requestTls: ca ? { ca } : undefined,
+ }) as RequestInit["dispatcher"];
+ } else if (ca && undici.Agent) {
+ dispatcher = new undici.Agent({
+ connect: { ca },
+ }) as RequestInit["dispatcher"];
+ }
+
+ if (dispatcher) {
+ this.fetchDispatcherCache.set(cacheKey, dispatcher);
+ }
+
+ return dispatcher;
+ }
+}
diff --git a/src/main/managers/settingsManager.ts b/src/main/managers/settingsManager.ts
index 2b8d9e23..57397a4e 100644
--- a/src/main/managers/settingsManager.ts
+++ b/src/main/managers/settingsManager.ts
@@ -1,6 +1,6 @@
import { randomBytes } from "crypto";
import Store from "electron-store";
-import { CspConsentRecord, LastUsedToolConnectionInfo, LastUsedToolEntry, LastUsedToolUpdate, MarketplaceSource, TelemetryConsentChoice, ToolSettings, UserSettings } from "../../common/types";
+import { CspConsentRecord, LastUsedToolConnectionInfo, LastUsedToolEntry, LastUsedToolUpdate, MarketplaceSource, ProxySettings, TelemetryConsentChoice, ToolSettings, UserSettings } from "../../common/types";
import { normalizeTelemetryConsent } from "../../common/telemetryConsent";
import { buildPreviewFeatureFlags } from "../../common/types/settings";
import { AZURE_BLOB_BASE_URL } from "../constants";
@@ -44,6 +44,12 @@ export class SettingsManager {
enablePreviewFeatures: false, // Show preview/experimental features in the UI
previewFeatures: buildPreviewFeatureFlags(), // Per-feature preview toggles
marketplaceSources: this.getDefaultMarketplaceSources(),
+ proxy: {
+ mode: "auto",
+ manualProxyUrl: "",
+ noProxyList: [],
+ caBundlePath: "",
+ },
},
});
@@ -122,6 +128,18 @@ export class SettingsManager {
return this.normalizeMarketplaceSources(storedSources as MarketplaceSource[] | undefined);
}
+ private normalizeProxySettings(proxy?: ProxySettings): ProxySettings {
+ const normalizedMode = proxy?.mode === "manual" || proxy?.mode === "none" ? proxy.mode : "auto";
+ const normalizedNoProxyList = Array.isArray(proxy?.noProxyList) ? proxy?.noProxyList.filter((entry): entry is string => typeof entry === "string").map((entry) => entry.trim()).filter((entry) => entry.length > 0) : [];
+
+ return {
+ mode: normalizedMode,
+ manualProxyUrl: typeof proxy?.manualProxyUrl === "string" ? proxy.manualProxyUrl.trim() : "",
+ noProxyList: normalizedNoProxyList,
+ caBundlePath: typeof proxy?.caBundlePath === "string" ? proxy.caBundlePath.trim() : "",
+ };
+ }
+
private persistMarketplaceSources(sources: MarketplaceSource[]): void {
this.store.set("marketplaceSources", this.normalizeMarketplaceSources(sources));
}
@@ -163,6 +181,7 @@ export class SettingsManager {
...settings,
marketplaceSources: this.getMarketplaceSourcesFromStore(),
sentryTelemetryConsent: normalizeTelemetryConsent(settings.sentryTelemetryConsent),
+ proxy: this.normalizeProxySettings(settings.proxy as ProxySettings | undefined),
};
}
@@ -181,6 +200,11 @@ export class SettingsManager {
return;
}
+ if (key === "proxy") {
+ this.store.set("proxy", this.normalizeProxySettings(value as ProxySettings | undefined));
+ return;
+ }
+
this.store.set(key as keyof UserSettings, value);
});
}
@@ -201,6 +225,11 @@ export class SettingsManager {
return;
}
+ if (key === "proxy") {
+ this.store.set("proxy", this.normalizeProxySettings(value as ProxySettings | undefined));
+ return;
+ }
+
this.store.set(key, value);
}
diff --git a/src/main/managers/toolRegistryManager.ts b/src/main/managers/toolRegistryManager.ts
index d9780493..2d3b3dab 100644
--- a/src/main/managers/toolRegistryManager.ts
+++ b/src/main/managers/toolRegistryManager.ts
@@ -12,6 +12,7 @@ import { CapabilityTagEntry, CommunityLinksCollection, CommunityLinksGroup, Comm
import { AZURE_BLOB_BASE_URL, SUPABASE_ANON_KEY, SUPABASE_URL } from "../constants";
import { loadOfflineMockRegistryTools, OfflineMockRegistryTool } from "../utilities/mockRegistry";
import { InstallIdManager } from "./installIdManager";
+import { ProxyManager } from "./proxyManager";
/**
* Supabase database types
@@ -148,6 +149,7 @@ export class ToolRegistryManager extends EventEmitter {
private installIdManager: InstallIdManager | null = null;
private azureBlobBaseUrl: string;
private settingsManager: { getMarketplaceSources(): MarketplaceSource[] } | null = null;
+ private proxyManager?: ProxyManager;
// Registry fetch de-duping + caching
private registryFetchInFlight: Promise | null = null;
@@ -175,6 +177,7 @@ export class ToolRegistryManager extends EventEmitter {
installIdManager?: InstallIdManager,
azureBlobBaseUrl?: string,
settingsManager?: { getMarketplaceSources(): MarketplaceSource[] },
+ proxyManager?: ProxyManager,
) {
super();
this.toolsDirectory = toolsDirectory;
@@ -182,6 +185,7 @@ export class ToolRegistryManager extends EventEmitter {
this.installIdManager = installIdManager || null;
this.azureBlobBaseUrl = azureBlobBaseUrl || AZURE_BLOB_BASE_URL;
this.settingsManager = settingsManager || null;
+ this.proxyManager = proxyManager;
// Initialize Supabase client
const url = supabaseUrl || SUPABASE_URL;
@@ -194,7 +198,8 @@ export class ToolRegistryManager extends EventEmitter {
this.useLocalFallback = true;
} else {
logInfo("[ToolRegistry] Initializing Supabase client");
- this.supabase = createClient(url, key);
+ const proxyFetch = this.proxyManager?.createProxyAwareFetch();
+ this.supabase = proxyFetch ? createClient(url, key, { global: { fetch: proxyFetch } }) : createClient(url, key);
}
this.ensureToolsDirectory();
@@ -209,6 +214,18 @@ export class ToolRegistryManager extends EventEmitter {
}
}
+ private getRequestOptions(targetUrl: string, timeoutMs?: number): http.RequestOptions {
+ const options: http.RequestOptions = {
+ agent: this.proxyManager?.getAgentForUrl(targetUrl),
+ };
+
+ if (typeof timeoutMs === "number") {
+ options.timeout = timeoutMs;
+ }
+
+ return options;
+ }
+
/**
* Fetch the tool registry from Supabase database or local fallback
*/
@@ -299,7 +316,7 @@ export class ToolRegistryManager extends EventEmitter {
const rawJson = await new Promise((resolve, reject) => {
const protocol = registryUrl.startsWith("https") ? https : http;
protocol
- .get(registryUrl, (res) => {
+ .get(registryUrl, this.getRequestOptions(registryUrl), (res) => {
if (res.statusCode !== 200) {
reject(new Error(`Marketplace registry request failed: HTTP ${res.statusCode} for ${registryUrl}`));
return;
@@ -481,7 +498,7 @@ export class ToolRegistryManager extends EventEmitter {
const rawJson = await new Promise((resolve, reject) => {
const protocol = registryUrl.startsWith("https") ? https : http;
protocol
- .get(registryUrl, (res) => {
+ .get(registryUrl, this.getRequestOptions(registryUrl), (res) => {
if (res.statusCode !== 200) {
reject(new Error(`Azure Blob registry request failed: HTTP ${res.statusCode} for ${registryUrl}`));
return;
@@ -659,7 +676,7 @@ export class ToolRegistryManager extends EventEmitter {
const protocol = tool.downloadUrl.startsWith("https") ? https : http;
protocol
- .get(tool.downloadUrl, (res) => {
+ .get(tool.downloadUrl, this.getRequestOptions(tool.downloadUrl), (res) => {
if (res.statusCode === 302 || res.statusCode === 301) {
// Handle redirects
const redirectUrl = res.headers.location;
@@ -667,7 +684,7 @@ export class ToolRegistryManager extends EventEmitter {
logInfo(`[ToolRegistry] Following redirect to ${redirectUrl}`);
const redirectProtocol = redirectUrl.startsWith("https") ? https : http;
redirectProtocol
- .get(redirectUrl, (redirectRes) => {
+ .get(redirectUrl, this.getRequestOptions(redirectUrl), (redirectRes) => {
this.handleDownloadResponse(redirectRes, downloadPath, toolPath, resolve, reject);
})
.on("error", reject);
@@ -1091,7 +1108,8 @@ export class ToolRegistryManager extends EventEmitter {
* Update Supabase credentials (if needed)
*/
updateSupabaseClient(url: string, key: string): void {
- this.supabase = createClient(url, key);
+ const proxyFetch = this.proxyManager?.createProxyAwareFetch();
+ this.supabase = proxyFetch ? createClient(url, key, { global: { fetch: proxyFetch } }) : createClient(url, key);
this.useLocalFallback = false;
logInfo(`[ToolRegistry] Supabase client updated`);
}
@@ -1346,7 +1364,7 @@ export class ToolRegistryManager extends EventEmitter {
const rawJson = await new Promise((resolve, reject) => {
https
- .get(url, { timeout: 10000 }, (res) => {
+ .get(url, this.getRequestOptions(url, 10000), (res) => {
if (res.statusCode === 404) {
// Package not found on npm — no beta available
resolve("{}");
diff --git a/src/main/managers/toolsManager.ts b/src/main/managers/toolsManager.ts
index 976b37cc..b8102150 100644
--- a/src/main/managers/toolsManager.ts
+++ b/src/main/managers/toolsManager.ts
@@ -6,6 +6,7 @@ import { pathToFileURL } from "url";
import { logError, logInfo, logWarn } from "../../common/logger";
import { CapabilityTagEntry, CommunityLinksCollection, CspExceptions, MarketplaceSource, Tool, ToolFeatures, ToolManifest } from "../../common/types";
import { InstallIdManager } from "./installIdManager";
+import { ProxyManager } from "./proxyManager";
import { ToolRegistryManager } from "./toolRegistryManager";
import { VersionManager } from "./versionManager";
@@ -37,6 +38,7 @@ export class ToolManager extends EventEmitter {
private registryManager: ToolRegistryManager;
private analyticsCache: Map = new Map();
private updatingTools: Set = new Set();
+ private proxyManager?: ProxyManager;
constructor(
toolsDirectory: string,
@@ -45,10 +47,12 @@ export class ToolManager extends EventEmitter {
installIdManager?: InstallIdManager,
azureBlobBaseUrl?: string,
settingsManager?: { getMarketplaceSources(): MarketplaceSource[] },
+ proxyManager?: ProxyManager,
) {
super();
this.toolsDirectory = toolsDirectory;
- this.registryManager = new ToolRegistryManager(toolsDirectory, supabaseUrl, supabaseKey, installIdManager, azureBlobBaseUrl, settingsManager);
+ this.proxyManager = proxyManager;
+ this.registryManager = new ToolRegistryManager(toolsDirectory, supabaseUrl, supabaseKey, installIdManager, azureBlobBaseUrl, settingsManager, proxyManager);
this.ensureToolsDirectory();
// Forward registry events
@@ -435,6 +439,7 @@ export class ToolManager extends EventEmitter {
return {
...process.env,
+ ...this.proxyManager?.getProxyEnvironmentVariables(),
PATH: [...new Set(paths)].join(path.delimiter),
};
}
diff --git a/src/main/preload.ts b/src/main/preload.ts
index 0408540a..63e1da76 100644
--- a/src/main/preload.ts
+++ b/src/main/preload.ts
@@ -14,7 +14,7 @@ import {
UPDATE_CHANNELS,
UTIL_CHANNELS,
} from "../common/ipc/channels";
-import type { EntityRelatedMetadataPath, EntityRelatedMetadataResponse, LastUsedToolUpdate } from "../common/types";
+import type { EntityRelatedMetadataPath, EntityRelatedMetadataResponse, LastUsedToolUpdate, ProxySettings } from "../common/types";
/**
* Preload script that exposes safe APIs to the renderer process
@@ -197,6 +197,7 @@ contextBridge.exposeInMainWorld("toolboxAPI", {
checkConnections: () => ipcRenderer.invoke(UTIL_CHANNELS.CHECK_CONNECTIONS),
checkToolDownload: () => ipcRenderer.invoke(UTIL_CHANNELS.CHECK_TOOL_DOWNLOAD),
checkInternetConnectivity: () => ipcRenderer.invoke(UTIL_CHANNELS.CHECK_INTERNET_CONNECTIVITY),
+ testProxyConnection: (settings?: ProxySettings) => ipcRenderer.invoke(UTIL_CHANNELS.TEST_PROXY_CONNECTION, settings),
},
// FileSystem namespace - filesystem operations
diff --git a/src/renderer/modules/settingsManagement.ts b/src/renderer/modules/settingsManagement.ts
index d6a945b5..1f89ee60 100644
--- a/src/renderer/modules/settingsManagement.ts
+++ b/src/renderer/modules/settingsManagement.ts
@@ -4,7 +4,7 @@
*/
import { logError } from "../../common/logger";
-import { buildPreviewFeatureFlags, type MarketplaceSource } from "../../common/types";
+import { buildPreviewFeatureFlags, type MarketplaceSource, type ProxySettings } from "../../common/types";
import { normalizeTelemetryConsent } from "../../common/telemetryConsent";
import {
DEFAULT_CATEGORY_COLOR_THICKNESS,
@@ -166,6 +166,68 @@ function collectMarketplaceSourcesFromSettingsPanel(): MarketplaceSource[] {
return sources;
}
+function normalizeProxyMode(value: string | undefined): ProxySettings["mode"] {
+ if (value === "manual" || value === "none") {
+ return value;
+ }
+ return "auto";
+}
+
+function parseNoProxyList(value: string): string[] {
+ return value
+ .split(",")
+ .map((entry) => entry.trim())
+ .filter((entry) => entry.length > 0);
+}
+
+function formatNoProxyList(entries?: string[]): string {
+ if (!Array.isArray(entries) || entries.length === 0) {
+ return "";
+ }
+ return entries.join(", ");
+}
+
+function collectProxySettingsFromPanel(): ProxySettings {
+ const modeSelect = document.getElementById("sidebar-proxy-mode-select") as HTMLSelectElement | null;
+ const manualProxyInput = document.getElementById("sidebar-proxy-manual-url-input") as HTMLInputElement | null;
+ const noProxyInput = document.getElementById("sidebar-proxy-no-proxy-input") as HTMLInputElement | null;
+ const caBundlePathInput = document.getElementById("sidebar-proxy-ca-bundle-path-input") as HTMLInputElement | null;
+
+ return {
+ mode: normalizeProxyMode(modeSelect?.value),
+ manualProxyUrl: (manualProxyInput?.value || "").trim(),
+ noProxyList: parseNoProxyList(noProxyInput?.value || ""),
+ caBundlePath: (caBundlePathInput?.value || "").trim(),
+ };
+}
+
+function areProxySettingsEqual(left?: ProxySettings, right?: ProxySettings): boolean {
+ const normalizedLeft = {
+ mode: normalizeProxyMode(left?.mode),
+ manualProxyUrl: (left?.manualProxyUrl || "").trim(),
+ noProxyList: Array.isArray(left?.noProxyList) ? left.noProxyList.map((entry) => entry.trim()).filter((entry) => entry.length > 0) : [],
+ caBundlePath: (left?.caBundlePath || "").trim(),
+ };
+ const normalizedRight = {
+ mode: normalizeProxyMode(right?.mode),
+ manualProxyUrl: (right?.manualProxyUrl || "").trim(),
+ noProxyList: Array.isArray(right?.noProxyList) ? right.noProxyList.map((entry) => entry.trim()).filter((entry) => entry.length > 0) : [],
+ caBundlePath: (right?.caBundlePath || "").trim(),
+ };
+
+ return JSON.stringify(normalizedLeft) === JSON.stringify(normalizedRight);
+}
+
+function updateProxyModeUi(): void {
+ const modeSelect = document.getElementById("sidebar-proxy-mode-select") as HTMLSelectElement | null;
+ const manualSettings = document.getElementById("sidebar-proxy-manual-settings") as HTMLElement | null;
+ if (!modeSelect || !manualSettings) {
+ return;
+ }
+
+ manualSettings.style.display = modeSelect.value === "manual" ? "block" : "none";
+}
+
function shouldPromptRestartForMarketplaceSourceChanges(previousSources: MarketplaceSource[], nextSources: MarketplaceSource[]): boolean {
const previousBuiltInEnabled = previousSources.find((source) => source.id === "builtin-pptb")?.enabled ?? true;
const nextBuiltInEnabled = nextSources.find((source) => source.id === "builtin-pptb")?.enabled ?? true;
@@ -318,6 +380,10 @@ export async function loadSettings(): Promise {
const showEnvironmentColorCheck = document.getElementById("sidebar-show-environment-color-check") as HTMLInputElement | null;
const categoryColorThicknessInput = document.getElementById("sidebar-category-color-thickness") as HTMLInputElement | null;
const environmentColorThicknessInput = document.getElementById("sidebar-environment-color-thickness") as HTMLInputElement | null;
+ const proxyModeSelect = document.getElementById("sidebar-proxy-mode-select") as HTMLSelectElement | null;
+ const proxyManualUrlInput = document.getElementById("sidebar-proxy-manual-url-input") as HTMLInputElement | null;
+ const proxyNoProxyInput = document.getElementById("sidebar-proxy-no-proxy-input") as HTMLInputElement | null;
+ const proxyCaBundlePathInput = document.getElementById("sidebar-proxy-ca-bundle-path-input") as HTMLInputElement | null;
const sentryTelemetryConsentSelect = document.getElementById("sidebar-sentry-telemetry-consent-select") as HTMLSelectElement | null;
const marketplaceBuiltinCheck = document.getElementById("sidebar-marketplace-builtin-check") as HTMLInputElement | null;
const marketplaceSourcesList = document.getElementById("marketplace-sources-list") as HTMLElement | null;
@@ -326,6 +392,12 @@ export async function loadSettings(): Promise {
const settings = await window.toolboxAPI.getUserSettings();
const previewFeatures = normalizePreviewFeatureFlags(settings);
const sentryTelemetryConsent = normalizeTelemetryConsent(settings.sentryTelemetryConsent);
+ const proxySettings: ProxySettings = {
+ mode: normalizeProxyMode(settings.proxy?.mode),
+ manualProxyUrl: settings.proxy?.manualProxyUrl || "",
+ noProxyList: settings.proxy?.noProxyList || [],
+ caBundlePath: settings.proxy?.caBundlePath || "",
+ };
// Store original settings for change detection
originalSettings = {
@@ -345,6 +417,7 @@ export async function loadSettings(): Promise {
previewFeatures,
marketplaceSources: settings.marketplaceSources ?? [],
sentryTelemetryConsent,
+ proxy: proxySettings,
};
themeSelect.value = settings.theme;
@@ -382,6 +455,19 @@ export async function loadSettings(): Promise {
if (sentryTelemetryConsentSelect) {
sentryTelemetryConsentSelect.value = sentryTelemetryConsent ?? "";
}
+ if (proxyModeSelect) {
+ proxyModeSelect.value = proxySettings.mode;
+ }
+ if (proxyManualUrlInput) {
+ proxyManualUrlInput.value = proxySettings.manualProxyUrl || "";
+ }
+ if (proxyNoProxyInput) {
+ proxyNoProxyInput.value = formatNoProxyList(proxySettings.noProxyList);
+ }
+ if (proxyCaBundlePathInput) {
+ proxyCaBundlePathInput.value = proxySettings.caBundlePath || "";
+ }
+ updateProxyModeUi();
getPreviewFeatureDefinitions().forEach((feature) => {
const checkbox = document.getElementById(getPreviewFeatureCheckboxId(feature.id)) as HTMLInputElement | null;
if (checkbox) {
@@ -457,6 +543,7 @@ export async function saveSettings(): Promise {
const enablePreviewFeatures = Object.values(previewFeatures).some((enabled) => enabled === true);
const marketplaceSources = collectMarketplaceSourcesFromSettingsPanel();
const sentryTelemetryConsent = normalizeTelemetryConsent(sentryTelemetryConsentSelect?.value);
+ const proxy = collectProxySettingsFromPanel();
const currentSettings = {
theme: themeSelect.value,
@@ -475,6 +562,7 @@ export async function saveSettings(): Promise {
previewFeatures,
marketplaceSources,
sentryTelemetryConsent,
+ proxy,
};
const requiresRestartForMarketplaceSources = shouldPromptRestartForMarketplaceSourceChanges(originalSettings.marketplaceSources ?? [], currentSettings.marketplaceSources);
@@ -530,6 +618,9 @@ export async function saveSettings(): Promise {
if ((currentSettings.sentryTelemetryConsent ?? null) !== (originalSettings.sentryTelemetryConsent ?? null)) {
changedSettings.sentryTelemetryConsent = currentSettings.sentryTelemetryConsent;
}
+ if (!areProxySettingsEqual(currentSettings.proxy, originalSettings.proxy)) {
+ changedSettings.proxy = currentSettings.proxy;
+ }
// Only save and emit event if something changed
if (Object.keys(changedSettings).length > 0) {
@@ -601,6 +692,10 @@ function hasUnsavedChanges(): boolean {
const showEnvironmentColorCheck = document.getElementById("sidebar-show-environment-color-check") as HTMLInputElement | null;
const categoryColorThicknessInput = document.getElementById("sidebar-category-color-thickness") as HTMLInputElement | null;
const environmentColorThicknessInput = document.getElementById("sidebar-environment-color-thickness") as HTMLInputElement | null;
+ const proxyModeSelect = document.getElementById("sidebar-proxy-mode-select") as HTMLSelectElement | null;
+ const proxyManualUrlInput = document.getElementById("sidebar-proxy-manual-url-input") as HTMLInputElement | null;
+ const proxyNoProxyInput = document.getElementById("sidebar-proxy-no-proxy-input") as HTMLInputElement | null;
+ const proxyCaBundlePathInput = document.getElementById("sidebar-proxy-ca-bundle-path-input") as HTMLInputElement | null;
// If the DOM elements aren't present the settings panel isn't rendered — no unsaved changes
if (!themeSelect || !autoUpdateCheck || !showDebugMenuCheck || !deprecatedToolsSelect || !toolDisplayModeSelect || !terminalFontSelect) {
@@ -638,6 +733,11 @@ function hasUnsavedChanges(): boolean {
const currentMarketplaceSources = collectMarketplaceSourcesFromSettingsPanel();
if (JSON.stringify(currentMarketplaceSources) !== JSON.stringify(originalSettings.marketplaceSources ?? [])) return true;
+ if (proxyModeSelect || proxyManualUrlInput || proxyNoProxyInput || proxyCaBundlePathInput) {
+ const currentProxy = collectProxySettingsFromPanel();
+ if (!areProxySettingsEqual(currentProxy, originalSettings.proxy)) return true;
+ }
+
return false;
}
@@ -824,6 +924,70 @@ export function renderSettingsContent(panel: HTMLElement): void {
+
+ Network / Proxy
+
+
+
+
+
Configure proxy usage for Node-side network operations in the main process.
+
+
+
+
+
+
+
+
+
+
+
Test Connection
+
Run a lightweight request using the currently configured proxy settings.
+
+
+
+
+
+
+
+
Updates
@@ -923,6 +1087,52 @@ export function renderSettingsContent(panel: HTMLElement): void {
});
}
+ const proxyModeSelect = panel.querySelector("#sidebar-proxy-mode-select") as HTMLSelectElement | null;
+ if (proxyModeSelect) {
+ proxyModeSelect.addEventListener("change", () => {
+ updateProxyModeUi();
+ });
+ }
+
+ const proxyCaBundleSelectBtn = panel.querySelector("#sidebar-proxy-ca-bundle-select-btn") as HTMLButtonElement | null;
+ const proxyCaBundlePathInput = panel.querySelector("#sidebar-proxy-ca-bundle-path-input") as HTMLInputElement | null;
+ if (proxyCaBundleSelectBtn && proxyCaBundlePathInput) {
+ proxyCaBundleSelectBtn.addEventListener("click", async () => {
+ const selectedPath = await window.toolboxAPI.fileSystem.selectPath({
+ type: "file",
+ title: "Select custom CA bundle",
+ buttonLabel: "Select",
+ filters: [
+ { name: "Certificate files", extensions: ["pem", "crt", "cer"] },
+ { name: "All files", extensions: ["*"] },
+ ],
+ });
+
+ if (selectedPath) {
+ proxyCaBundlePathInput.value = selectedPath;
+ }
+ });
+ }
+
+ const proxyTestButton = panel.querySelector("#sidebar-proxy-test-btn") as HTMLButtonElement | null;
+ const proxyTestStatus = panel.querySelector("#sidebar-proxy-test-status") as HTMLElement | null;
+ if (proxyTestButton && proxyTestStatus) {
+ proxyTestButton.addEventListener("click", async () => {
+ proxyTestButton.disabled = true;
+ proxyTestStatus.style.display = "block";
+ proxyTestStatus.textContent = "Testing connection...";
+
+ try {
+ const result = await window.toolboxAPI.troubleshooting.testProxyConnection(collectProxySettingsFromPanel());
+ proxyTestStatus.textContent = result.success ? `✅ ${result.message || "Connection successful"}` : `❌ ${result.message || "Connection failed"}`;
+ } catch (error) {
+ proxyTestStatus.textContent = `❌ ${error instanceof Error ? error.message : "Connection test failed"}`;
+ } finally {
+ proxyTestButton.disabled = false;
+ }
+ });
+ }
+
// Wire up marketplace source add/remove actions
const addMarketplaceSourceBtn = panel.querySelector("#sidebar-add-marketplace-source-btn") as HTMLButtonElement | null;
const marketplaceSourcesList = panel.querySelector("#marketplace-sources-list") as HTMLElement | null;
diff --git a/src/renderer/types/index.ts b/src/renderer/types/index.ts
index 867c6930..690b2522 100644
--- a/src/renderer/types/index.ts
+++ b/src/renderer/types/index.ts
@@ -2,7 +2,7 @@
* Renderer-specific type definitions
*/
-import type { MarketplaceSource, PreviewFeatureFlags, TelemetryConsentChoice } from "../../common/types";
+import type { MarketplaceSource, PreviewFeatureFlags, ProxySettings, TelemetryConsentChoice } from "../../common/types";
/**
* Interface for an open tool instance
@@ -68,6 +68,7 @@ export interface SettingsState {
previewFeatures?: PreviewFeatureFlags;
marketplaceSources?: MarketplaceSource[];
sentryTelemetryConsent?: TelemetryConsentChoice | null;
+ proxy?: ProxySettings;
}
/**
diff --git a/tests/unit/main/managers/settingsManager.test.ts b/tests/unit/main/managers/settingsManager.test.ts
index c7e3e5c3..831d9a8c 100644
--- a/tests/unit/main/managers/settingsManager.test.ts
+++ b/tests/unit/main/managers/settingsManager.test.ts
@@ -24,6 +24,12 @@ describe("SettingsManager", () => {
expect(settings.installedTools).toEqual([]);
expect(settings.favoriteTools).toEqual([]);
expect(settings.sentryTelemetryConsent).toBeNull();
+ expect(settings.proxy).toEqual({
+ mode: "auto",
+ manualProxyUrl: "",
+ noProxyList: [],
+ caBundlePath: "",
+ });
});
});
@@ -41,6 +47,24 @@ describe("SettingsManager", () => {
expect(manager.getSetting("theme")).toBe("light");
expect(manager.getSetting("autoUpdate")).toBe(false);
});
+
+ it("normalizes proxy settings when updated", () => {
+ manager.updateUserSettings({
+ proxy: {
+ mode: "manual",
+ manualProxyUrl: " http://proxy.local:8080 ",
+ noProxyList: [" localhost ", "", "127.0.0.1"],
+ caBundlePath: " /tmp/custom.pem ",
+ },
+ });
+
+ expect(manager.getUserSettings().proxy).toEqual({
+ mode: "manual",
+ manualProxyUrl: "http://proxy.local:8080",
+ noProxyList: ["localhost", "127.0.0.1"],
+ caBundlePath: "/tmp/custom.pem",
+ });
+ });
});
describe("setSetting / getSetting", () => {
From 1555164b8c3623dd10d9fcda5617bfca947e0449 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 10 Aug 2026 03:24:27 +0000
Subject: [PATCH 3/4] Fix proxy test status handling and expand local proxy
setup guide
Co-authored-by: Power-Maverick <36135520+Power-Maverick@users.noreply.github.com>
---
docs/PROXY_TESTING.md | 70 ++++++++++++++++++++++++-------
src/main/managers/proxyManager.ts | 13 +++++-
2 files changed, 67 insertions(+), 16 deletions(-)
diff --git a/docs/PROXY_TESTING.md b/docs/PROXY_TESTING.md
index 6b8b9975..f43c5693 100644
--- a/docs/PROXY_TESTING.md
+++ b/docs/PROXY_TESTING.md
@@ -2,36 +2,69 @@
## 1) Manual proxy routing (local proxy)
-1. Start a local proxy (for example `mitmproxy` or a simple Node proxy) on `127.0.0.1:8080`.
-2. Open ToolBox settings and go to **Network / Proxy**.
-3. Set mode to **Manual proxy** and enter `http://127.0.0.1:8080`.
+### Prerequisites
+
+1. Install mitmproxy:
+ - **Windows**: `winget install mitmproxy.mitmproxy`
+ - **macOS**: `brew install mitmproxy`
+ - **Linux**: `python3 -m pip install --user mitmproxy`
+2. Verify install: `mitmproxy --version`
+
+### Start local proxy
+
+1. Start mitmproxy on port `8888`:
+ - `mitmproxy --listen-host 127.0.0.1 --listen-port 8888`
+2. Keep this terminal open while testing.
+
+### Configure ToolBox
+
+1. Open ToolBox settings and go to **Network / Proxy**.
+2. Set mode to **Manual proxy**.
+3. Set proxy URL to `http://127.0.0.1:8888`.
4. Click **Test Connection**.
-5. Trigger other Node-side flows (marketplace fetch, tool download).
-6. Verify requests appear in the proxy logs to confirm traffic is routed through the proxy.
+5. Trigger Node-side flows (marketplace refresh, tool download).
+6. In mitmproxy, verify requests from ToolBox are listed.
## 2) PAC / WPAD auto-detection
-1. Serve a local PAC file containing `FindProxyForURL`.
+1. Create a PAC file (for example `/tmp/proxy.pac`) with:
+ ```javascript
+ function FindProxyForURL(url, host) {
+ if (host === "localhost" || shExpMatch(host, "*.local")) {
+ return "DIRECT";
+ }
+ return "PROXY 127.0.0.1:8888; DIRECT";
+ }
+ ```
+2. Serve PAC file:
+ - `python3 -m http.server 9000 --directory /tmp`
+3. Start mitmproxy:
+ - `mitmproxy --listen-host 127.0.0.1 --listen-port 8888`
2. Configure OS proxy auto-config:
- **Windows**: Settings → Network & Internet → Proxy → Use setup script.
- **macOS**: System Settings → Network → active adapter → Details → Proxies → Automatic Proxy Configuration.
-3. In ToolBox, set mode to **Auto-detect system proxy**.
-4. Restart the app.
-5. Use **Test Connection** and verify traffic routes according to PAC rules.
-6. Confirm behavior in proxy/PAC server logs.
+ - PAC URL: `http://127.0.0.1:9000/proxy.pac`
+4. In ToolBox, set mode to **Auto-detect system proxy**.
+5. Restart the app.
+6. Use **Test Connection** and verify traffic routes according to PAC rules.
+7. Confirm behavior in proxy/PAC server logs.
## 3) TLS interception simulation (custom CA bundle)
-1. Use `mitmproxy` with TLS interception enabled.
+1. Use `mitmproxy` with TLS interception enabled (`mitmproxy --listen-host 127.0.0.1 --listen-port 8888`).
2. Do **not** rely on OS trust store alone for Node-side calls.
-3. Export mitmproxy CA certificate as PEM (bundle file).
-4. In ToolBox settings (Manual proxy mode), set **Custom CA Bundle** to that PEM file.
+3. Export mitmproxy CA bundle:
+ - Default path is usually `~/.mitmproxy/mitmproxy-ca-cert.pem`.
+4. In ToolBox settings (Manual proxy mode), set:
+ - Proxy URL: `http://127.0.0.1:8888`
+ - **Custom CA Bundle**: full path to `mitmproxy-ca-cert.pem`
5. Run **Test Connection** and marketplace/tool download operations.
6. Confirm requests succeed without `UNABLE_TO_VERIFY_LEAF_SIGNATURE`.
## 4) Proxy authentication challenge (407)
-1. Start `mitmproxy` with proxy authentication enabled (for example `--proxyauth user:pass`).
+1. Start mitmproxy with authentication:
+ - `mitmproxy --listen-host 127.0.0.1 --listen-port 8888 --proxyauth user:pass`
2. Set ToolBox proxy mode to **Manual proxy** with that endpoint.
3. Trigger a Node-side network call.
4. Confirm a proxy credentials prompt appears only when the 407 challenge occurs.
@@ -60,3 +93,12 @@ If requests are still direct:
2. Verify no-proxy host patterns are not unintentionally matching the target.
3. Verify CA bundle path points to a readable PEM file.
4. Restart the app after changing auto-detect mode to refresh resolved proxy state.
+
+If proxy test shows errors:
+
+1. `connect ECONNREFUSED 127.0.0.1:8888`
+ - Nothing is listening on `127.0.0.1:8888`.
+ - Start mitmproxy, or update ToolBox manual proxy URL to the actual port.
+2. `Connection reached endpoint (HTTP 404)`
+ - Network path is working, but that endpoint returned 404.
+ - This still confirms proxy routing and connectivity.
diff --git a/src/main/managers/proxyManager.ts b/src/main/managers/proxyManager.ts
index fac09265..9c283a61 100644
--- a/src/main/managers/proxyManager.ts
+++ b/src/main/managers/proxyManager.ts
@@ -326,7 +326,8 @@ export class ProxyManager {
}
async testConnection(override?: ProxySettings): Promise<{ success: boolean; message: string }> {
- const testUrl = process.env.SUPABASE_URL || "https://api.github.com/zen";
+ const configuredSupabaseUrl = process.env.SUPABASE_URL?.trim();
+ const testUrl = configuredSupabaseUrl ? new URL("/auth/v1/health", configuredSupabaseUrl).toString() : "https://api.github.com/zen";
const effective = this.getEffectiveProxySettings(override);
return new Promise((resolve) => {
@@ -344,11 +345,19 @@ export class ProxyManager {
},
(response) => {
response.resume();
- if (response.statusCode && response.statusCode >= 200 && response.statusCode < 400) {
+ if (response.statusCode && response.statusCode >= 100 && response.statusCode < 400) {
resolve({ success: true, message: `Connection successful (HTTP ${response.statusCode})` });
return;
}
+ if (response.statusCode && response.statusCode >= 400 && response.statusCode < 600) {
+ resolve({
+ success: true,
+ message: `Connection reached endpoint (HTTP ${response.statusCode}). Proxy route is working.`,
+ });
+ return;
+ }
+
resolve({
success: false,
message: `Connection failed with HTTP ${response.statusCode || "unknown"}`,
From 42299b64f2d3d0a762d7bd3a4abc8249b8a9d1a0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 10 Aug 2026 03:27:13 +0000
Subject: [PATCH 4/4] Polish PAC setup numbering in proxy testing guide
Co-authored-by: Power-Maverick <36135520+Power-Maverick@users.noreply.github.com>
---
docs/PROXY_TESTING.md | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/PROXY_TESTING.md b/docs/PROXY_TESTING.md
index f43c5693..15f64897 100644
--- a/docs/PROXY_TESTING.md
+++ b/docs/PROXY_TESTING.md
@@ -40,14 +40,14 @@
- `python3 -m http.server 9000 --directory /tmp`
3. Start mitmproxy:
- `mitmproxy --listen-host 127.0.0.1 --listen-port 8888`
-2. Configure OS proxy auto-config:
+4. Configure OS proxy auto-config:
- **Windows**: Settings → Network & Internet → Proxy → Use setup script.
- **macOS**: System Settings → Network → active adapter → Details → Proxies → Automatic Proxy Configuration.
- PAC URL: `http://127.0.0.1:9000/proxy.pac`
-4. In ToolBox, set mode to **Auto-detect system proxy**.
-5. Restart the app.
-6. Use **Test Connection** and verify traffic routes according to PAC rules.
-7. Confirm behavior in proxy/PAC server logs.
+5. In ToolBox, set mode to **Auto-detect system proxy**.
+6. Restart the app.
+7. Use **Test Connection** and verify traffic routes according to PAC rules.
+8. Confirm behavior in proxy/PAC server logs.
## 3) TLS interception simulation (custom CA bundle)