From 09526e21371fa040e9a31365172db07f6e6e8212 Mon Sep 17 00:00:00 2001 From: Charles-Henri ROBICHE Date: Wed, 4 Mar 2026 00:12:24 +0100 Subject: [PATCH 01/14] feat: add customCACertPath to AppSettings type --- apps/frontend/src/shared/types/settings.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/frontend/src/shared/types/settings.ts b/apps/frontend/src/shared/types/settings.ts index 77d3d6a32f..e97f438b09 100644 --- a/apps/frontend/src/shared/types/settings.ts +++ b/apps/frontend/src/shared/types/settings.ts @@ -296,6 +296,8 @@ export interface AppSettings { sidebarCollapsed?: boolean; // GPU acceleration for terminal rendering (WebGL) gpuAcceleration?: GpuAcceleration; + // Custom CA certificate path for enterprise proxy SSL (e.g., Zscaler) + customCACertPath?: string; } // GPU acceleration mode for terminal WebGL rendering From 6e7514f689f9e61129a2381caf98da637211fbaf Mon Sep 17 00:00:00 2001 From: Charles-Henri ROBICHE Date: Wed, 4 Mar 2026 00:12:48 +0100 Subject: [PATCH 02/14] feat: add DIALOG_SELECT_FILE IPC channel for file picker Co-Authored-By: Claude Opus 4.6 --- .../main/ipc-handlers/settings-handlers.ts | 19 +++++++++++++++++++ apps/frontend/src/preload/api/project-api.ts | 4 ++++ apps/frontend/src/shared/constants/ipc.ts | 1 + 3 files changed, 24 insertions(+) diff --git a/apps/frontend/src/main/ipc-handlers/settings-handlers.ts b/apps/frontend/src/main/ipc-handlers/settings-handlers.ts index 697711049a..f3ed6709f4 100644 --- a/apps/frontend/src/main/ipc-handlers/settings-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/settings-handlers.ts @@ -384,6 +384,25 @@ export function registerSettingsHandlers( } ); + ipcMain.handle( + IPC_CHANNELS.DIALOG_SELECT_FILE, + async (_, filters?: { name: string; extensions: string[] }[]): Promise => { + const mainWindow = getMainWindow(); + if (!mainWindow) return null; + + const result = await dialog.showOpenDialog(mainWindow, { + properties: ['openFile'], + filters: filters || [{ name: 'All Files', extensions: ['*'] }] + }); + + if (result.canceled || result.filePaths.length === 0) { + return null; + } + + return result.filePaths[0]; + } + ); + ipcMain.handle( IPC_CHANNELS.DIALOG_CREATE_PROJECT_FOLDER, async ( diff --git a/apps/frontend/src/preload/api/project-api.ts b/apps/frontend/src/preload/api/project-api.ts index b37face307..9f8f51f7c8 100644 --- a/apps/frontend/src/preload/api/project-api.ts +++ b/apps/frontend/src/preload/api/project-api.ts @@ -58,6 +58,7 @@ export interface ProjectAPI { // Dialog Operations selectDirectory: () => Promise; + selectFile: (filters?: { name: string; extensions: string[] }[]) => Promise; createProjectFolder: ( location: string, name: string, @@ -219,6 +220,9 @@ export const createProjectAPI = (): ProjectAPI => ({ selectDirectory: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.DIALOG_SELECT_DIRECTORY), + selectFile: (filters?: { name: string; extensions: string[] }[]): Promise => + ipcRenderer.invoke(IPC_CHANNELS.DIALOG_SELECT_FILE, filters), + createProjectFolder: ( location: string, name: string, diff --git a/apps/frontend/src/shared/constants/ipc.ts b/apps/frontend/src/shared/constants/ipc.ts index 48b3e95c22..9d23fe45b1 100644 --- a/apps/frontend/src/shared/constants/ipc.ts +++ b/apps/frontend/src/shared/constants/ipc.ts @@ -165,6 +165,7 @@ export const IPC_CHANNELS = { // Dialogs DIALOG_SELECT_DIRECTORY: 'dialog:selectDirectory', + DIALOG_SELECT_FILE: 'dialog:selectFile', DIALOG_CREATE_PROJECT_FOLDER: 'dialog:createProjectFolder', DIALOG_GET_DEFAULT_PROJECT_LOCATION: 'dialog:getDefaultProjectLocation', From 9a5d2f2c1a314da2b0fcc76b65492a0b70c47256 Mon Sep 17 00:00:00 2001 From: Charles-Henri ROBICHE Date: Wed, 4 Mar 2026 00:12:49 +0100 Subject: [PATCH 03/14] feat: add i18n keys for custom CA certificate setting --- apps/frontend/src/shared/i18n/locales/en/settings.json | 4 ++++ apps/frontend/src/shared/i18n/locales/fr/settings.json | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/apps/frontend/src/shared/i18n/locales/en/settings.json b/apps/frontend/src/shared/i18n/locales/en/settings.json index bc7fd8fa8f..afab06ddf3 100644 --- a/apps/frontend/src/shared/i18n/locales/en/settings.json +++ b/apps/frontend/src/shared/i18n/locales/en/settings.json @@ -243,6 +243,10 @@ "autoClaudePath": "Auto Claude Path", "autoClaudePathDescription": "Relative path to auto-claude directory in projects", "autoClaudePathPlaceholder": "auto-claude (default)", + "customCACertPath": "Custom CA Certificate", + "customCACertPathDescription": "Path to a custom CA certificate file (.pem/.crt) for SSL connections (e.g., Zscaler, corporate proxies)", + "customCACertPathPlaceholder": "Leave empty to use system defaults", + "customCACertBrowse": "Browse for certificate file", "autoNameTerminals": "Automatically name terminals", "autoNameTerminalsDescription": "Use AI to generate descriptive names for terminal tabs based on their activity" }, diff --git a/apps/frontend/src/shared/i18n/locales/fr/settings.json b/apps/frontend/src/shared/i18n/locales/fr/settings.json index 8d506e900f..fe6400b871 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/settings.json +++ b/apps/frontend/src/shared/i18n/locales/fr/settings.json @@ -243,6 +243,10 @@ "autoClaudePath": "Chemin Auto Claude", "autoClaudePathDescription": "Chemin relatif vers le répertoire auto-claude dans les projets", "autoClaudePathPlaceholder": "auto-claude (par défaut)", + "customCACertPath": "Certificat CA personnalisé", + "customCACertPathDescription": "Chemin vers un fichier de certificat CA (.pem/.crt) pour les connexions SSL (ex: Zscaler, proxys d'entreprise)", + "customCACertPathPlaceholder": "Laisser vide pour utiliser les paramètres système", + "customCACertBrowse": "Parcourir pour un fichier de certificat", "autoNameTerminals": "Nommer automatiquement les terminaux", "autoNameTerminalsDescription": "Utiliser l'IA pour générer des noms descriptifs pour les onglets de terminal en fonction de leur activité" }, From 09c596a6b3e80a9188c202f765c863e72ad68393 Mon Sep 17 00:00:00 2001 From: Charles-Henri ROBICHE Date: Wed, 4 Mar 2026 00:13:35 +0100 Subject: [PATCH 04/14] feat: forward NODE_EXTRA_CA_CERTS to Claude CLI subprocess --- apps/backend/core/auth.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/backend/core/auth.py b/apps/backend/core/auth.py index 78faac550e..b66090de34 100644 --- a/apps/backend/core/auth.py +++ b/apps/backend/core/auth.py @@ -64,6 +64,8 @@ "CLAUDE_CLI_PATH", # Profile's custom config directory (for multi-profile token storage) "CLAUDE_CONFIG_DIR", + # Custom CA certificate for enterprise proxy SSL (Zscaler, etc.) + "NODE_EXTRA_CA_CERTS", ] From 9ee56dcbd231f20272af85a6c2c989b7bf1b8b79 Mon Sep 17 00:00:00 2001 From: Charles-Henri ROBICHE Date: Wed, 4 Mar 2026 00:13:38 +0100 Subject: [PATCH 05/14] feat: inject NODE_EXTRA_CA_CERTS from settings into agent subprocess Co-Authored-By: Claude Opus 4.6 --- apps/frontend/src/main/agent/agent-process.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/frontend/src/main/agent/agent-process.ts b/apps/frontend/src/main/agent/agent-process.ts index f46c9bfc4d..3275e8dea4 100644 --- a/apps/frontend/src/main/agent/agent-process.ts +++ b/apps/frontend/src/main/agent/agent-process.ts @@ -229,6 +229,13 @@ export class AgentProcessManager { const ghCliEnv = this.detectAndSetCliPath('gh'); const glabCliEnv = this.detectAndSetCliPath('glab'); + // Inject custom CA certificate path for enterprise proxy SSL support + const certEnv: Record = {}; + const appSettingsForCert = readSettingsFile() as Partial | null; + if (appSettingsForCert?.customCACertPath) { + certEnv['NODE_EXTRA_CA_CERTS'] = appSettingsForCert.customCACertPath; + } + // Profile env is spread last to ensure CLAUDE_CONFIG_DIR and auth vars // from the active profile always win over extraEnv or augmentedEnv. const mergedEnv = { @@ -237,6 +244,7 @@ export class AgentProcessManager { ...claudeCliEnv, ...ghCliEnv, ...glabCliEnv, + ...certEnv, ...extraEnv, ...profileEnv, PYTHONUNBUFFERED: '1', From 082d50554be15923b376defd5bbecea3f8bf515e Mon Sep 17 00:00:00 2001 From: Charles-Henri ROBICHE Date: Wed, 4 Mar 2026 00:13:48 +0100 Subject: [PATCH 06/14] feat: add custom CA certificate field to Settings > Paths Co-Authored-By: Claude Opus 4.6 --- .../components/settings/GeneralSettings.tsx | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/apps/frontend/src/renderer/components/settings/GeneralSettings.tsx b/apps/frontend/src/renderer/components/settings/GeneralSettings.tsx index e44358c6c7..9c9f482861 100644 --- a/apps/frontend/src/renderer/components/settings/GeneralSettings.tsx +++ b/apps/frontend/src/renderer/components/settings/GeneralSettings.tsx @@ -2,6 +2,8 @@ import { useTranslation } from 'react-i18next'; import { useEffect, useState } from 'react'; import { Label } from '../ui/label'; import { Input } from '../ui/input'; +import { Button } from '../ui/button'; +import { FileText } from 'lucide-react'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; import { Switch } from '../ui/switch'; import { SettingsSection } from './SettingsSection'; @@ -352,6 +354,35 @@ export function GeneralSettings({ settings, onSettingsChange, section }: General onChange={(e) => onSettingsChange({ ...settings, autoBuildPath: e.target.value })} /> + {/* Custom CA Certificate */} +
+ +

{t('general.customCACertPathDescription')}

+
+ onSettingsChange({ ...settings, customCACertPath: e.target.value })} + /> + +
+
); From 3910f6e5ca36161c4368f994cfdfcaf7086f93d5 Mon Sep 17 00:00:00 2001 From: Charles-Henri ROBICHE Date: Wed, 4 Mar 2026 00:16:06 +0100 Subject: [PATCH 07/14] fix: add selectFile to ElectronAPI type and browser mock Co-Authored-By: Claude Opus 4.6 --- apps/frontend/src/renderer/lib/mocks/project-mock.ts | 4 ++++ apps/frontend/src/shared/types/ipc.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/apps/frontend/src/renderer/lib/mocks/project-mock.ts b/apps/frontend/src/renderer/lib/mocks/project-mock.ts index 153600e098..1314a9c51e 100644 --- a/apps/frontend/src/renderer/lib/mocks/project-mock.ts +++ b/apps/frontend/src/renderer/lib/mocks/project-mock.ts @@ -64,6 +64,10 @@ export const projectMock = { return prompt('Enter project path (browser mock):', '/Users/demo/projects/new-project'); }, + selectFile: async () => { + return prompt('Enter file path (browser mock):', '/path/to/certificate.pem'); + }, + createProjectFolder: async (_location: string, name: string, initGit: boolean) => ({ success: true, data: { diff --git a/apps/frontend/src/shared/types/ipc.ts b/apps/frontend/src/shared/types/ipc.ts index 532722db53..04a4cc7c7f 100644 --- a/apps/frontend/src/shared/types/ipc.ts +++ b/apps/frontend/src/shared/types/ipc.ts @@ -407,6 +407,7 @@ export interface ElectronAPI { // Dialog operations selectDirectory: () => Promise; + selectFile: (filters?: { name: string; extensions: string[] }[]) => Promise; createProjectFolder: (location: string, name: string, initGit: boolean) => Promise>; getDefaultProjectLocation: () => Promise; From a17af01d624ee5f24f5310a23210932438b2bd7a Mon Sep 17 00:00:00 2001 From: Charles-Henri Robiche Date: Wed, 4 Mar 2026 11:52:54 +0100 Subject: [PATCH 08/14] feat: implement custom CA certificate support in profile handlers and services --- .../ipc-handlers/profile-handlers.test.ts | 9 +- .../src/main/ipc-handlers/profile-handlers.ts | 14 ++- .../main/services/profile/profile-service.ts | 93 ++++++++++++++++++- package-lock.json | 38 +++++++- 4 files changed, 145 insertions(+), 9 deletions(-) diff --git a/apps/frontend/src/main/ipc-handlers/profile-handlers.test.ts b/apps/frontend/src/main/ipc-handlers/profile-handlers.test.ts index 0e115e4647..c28aad92bc 100644 --- a/apps/frontend/src/main/ipc-handlers/profile-handlers.test.ts +++ b/apps/frontend/src/main/ipc-handlers/profile-handlers.test.ts @@ -23,6 +23,12 @@ vi.mock('electron', () => ({ } })); +// Mock settings-utils to avoid electron.app dependency +vi.mock('../settings-utils', () => ({ + readSettingsFile: vi.fn(() => ({})), + getSettingsPath: vi.fn(() => '/test/settings.json') +})); + // Mock profile service vi.mock('../services/profile', () => ({ loadProfilesFile: mockedLoadProfilesFile, @@ -244,7 +250,8 @@ describe('profile-handlers - testConnection', () => { expect(testConnection).toHaveBeenCalledWith( 'https://api.anthropic.com', 'sk-test-key-12chars', - expect.any(AbortSignal) + expect.any(AbortSignal), + undefined ); }); }); diff --git a/apps/frontend/src/main/ipc-handlers/profile-handlers.ts b/apps/frontend/src/main/ipc-handlers/profile-handlers.ts index 9b522a6553..d415a241dd 100644 --- a/apps/frontend/src/main/ipc-handlers/profile-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/profile-handlers.ts @@ -13,6 +13,7 @@ import { ipcMain } from 'electron'; import { IPC_CHANNELS } from '../../shared/constants'; import type { IPCResult } from '../../shared/types'; +import type { AppSettings } from '../../shared/types'; import type { APIProfile, ProfileFormData, ProfilesFile, TestConnectionResult, DiscoverModelsResult } from '@shared/types/profile'; import { loadProfilesFile, @@ -25,6 +26,7 @@ import { testConnection, discoverModels } from '../services/profile'; +import { readSettingsFile } from '../settings-utils'; // Track active test connection requests for cancellation const activeTestConnections = new Map(); @@ -213,8 +215,12 @@ export function registerProfileHandlers(): void { }; } + // Read custom CA cert path from settings + const appSettings = readSettingsFile() as Partial | null; + const caCertPath = appSettings?.customCACertPath || undefined; + // Call testConnection from service layer with abort signal - const result = await testConnection(baseUrl, apiKey, controller.signal); + const result = await testConnection(baseUrl, apiKey, controller.signal, caCertPath); // Clear timeout on success clearTimeout(timeoutId); @@ -299,8 +305,12 @@ export function registerProfileHandlers(): void { }; } + // Read custom CA cert path from settings + const appSettingsForDiscover = readSettingsFile() as Partial | null; + const caCertPathForDiscover = appSettingsForDiscover?.customCACertPath || undefined; + // Call discoverModels from service layer with abort signal - const result = await discoverModels(baseUrl, apiKey, controller.signal); + const result = await discoverModels(baseUrl, apiKey, controller.signal, caCertPathForDiscover); // Clear timeout on success clearTimeout(timeoutId); diff --git a/apps/frontend/src/main/services/profile/profile-service.ts b/apps/frontend/src/main/services/profile/profile-service.ts index c57f60ce25..d5202f241a 100644 --- a/apps/frontend/src/main/services/profile/profile-service.ts +++ b/apps/frontend/src/main/services/profile/profile-service.ts @@ -6,6 +6,9 @@ * Uses atomic operations with file locking to prevent TOCTOU race conditions. */ +import https from 'node:https'; +import fs from 'node:fs'; +import type { OutgoingHttpHeaders } from 'node:http'; import Anthropic, { AuthenticationError, NotFoundError, @@ -14,6 +17,66 @@ import Anthropic, { } from '@anthropic-ai/sdk'; import { loadProfilesFile, generateProfileId, atomicModifyProfiles } from './profile-manager'; + +/** + * Creates a custom fetch function that routes HTTPS requests through a + * Node.js https.Agent configured with the provided CA certificate. + * Used to support custom CA certs (e.g., Zscaler, corporate proxies). + */ +function createFetchWithCA(ca: Buffer): typeof globalThis.fetch { + const agent = new https.Agent({ ca }); + + return async (input, init): Promise => { + const urlStr = typeof input === 'string' ? input : (input as URL | Request).toString(); + const url = new URL(urlStr); + const method = init?.method ?? 'GET'; + + const rawHeaders: OutgoingHttpHeaders = {}; + const initHeaders = init?.headers; + if (initHeaders) { + if (initHeaders instanceof Headers) { + initHeaders.forEach((v, k) => { rawHeaders[k] = v; }); + } else if (Array.isArray(initHeaders)) { + for (const [k, v] of initHeaders as [string, string][]) rawHeaders[k] = v; + } else { + Object.assign(rawHeaders, initHeaders); + } + } + + const bodyStr = init?.body != null ? String(init.body) : undefined; + + return new Promise((resolve, reject) => { + const req = https.request( + { + hostname: url.hostname, + port: Number(url.port) || 443, + path: url.pathname + url.search, + method, + headers: rawHeaders, + agent, + }, + (res) => { + const chunks: Buffer[] = []; + res.on('data', (c: Buffer) => chunks.push(c)); + res.on('end', () => { + const body = Buffer.concat(chunks); + const headers = new Headers(); + for (const [k, v] of Object.entries(res.headers)) { + if (v !== undefined) { + (Array.isArray(v) ? v : [v]).forEach((val) => headers.append(k, val)); + } + } + resolve(new Response(body, { status: res.statusCode ?? 200, headers })); + }); + res.on('error', reject); + } + ); + req.on('error', reject); + if (bodyStr) req.write(bodyStr); + req.end(); + }); + }; +} import type { APIProfile, TestConnectionResult, ModelInfo, DiscoverModelsResult } from '@shared/types/profile'; /** @@ -309,7 +372,8 @@ export async function getAPIProfileEnv(): Promise> { export async function testConnection( baseUrl: string, apiKey: string, - signal?: AbortSignal + signal?: AbortSignal, + caCertPath?: string ): Promise { // Validate API key first (key format doesn't depend on URL normalization) if (!validateApiKey(apiKey)) { @@ -391,12 +455,24 @@ export async function testConnection( } try { + // Build custom fetch with CA cert if provided (supports corporate proxies / Zscaler) + let customFetch: typeof globalThis.fetch | undefined; + if (caCertPath) { + try { + const ca = fs.readFileSync(caCertPath); + customFetch = createFetchWithCA(ca); + } catch { + // If cert can't be read, proceed without it + } + } + // Create Anthropic client with SDK const client = new Anthropic({ apiKey, baseURL: normalizedUrl, timeout: 10000, // 10 seconds maxRetries: 0, // Disable retries for immediate feedback + fetch: customFetch, }); // Make minimal request to test connection (pass signal for cancellation) @@ -514,7 +590,8 @@ export async function testConnection( export async function discoverModels( baseUrl: string, apiKey: string, - signal?: AbortSignal + signal?: AbortSignal, + caCertPath?: string ): Promise { // Validate API key first if (!validateApiKey(apiKey)) { @@ -556,12 +633,24 @@ export async function discoverModels( } try { + // Build custom fetch with CA cert if provided + let customFetchForDiscover: typeof globalThis.fetch | undefined; + if (caCertPath) { + try { + const ca = fs.readFileSync(caCertPath); + customFetchForDiscover = createFetchWithCA(ca); + } catch { + // If cert can't be read, proceed without it + } + } + // Create Anthropic client with SDK const client = new Anthropic({ apiKey, baseURL: normalizedUrl, timeout: 10000, // 10 seconds maxRetries: 0, // Disable retries for immediate feedback + fetch: customFetchForDiscover, }); // Fetch models with pagination (1000 limit to get all), pass signal for cancellation diff --git a/package-lock.json b/package-lock.json index 31ab465ad8..b657d2798d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "auto-claude", - "version": "2.7.6-beta.6", + "version": "2.7.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "auto-claude", - "version": "2.7.6-beta.6", + "version": "2.7.6", "license": "AGPL-3.0", "workspaces": [ "apps/*", @@ -25,7 +25,7 @@ }, "apps/frontend": { "name": "auto-claude-ui", - "version": "2.7.6-beta.6", + "version": "2.7.6", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -261,6 +261,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -825,6 +826,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -868,6 +870,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -907,6 +910,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -2193,6 +2197,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -2214,6 +2219,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.4.0.tgz", "integrity": "sha512-jn0phJ+hU7ZuvaoZE/8/Euw3gvHJrn2yi+kXrymwObEPVPjtwCmkvXDRQCWli+fCTTF/aSOtXaLr7CLIvv3LQg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": "^18.19.0 || >=20.6.0" }, @@ -2226,6 +2232,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -2241,6 +2248,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.208.0.tgz", "integrity": "sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.208.0", "import-in-the-middle": "^2.0.0", @@ -2643,6 +2651,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.4.0.tgz", "integrity": "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -2659,6 +2668,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.4.0.tgz", "integrity": "sha512-WH0xXkz/OHORDLKqaxcUZS0X+t1s7gGlumr2ebiEgNZQl2b0upK2cdoD0tatf7l8iP74woGJ/Kmxe82jdvcWRw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.4.0", "@opentelemetry/resources": "2.4.0", @@ -2676,6 +2686,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.39.0.tgz", "integrity": "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=14" } @@ -4870,6 +4881,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -5172,6 +5184,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5182,6 +5195,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -5451,6 +5465,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5483,6 +5498,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5953,6 +5969,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6923,6 +6940,7 @@ "integrity": "sha512-ce4Ogns4VMeisIuCSK0C62umG0lFy012jd8LMZ6w/veHUeX4fqfDrGe+HTWALAEwK6JwKP+dhPvizhArSOsFbg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.4.0", "builder-util": "26.3.4", @@ -7080,6 +7098,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", @@ -8438,6 +8457,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4" }, @@ -8764,6 +8784,7 @@ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -11250,6 +11271,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11440,6 +11462,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11449,6 +11472,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -12434,7 +12458,8 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", @@ -12613,6 +12638,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -12771,6 +12797,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13091,6 +13118,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -13683,6 +13711,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -14147,6 +14176,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From 957de50d51297af6bb778e8216728da6050effc2 Mon Sep 17 00:00:00 2001 From: Charles-Henri Robiche Date: Wed, 4 Mar 2026 12:02:32 +0100 Subject: [PATCH 09/14] refactor: address code review feedback on custom CA cert support - Extract buildCustomFetch() helper to eliminate duplicated cert-reading logic in testConnection and discoverModels - Extract getCustomCaCertPath() helper in profile-handlers to remove duplicated readSettingsFile calls - Fix body handling in createFetchWithCA: handle string, Uint8Array, and URLSearchParams correctly instead of blindly calling String() Co-Authored-By: Claude Sonnet 4.6 --- .../src/main/ipc-handlers/profile-handlers.ts | 17 +++--- .../main/services/profile/profile-service.ts | 60 +++++++++---------- 2 files changed, 37 insertions(+), 40 deletions(-) diff --git a/apps/frontend/src/main/ipc-handlers/profile-handlers.ts b/apps/frontend/src/main/ipc-handlers/profile-handlers.ts index d415a241dd..c69f212e8d 100644 --- a/apps/frontend/src/main/ipc-handlers/profile-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/profile-handlers.ts @@ -31,6 +31,11 @@ import { readSettingsFile } from '../settings-utils'; // Track active test connection requests for cancellation const activeTestConnections = new Map(); +function getCustomCaCertPath(): string | undefined { + const settings = readSettingsFile() as Partial | null; + return settings?.customCACertPath || undefined; +} + // Track active discover models requests for cancellation const activeDiscoverModelsRequests = new Map(); @@ -215,12 +220,8 @@ export function registerProfileHandlers(): void { }; } - // Read custom CA cert path from settings - const appSettings = readSettingsFile() as Partial | null; - const caCertPath = appSettings?.customCACertPath || undefined; - // Call testConnection from service layer with abort signal - const result = await testConnection(baseUrl, apiKey, controller.signal, caCertPath); + const result = await testConnection(baseUrl, apiKey, controller.signal, getCustomCaCertPath()); // Clear timeout on success clearTimeout(timeoutId); @@ -305,12 +306,8 @@ export function registerProfileHandlers(): void { }; } - // Read custom CA cert path from settings - const appSettingsForDiscover = readSettingsFile() as Partial | null; - const caCertPathForDiscover = appSettingsForDiscover?.customCACertPath || undefined; - // Call discoverModels from service layer with abort signal - const result = await discoverModels(baseUrl, apiKey, controller.signal, caCertPathForDiscover); + const result = await discoverModels(baseUrl, apiKey, controller.signal, getCustomCaCertPath()); // Clear timeout on success clearTimeout(timeoutId); diff --git a/apps/frontend/src/main/services/profile/profile-service.ts b/apps/frontend/src/main/services/profile/profile-service.ts index d5202f241a..69ec40298a 100644 --- a/apps/frontend/src/main/services/profile/profile-service.ts +++ b/apps/frontend/src/main/services/profile/profile-service.ts @@ -17,6 +17,7 @@ import Anthropic, { } from '@anthropic-ai/sdk'; import { loadProfilesFile, generateProfileId, atomicModifyProfiles } from './profile-manager'; +import type { APIProfile, TestConnectionResult, ModelInfo, DiscoverModelsResult } from '@shared/types/profile'; /** * Creates a custom fetch function that routes HTTPS requests through a @@ -43,8 +44,6 @@ function createFetchWithCA(ca: Buffer): typeof globalThis.fetch { } } - const bodyStr = init?.body != null ? String(init.body) : undefined; - return new Promise((resolve, reject) => { const req = https.request( { @@ -72,12 +71,35 @@ function createFetchWithCA(ca: Buffer): typeof globalThis.fetch { } ); req.on('error', reject); - if (bodyStr) req.write(bodyStr); + // Handle all valid BodyInit types: string, Buffer/Uint8Array, URLSearchParams + const body = init?.body; + if (body != null) { + if (typeof body === 'string' || body instanceof Uint8Array) { + req.write(body); + } else if (body instanceof URLSearchParams) { + req.write(body.toString()); + } + // ReadableStream is not used by the Anthropic SDK for these endpoints + } req.end(); }); }; } -import type { APIProfile, TestConnectionResult, ModelInfo, DiscoverModelsResult } from '@shared/types/profile'; + +/** + * Build a custom fetch for the Anthropic SDK from a CA cert path. + * Returns undefined if no path given or the file cannot be read. + */ +function buildCustomFetch(caCertPath?: string): typeof globalThis.fetch | undefined { + if (!caCertPath) return undefined; + try { + const ca = fs.readFileSync(caCertPath); + return createFetchWithCA(ca); + } catch { + // If the cert file can't be read (missing/unreadable), fall back to default TLS + return undefined; + } +} /** * Input type for creating a profile (without id, createdAt, updatedAt) @@ -455,24 +477,13 @@ export async function testConnection( } try { - // Build custom fetch with CA cert if provided (supports corporate proxies / Zscaler) - let customFetch: typeof globalThis.fetch | undefined; - if (caCertPath) { - try { - const ca = fs.readFileSync(caCertPath); - customFetch = createFetchWithCA(ca); - } catch { - // If cert can't be read, proceed without it - } - } - - // Create Anthropic client with SDK + // Create Anthropic client with SDK, using custom fetch if a CA cert is configured const client = new Anthropic({ apiKey, baseURL: normalizedUrl, timeout: 10000, // 10 seconds maxRetries: 0, // Disable retries for immediate feedback - fetch: customFetch, + fetch: buildCustomFetch(caCertPath), }); // Make minimal request to test connection (pass signal for cancellation) @@ -633,24 +644,13 @@ export async function discoverModels( } try { - // Build custom fetch with CA cert if provided - let customFetchForDiscover: typeof globalThis.fetch | undefined; - if (caCertPath) { - try { - const ca = fs.readFileSync(caCertPath); - customFetchForDiscover = createFetchWithCA(ca); - } catch { - // If cert can't be read, proceed without it - } - } - - // Create Anthropic client with SDK + // Create Anthropic client with SDK, using custom fetch if a CA cert is configured const client = new Anthropic({ apiKey, baseURL: normalizedUrl, timeout: 10000, // 10 seconds maxRetries: 0, // Disable retries for immediate feedback - fetch: customFetchForDiscover, + fetch: buildCustomFetch(caCertPath), }); // Fetch models with pagination (1000 limit to get all), pass signal for cancellation From 0a4c951ec3a28c012707c33fff597159bbedbf17 Mon Sep 17 00:00:00 2001 From: Charles-Henri Robiche Date: Wed, 4 Mar 2026 12:12:08 +0100 Subject: [PATCH 10/14] fix: address all PR review feedback on custom CA cert support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - createFetchWithCA: use Request.url instead of toString(), support HTTP URLs alongside HTTPS, wire AbortSignal to req.destroy() for proper cancellation - buildCustomFetch: throw with a clear message on cert read failure instead of silently falling back; both testConnection and discoverModels now surface cert errors with a descriptive message - agent-process: resolve cert path to absolute and check existsSync before setting NODE_EXTRA_CA_CERTS, warn if file not found - GeneralSettings: move hardcoded 'Certificates' file-filter label to i18n key (en + fr) - fr/settings.json: fix typo proxys → proxies Co-Authored-By: Claude Sonnet 4.6 --- apps/frontend/src/main/agent/agent-process.ts | 12 ++- .../main/services/profile/profile-service.ts | 83 +++++++++++++++---- .../components/settings/GeneralSettings.tsx | 2 +- .../src/shared/i18n/locales/en/settings.json | 1 + .../src/shared/i18n/locales/fr/settings.json | 3 +- 5 files changed, 82 insertions(+), 19 deletions(-) diff --git a/apps/frontend/src/main/agent/agent-process.ts b/apps/frontend/src/main/agent/agent-process.ts index 3275e8dea4..4db37c7577 100644 --- a/apps/frontend/src/main/agent/agent-process.ts +++ b/apps/frontend/src/main/agent/agent-process.ts @@ -232,8 +232,16 @@ export class AgentProcessManager { // Inject custom CA certificate path for enterprise proxy SSL support const certEnv: Record = {}; const appSettingsForCert = readSettingsFile() as Partial | null; - if (appSettingsForCert?.customCACertPath) { - certEnv['NODE_EXTRA_CA_CERTS'] = appSettingsForCert.customCACertPath; + const configuredCertPath = appSettingsForCert?.customCACertPath?.trim(); + if (configuredCertPath) { + const resolvedCertPath = path.isAbsolute(configuredCertPath) + ? configuredCertPath + : path.resolve(configuredCertPath); + if (existsSync(resolvedCertPath)) { + certEnv['NODE_EXTRA_CA_CERTS'] = resolvedCertPath; + } else { + console.warn('[AgentProcess] customCACertPath not found, skipping NODE_EXTRA_CA_CERTS:', resolvedCertPath); + } } // Profile env is spread last to ensure CLAUDE_CONFIG_DIR and auth vars diff --git a/apps/frontend/src/main/services/profile/profile-service.ts b/apps/frontend/src/main/services/profile/profile-service.ts index 69ec40298a..3e72708080 100644 --- a/apps/frontend/src/main/services/profile/profile-service.ts +++ b/apps/frontend/src/main/services/profile/profile-service.ts @@ -6,6 +6,7 @@ * Uses atomic operations with file locking to prevent TOCTOU race conditions. */ +import http from 'node:http'; import https from 'node:https'; import fs from 'node:fs'; import type { OutgoingHttpHeaders } from 'node:http'; @@ -20,15 +21,21 @@ import { loadProfilesFile, generateProfileId, atomicModifyProfiles } from './pro import type { APIProfile, TestConnectionResult, ModelInfo, DiscoverModelsResult } from '@shared/types/profile'; /** - * Creates a custom fetch function that routes HTTPS requests through a - * Node.js https.Agent configured with the provided CA certificate. + * Creates a custom fetch function that routes requests through a Node.js + * http/https Agent configured with the provided CA certificate. * Used to support custom CA certs (e.g., Zscaler, corporate proxies). */ function createFetchWithCA(ca: Buffer): typeof globalThis.fetch { - const agent = new https.Agent({ ca }); + const httpsAgent = new https.Agent({ ca }); return async (input, init): Promise => { - const urlStr = typeof input === 'string' ? input : (input as URL | Request).toString(); + // Derive URL string — use Request.url for Request objects, not toString() + const urlStr = + typeof input === 'string' + ? input + : input instanceof URL + ? input.toString() + : (input as Request).url; const url = new URL(urlStr); const method = init?.method ?? 'GET'; @@ -44,15 +51,26 @@ function createFetchWithCA(ca: Buffer): typeof globalThis.fetch { } } + // Abort signal: reject immediately if already aborted + const signal = init?.signal ?? undefined; + if (signal?.aborted) { + return Promise.reject(new DOMException('Aborted', 'AbortError')); + } + return new Promise((resolve, reject) => { - const req = https.request( + // Support both HTTP and HTTPS — only inject the custom CA agent for HTTPS + const isHttps = url.protocol === 'https:'; + const requestFn = isHttps ? https.request : http.request; + const defaultPort = isHttps ? 443 : 80; + + const req = requestFn( { hostname: url.hostname, - port: Number(url.port) || 443, + port: Number(url.port) || defaultPort, path: url.pathname + url.search, method, headers: rawHeaders, - agent, + agent: isHttps ? httpsAgent : undefined, }, (res) => { const chunks: Buffer[] = []; @@ -70,8 +88,18 @@ function createFetchWithCA(ca: Buffer): typeof globalThis.fetch { res.on('error', reject); } ); + + // Wire abort signal to destroy the request + const onAbort = () => { + req.destroy(new DOMException('Aborted', 'AbortError')); + reject(new DOMException('Aborted', 'AbortError')); + }; + signal?.addEventListener('abort', onAbort, { once: true }); + req.on('close', () => signal?.removeEventListener('abort', onAbort)); + req.on('error', reject); - // Handle all valid BodyInit types: string, Buffer/Uint8Array, URLSearchParams + + // Handle all valid BodyInit types the Anthropic SDK sends const body = init?.body; if (body != null) { if (typeof body === 'string' || body instanceof Uint8Array) { @@ -79,7 +107,6 @@ function createFetchWithCA(ca: Buffer): typeof globalThis.fetch { } else if (body instanceof URLSearchParams) { req.write(body.toString()); } - // ReadableStream is not used by the Anthropic SDK for these endpoints } req.end(); }); @@ -88,16 +115,18 @@ function createFetchWithCA(ca: Buffer): typeof globalThis.fetch { /** * Build a custom fetch for the Anthropic SDK from a CA cert path. - * Returns undefined if no path given or the file cannot be read. + * Throws with a clear message if the cert file cannot be read, + * so misconfiguration surfaces immediately rather than producing + * a misleading "network error". */ function buildCustomFetch(caCertPath?: string): typeof globalThis.fetch | undefined { if (!caCertPath) return undefined; try { const ca = fs.readFileSync(caCertPath); return createFetchWithCA(ca); - } catch { - // If the cert file can't be read (missing/unreadable), fall back to default TLS - return undefined; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + throw new Error(`Failed to read CA certificate at "${caCertPath}": ${msg}`); } } @@ -476,6 +505,18 @@ export async function testConnection( }; } + // Build custom fetch before the SDK try-catch so cert errors surface clearly + let customFetch: typeof globalThis.fetch | undefined; + try { + customFetch = buildCustomFetch(caCertPath); + } catch (certErr) { + return { + success: false, + errorType: 'network', + message: certErr instanceof Error ? certErr.message : 'Failed to read CA certificate.' + }; + } + try { // Create Anthropic client with SDK, using custom fetch if a CA cert is configured const client = new Anthropic({ @@ -483,7 +524,7 @@ export async function testConnection( baseURL: normalizedUrl, timeout: 10000, // 10 seconds maxRetries: 0, // Disable retries for immediate feedback - fetch: buildCustomFetch(caCertPath), + fetch: customFetch, }); // Make minimal request to test connection (pass signal for cancellation) @@ -643,6 +684,18 @@ export async function discoverModels( throw error; } + // Build custom fetch before the SDK try-catch so cert errors surface clearly + let customFetchForDiscover: typeof globalThis.fetch | undefined; + try { + customFetchForDiscover = buildCustomFetch(caCertPath); + } catch (certErr) { + const certError: Error & { errorType?: string } = new Error( + certErr instanceof Error ? certErr.message : 'Failed to read CA certificate.' + ); + certError.errorType = 'network'; + throw certError; + } + try { // Create Anthropic client with SDK, using custom fetch if a CA cert is configured const client = new Anthropic({ @@ -650,7 +703,7 @@ export async function discoverModels( baseURL: normalizedUrl, timeout: 10000, // 10 seconds maxRetries: 0, // Disable retries for immediate feedback - fetch: buildCustomFetch(caCertPath), + fetch: customFetchForDiscover, }); // Fetch models with pagination (1000 limit to get all), pass signal for cancellation diff --git a/apps/frontend/src/renderer/components/settings/GeneralSettings.tsx b/apps/frontend/src/renderer/components/settings/GeneralSettings.tsx index 9c9f482861..c9621e727d 100644 --- a/apps/frontend/src/renderer/components/settings/GeneralSettings.tsx +++ b/apps/frontend/src/renderer/components/settings/GeneralSettings.tsx @@ -371,7 +371,7 @@ export function GeneralSettings({ settings, onSettingsChange, section }: General size="icon" onClick={async () => { const result = await window.electronAPI.selectFile([ - { name: 'Certificates', extensions: ['pem', 'crt', 'cer'] } + { name: t('general.customCACertFileFilter'), extensions: ['pem', 'crt', 'cer'] } ]); if (result) { onSettingsChange({ ...settings, customCACertPath: result }); diff --git a/apps/frontend/src/shared/i18n/locales/en/settings.json b/apps/frontend/src/shared/i18n/locales/en/settings.json index afab06ddf3..60a3255516 100644 --- a/apps/frontend/src/shared/i18n/locales/en/settings.json +++ b/apps/frontend/src/shared/i18n/locales/en/settings.json @@ -247,6 +247,7 @@ "customCACertPathDescription": "Path to a custom CA certificate file (.pem/.crt) for SSL connections (e.g., Zscaler, corporate proxies)", "customCACertPathPlaceholder": "Leave empty to use system defaults", "customCACertBrowse": "Browse for certificate file", + "customCACertFileFilter": "Certificates", "autoNameTerminals": "Automatically name terminals", "autoNameTerminalsDescription": "Use AI to generate descriptive names for terminal tabs based on their activity" }, diff --git a/apps/frontend/src/shared/i18n/locales/fr/settings.json b/apps/frontend/src/shared/i18n/locales/fr/settings.json index fe6400b871..f21f2282d6 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/settings.json +++ b/apps/frontend/src/shared/i18n/locales/fr/settings.json @@ -244,9 +244,10 @@ "autoClaudePathDescription": "Chemin relatif vers le répertoire auto-claude dans les projets", "autoClaudePathPlaceholder": "auto-claude (par défaut)", "customCACertPath": "Certificat CA personnalisé", - "customCACertPathDescription": "Chemin vers un fichier de certificat CA (.pem/.crt) pour les connexions SSL (ex: Zscaler, proxys d'entreprise)", + "customCACertPathDescription": "Chemin vers un fichier de certificat CA (.pem/.crt) pour les connexions SSL (ex: Zscaler, proxies d'entreprise)", "customCACertPathPlaceholder": "Laisser vide pour utiliser les paramètres système", "customCACertBrowse": "Parcourir pour un fichier de certificat", + "customCACertFileFilter": "Certificats", "autoNameTerminals": "Nommer automatiquement les terminaux", "autoNameTerminalsDescription": "Utiliser l'IA pour générer des noms descriptifs pour les onglets de terminal en fonction de leur activité" }, From d5b92bbbe6ef9fe55ad74eac3988d1bbdaa9d609 Mon Sep 17 00:00:00 2001 From: Charles-Henri Robiche Date: Wed, 4 Mar 2026 12:22:23 +0100 Subject: [PATCH 11/14] fix: mock settings-utils in agent-process test to fix CI Our change to agent-process.ts added a readSettingsFile() call for the custom CA cert path. readSettingsFile() calls app.getPath('userData') which was not mocked in agent-process.test.ts, causing 24 test failures. Added vi.mock('../settings-utils') matching the pattern used in profile-handlers.test.ts. Co-Authored-By: Claude Sonnet 4.6 --- apps/frontend/src/main/agent/agent-process.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/frontend/src/main/agent/agent-process.test.ts b/apps/frontend/src/main/agent/agent-process.test.ts index e2102d005e..904397853b 100644 --- a/apps/frontend/src/main/agent/agent-process.test.ts +++ b/apps/frontend/src/main/agent/agent-process.test.ts @@ -116,6 +116,12 @@ vi.mock('electron', () => ({ } })); +// Mock settings-utils to avoid electron.app.getPath dependency +vi.mock('../settings-utils', () => ({ + readSettingsFile: vi.fn(() => ({})), + getSettingsPath: vi.fn(() => '/fake/settings.json') +})); + // Mock cli-tool-manager to avoid blocking tool detection on Windows vi.mock('../cli-tool-manager', () => ({ getToolInfo: vi.fn((tool: string) => { From b9738da68afde2de2003aba0b1467870943733d1 Mon Sep 17 00:00:00 2001 From: Charles-Henri Robiche Date: Wed, 4 Mar 2026 12:28:42 +0100 Subject: [PATCH 12/14] fix: guard CA cert path type and handle selectFile rejection - agent-process.ts: add typeof check before .trim() to avoid throwing if customCACertPath is a non-string value in corrupted settings - GeneralSettings.tsx: wrap selectFile IPC call in try/catch to prevent unhandled promise rejection if file dialog fails Co-Authored-By: Claude Sonnet 4.6 --- apps/frontend/src/main/agent/agent-process.ts | 3 ++- .../components/settings/GeneralSettings.tsx | 14 +++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/frontend/src/main/agent/agent-process.ts b/apps/frontend/src/main/agent/agent-process.ts index 4db37c7577..e396809cf9 100644 --- a/apps/frontend/src/main/agent/agent-process.ts +++ b/apps/frontend/src/main/agent/agent-process.ts @@ -232,7 +232,8 @@ export class AgentProcessManager { // Inject custom CA certificate path for enterprise proxy SSL support const certEnv: Record = {}; const appSettingsForCert = readSettingsFile() as Partial | null; - const configuredCertPath = appSettingsForCert?.customCACertPath?.trim(); + const rawCertPath = appSettingsForCert?.customCACertPath; + const configuredCertPath = typeof rawCertPath === 'string' ? rawCertPath.trim() : undefined; if (configuredCertPath) { const resolvedCertPath = path.isAbsolute(configuredCertPath) ? configuredCertPath diff --git a/apps/frontend/src/renderer/components/settings/GeneralSettings.tsx b/apps/frontend/src/renderer/components/settings/GeneralSettings.tsx index c9621e727d..fc7e9ae948 100644 --- a/apps/frontend/src/renderer/components/settings/GeneralSettings.tsx +++ b/apps/frontend/src/renderer/components/settings/GeneralSettings.tsx @@ -370,11 +370,15 @@ export function GeneralSettings({ settings, onSettingsChange, section }: General variant="outline" size="icon" onClick={async () => { - const result = await window.electronAPI.selectFile([ - { name: t('general.customCACertFileFilter'), extensions: ['pem', 'crt', 'cer'] } - ]); - if (result) { - onSettingsChange({ ...settings, customCACertPath: result }); + try { + const result = await window.electronAPI.selectFile([ + { name: t('general.customCACertFileFilter'), extensions: ['pem', 'crt', 'cer'] } + ]); + if (result) { + onSettingsChange({ ...settings, customCACertPath: result }); + } + } catch (err) { + console.error('Failed to open file dialog:', err); } }} aria-label={t('general.customCACertBrowse')} From 2f8f1be6c414b53a6039eebaf84726db391abd2a Mon Sep 17 00:00:00 2001 From: Charles-Henri Robiche Date: Wed, 4 Mar 2026 12:56:24 +0100 Subject: [PATCH 13/14] fix: require absolute CA cert path and verify it is a regular file Relative paths are unreliable in packaged Electron apps because process.cwd() varies between dev and production runtimes. Also add statSync().isFile() check to avoid injecting a directory or symlink target as NODE_EXTRA_CA_CERTS. Co-Authored-By: Claude Sonnet 4.6 --- apps/frontend/src/main/agent/agent-process.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/frontend/src/main/agent/agent-process.ts b/apps/frontend/src/main/agent/agent-process.ts index e396809cf9..78c86f1b8b 100644 --- a/apps/frontend/src/main/agent/agent-process.ts +++ b/apps/frontend/src/main/agent/agent-process.ts @@ -1,7 +1,7 @@ import { spawn } from 'child_process'; import path from 'path'; import { fileURLToPath } from 'url'; -import { existsSync, readFileSync } from 'fs'; +import { existsSync, readFileSync, statSync } from 'fs'; import { app } from 'electron'; // ESM-compatible __dirname @@ -235,13 +235,12 @@ export class AgentProcessManager { const rawCertPath = appSettingsForCert?.customCACertPath; const configuredCertPath = typeof rawCertPath === 'string' ? rawCertPath.trim() : undefined; if (configuredCertPath) { - const resolvedCertPath = path.isAbsolute(configuredCertPath) - ? configuredCertPath - : path.resolve(configuredCertPath); - if (existsSync(resolvedCertPath)) { - certEnv['NODE_EXTRA_CA_CERTS'] = resolvedCertPath; + if (!path.isAbsolute(configuredCertPath)) { + console.warn('[AgentProcess] customCACertPath must be an absolute path, skipping NODE_EXTRA_CA_CERTS:', configuredCertPath); + } else if (existsSync(configuredCertPath) && statSync(configuredCertPath).isFile()) { + certEnv['NODE_EXTRA_CA_CERTS'] = configuredCertPath; } else { - console.warn('[AgentProcess] customCACertPath not found, skipping NODE_EXTRA_CA_CERTS:', resolvedCertPath); + console.warn('[AgentProcess] customCACertPath is missing or not a file, skipping NODE_EXTRA_CA_CERTS:', configuredCertPath); } } From 254640a2888e756cccaab7e70d997a14d376292a Mon Sep 17 00:00:00 2001 From: Charles-Henri Robiche Date: Wed, 4 Mar 2026 13:01:21 +0100 Subject: [PATCH 14/14] fix: wrap statSync in try-catch to handle permission errors on CA cert path statSync() can throw EACCES if the process lacks read permissions on the file, which would crash agent process initialization. Wrap the existsSync/statSync block in try-catch so permission errors are logged as warnings and gracefully skipped instead of propagating. Co-Authored-By: Claude Sonnet 4.6 --- apps/frontend/src/main/agent/agent-process.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/main/agent/agent-process.ts b/apps/frontend/src/main/agent/agent-process.ts index 78c86f1b8b..1fd05c5e31 100644 --- a/apps/frontend/src/main/agent/agent-process.ts +++ b/apps/frontend/src/main/agent/agent-process.ts @@ -237,10 +237,16 @@ export class AgentProcessManager { if (configuredCertPath) { if (!path.isAbsolute(configuredCertPath)) { console.warn('[AgentProcess] customCACertPath must be an absolute path, skipping NODE_EXTRA_CA_CERTS:', configuredCertPath); - } else if (existsSync(configuredCertPath) && statSync(configuredCertPath).isFile()) { - certEnv['NODE_EXTRA_CA_CERTS'] = configuredCertPath; } else { - console.warn('[AgentProcess] customCACertPath is missing or not a file, skipping NODE_EXTRA_CA_CERTS:', configuredCertPath); + try { + if (existsSync(configuredCertPath) && statSync(configuredCertPath).isFile()) { + certEnv['NODE_EXTRA_CA_CERTS'] = configuredCertPath; + } else { + console.warn('[AgentProcess] customCACertPath is missing or not a file, skipping NODE_EXTRA_CA_CERTS:', configuredCertPath); + } + } catch (err) { + console.warn('[AgentProcess] customCACertPath stat failed, skipping NODE_EXTRA_CA_CERTS:', configuredCertPath, err); + } } }