From 598454c931267082ba80c4ef426d0ef2c0370f55 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Fri, 19 Dec 2025 09:44:14 -0500 Subject: [PATCH 1/3] fix(cliproxy): include BASE_URL and AUTH_TOKEN when applying presets Presets now always include ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN when applied to provider settings. This ensures CLIProxy routing works correctly for Gemini, Codex, and Agy providers. Also fixes settings save failing for new profiles by changing PUT /api/settings/:profile to upsert behavior (create if not exists). --- src/web-server/routes.ts | 45 +++++++++++-------- .../components/cliproxy/provider-editor.tsx | 9 ++++ 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/web-server/routes.ts b/src/web-server/routes.ts index 5bc1bcea..a7cf9175 100644 --- a/src/web-server/routes.ts +++ b/src/web-server/routes.ts @@ -781,6 +781,7 @@ apiRoutes.get('/settings/:profile/raw', (req: Request, res: Response): void => { /** * PUT /api/settings/:profile - Update settings with conflict detection and backup + * Creates the settings file if it doesn't exist (upsert behavior) */ apiRoutes.put('/settings/:profile', (req: Request, res: Response): void => { const { profile } = req.params; @@ -788,29 +789,36 @@ apiRoutes.put('/settings/:profile', (req: Request, res: Response): void => { const ccsDir = getCcsDir(); const settingsPath = path.join(ccsDir, `${profile}.settings.json`); - if (!fs.existsSync(settingsPath)) { - res.status(404).json({ error: 'Settings not found' }); - return; + const fileExists = fs.existsSync(settingsPath); + + // Only check conflict if file exists and expectedMtime was provided + if (fileExists && expectedMtime) { + const stat = fs.statSync(settingsPath); + if (stat.mtime.getTime() !== expectedMtime) { + res.status(409).json({ + error: 'File modified externally', + currentMtime: stat.mtime.getTime(), + }); + return; + } } - // Conflict detection - const stat = fs.statSync(settingsPath); - if (expectedMtime && stat.mtime.getTime() !== expectedMtime) { - res.status(409).json({ - error: 'File modified externally', - currentMtime: stat.mtime.getTime(), - }); - return; + // Create backup only if file exists + let backupPath: string | undefined; + if (fileExists) { + const backupDir = path.join(ccsDir, 'backups'); + if (!fs.existsSync(backupDir)) { + fs.mkdirSync(backupDir, { recursive: true }); + } + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + backupPath = path.join(backupDir, `${profile}.${timestamp}.settings.json`); + fs.copyFileSync(settingsPath, backupPath); } - // Create backup - const backupDir = path.join(ccsDir, 'backups'); - if (!fs.existsSync(backupDir)) { - fs.mkdirSync(backupDir, { recursive: true }); + // Ensure directory exists for new files + if (!fileExists) { + fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); } - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - const backupPath = path.join(backupDir, `${profile}.${timestamp}.settings.json`); - fs.copyFileSync(settingsPath, backupPath); // Write new settings atomically const tempPath = settingsPath + '.tmp'; @@ -822,6 +830,7 @@ apiRoutes.put('/settings/:profile', (req: Request, res: Response): void => { profile, mtime: newStat.mtime.getTime(), backupPath, + created: !fileExists, }); }); diff --git a/ui/src/components/cliproxy/provider-editor.tsx b/ui/src/components/cliproxy/provider-editor.tsx index 0fd9632e..a1c637e8 100644 --- a/ui/src/components/cliproxy/provider-editor.tsx +++ b/ui/src/components/cliproxy/provider-editor.tsx @@ -296,7 +296,10 @@ export function ProviderEditor({ sonnet: model.id, haiku: model.id, }; + // Always include BASE_URL and AUTH_TOKEN for CLIProxy providers updateEnvValues({ + ANTHROPIC_BASE_URL: `http://127.0.0.1:8317/api/provider/${provider}`, + ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed', ANTHROPIC_MODEL: mapping.default, ANTHROPIC_DEFAULT_OPUS_MODEL: mapping.opus, ANTHROPIC_DEFAULT_SONNET_MODEL: mapping.sonnet, @@ -318,7 +321,10 @@ export function ProviderEditor({ size="sm" className="text-xs h-7 gap-1 pr-6" onClick={() => { + // Always include BASE_URL and AUTH_TOKEN for CLIProxy providers updateEnvValues({ + ANTHROPIC_BASE_URL: `http://127.0.0.1:8317/api/provider/${provider}`, + ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed', ANTHROPIC_MODEL: preset.default, ANTHROPIC_DEFAULT_OPUS_MODEL: preset.opus, ANTHROPIC_DEFAULT_SONNET_MODEL: preset.sonnet, @@ -655,7 +661,10 @@ export function ProviderEditor({ haiku: haikuModel || '', }} onApply={(values, presetName) => { + // Always include BASE_URL and AUTH_TOKEN for CLIProxy providers updateEnvValues({ + ANTHROPIC_BASE_URL: `http://127.0.0.1:8317/api/provider/${provider}`, + ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed', ANTHROPIC_MODEL: values.default, ANTHROPIC_DEFAULT_OPUS_MODEL: values.opus, ANTHROPIC_DEFAULT_SONNET_MODEL: values.sonnet, From 7ea64bdb4392d8cd41bc871892466d30c50b58fd Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Fri, 19 Dec 2025 09:49:05 -0500 Subject: [PATCH 2/3] fix(cliproxy): prevent variant names matching reserved provider names Added validation to reject variant names that match reserved CLIProxy provider names (gemini, codex, agy, qwen, iflow). Also protected reserved provider settings files from being deleted when deleting variants. This prevents accidental deletion of default provider configurations when users create variants with conflicting names. --- src/web-server/routes.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/web-server/routes.ts b/src/web-server/routes.ts index a7cf9175..5ff038eb 100644 --- a/src/web-server/routes.ts +++ b/src/web-server/routes.ts @@ -378,6 +378,15 @@ apiRoutes.post('/cliproxy', (req: Request, res: Response): void => { return; } + // Reject reserved provider names as variant names + const reservedNames = ['gemini', 'codex', 'agy', 'qwen', 'iflow']; + if (reservedNames.includes(name.toLowerCase())) { + res.status(400).json({ + error: `Cannot use reserved provider name '${name}' as variant name`, + }); + return; + } + const config = readConfigSafe(); config.cliproxy = config.cliproxy || {}; @@ -472,10 +481,14 @@ apiRoutes.delete('/cliproxy/:name', (req: Request, res: Response): void => { return; } - // Delete settings file - const settingsPath = path.join(getCcsDir(), `${name}.settings.json`); - if (fs.existsSync(settingsPath)) { - fs.unlinkSync(settingsPath); + // Never delete settings files for reserved provider names (safety guard) + const reservedNames = ['gemini', 'codex', 'agy', 'qwen', 'iflow']; + if (!reservedNames.includes(name.toLowerCase())) { + // Only delete settings file for non-reserved variant names + const settingsPath = path.join(getCcsDir(), `${name}.settings.json`); + if (fs.existsSync(settingsPath)) { + fs.unlinkSync(settingsPath); + } } delete config.cliproxy[name]; From 2373100c177654910922c6ef0e5ffc7cfe087b26 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Fri, 19 Dec 2025 09:55:27 -0500 Subject: [PATCH 3/3] fix(web-server): add comprehensive reserved name validation - Import isReservedName from centralized reserved-names module - Add validation to POST /api/profiles endpoint - Add validation to POST /api/cliproxy/openai-compat endpoint - Refactor hardcoded arrays to use centralized isReservedName() - Fixes DRY violation and ensures consistent validation across all endpoints --- src/web-server/routes.ts | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/web-server/routes.ts b/src/web-server/routes.ts index 5ff038eb..6a599bfc 100644 --- a/src/web-server/routes.ts +++ b/src/web-server/routes.ts @@ -66,6 +66,7 @@ import { getWebSearchConfig } from '../config/unified-config-loader'; import type { WebSearchConfig } from '../config/unified-config-types'; import { isUnifiedConfig } from '../config/unified-config-types'; import { isSensitiveKey, maskSensitiveValue } from '../utils/sensitive-keys'; +import { isReservedName, RESERVED_PROFILE_NAMES } from '../config/reserved-names'; import { getWebSearchReadiness, getGeminiCliStatus, @@ -275,6 +276,15 @@ apiRoutes.post('/profiles', (req: Request, res: Response): void => { return; } + // Validate reserved names + if (isReservedName(name)) { + res.status(400).json({ + error: `Profile name '${name}' is reserved`, + reserved: RESERVED_PROFILE_NAMES, + }); + return; + } + const config = readConfigSafe(); if (config.profiles[name]) { @@ -378,11 +388,11 @@ apiRoutes.post('/cliproxy', (req: Request, res: Response): void => { return; } - // Reject reserved provider names as variant names - const reservedNames = ['gemini', 'codex', 'agy', 'qwen', 'iflow']; - if (reservedNames.includes(name.toLowerCase())) { + // Reject reserved names as variant names (prevents collision with built-in providers) + if (isReservedName(name)) { res.status(400).json({ - error: `Cannot use reserved provider name '${name}' as variant name`, + error: `Cannot use reserved name '${name}' as variant name`, + reserved: RESERVED_PROFILE_NAMES, }); return; } @@ -482,8 +492,7 @@ apiRoutes.delete('/cliproxy/:name', (req: Request, res: Response): void => { } // Never delete settings files for reserved provider names (safety guard) - const reservedNames = ['gemini', 'codex', 'agy', 'qwen', 'iflow']; - if (!reservedNames.includes(name.toLowerCase())) { + if (!isReservedName(name)) { // Only delete settings file for non-reserved variant names const settingsPath = path.join(getCcsDir(), `${name}.settings.json`); if (fs.existsSync(settingsPath)) { @@ -1614,6 +1623,14 @@ apiRoutes.post('/cliproxy/openai-compat', (req: Request, res: Response): void => res.status(400).json({ error: 'name is required' }); return; } + // Validate reserved names + if (isReservedName(name)) { + res.status(400).json({ + error: `Provider name '${name}' is reserved`, + reserved: RESERVED_PROFILE_NAMES, + }); + return; + } if (!baseUrl || typeof baseUrl !== 'string') { res.status(400).json({ error: 'baseUrl is required' }); return;