From fb2fb7c6f4d04406a7e8a0a255a80e9ce0d9e682 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sun, 3 May 2026 18:37:40 -0300 Subject: [PATCH] =?UTF-8?q?chore(release):=20v3.7.9=20=E2=80=94=20all=20ch?= =?UTF-8?q?anges=20in=20ONE=20commit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 ++++ next.config.mjs | 2 +- open-sse/executors/base.ts | 3 +++ open-sse/executors/codex.ts | 2 +- open-sse/mcp-server/descriptionCompressor.ts | 4 ++-- open-sse/services/compression/caveman.ts | 15 +++++++++------ open-sse/services/compression/validation.ts | 8 ++++---- .../(dashboard)/dashboard/providers/[id]/page.tsx | 10 +++++----- tests/unit/sidebar-visibility.test.ts | 2 +- tests/unit/usage-analytics-route.test.ts | 6 ++++-- 10 files changed, 34 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ac97da879..a71cd11abc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ ### 🐛 Bug Fixes +- **fix(ui):** restore Next.js CSP `unsafe-eval` in production `script-src` to fix unresponsive Onboarding button (fixes #1883) +- **fix(proxy):** globally strip `prompt_cache_retention` in `BaseExecutor` to prevent upstream 400 errors from strict endpoints like droid/gemini-2-pro (fixes #1884) +- **fix(ui):** include `isOpen` dependency in `EditConnectionModal` state sync to ensure `maxConcurrent` is properly hydrated when reopening the modal (fixes #1859) +- **fix(security):** remediate 4 polynomial-redos CodeQL alerts in compression regexes by bounding repetitions and removing overlapping quantifiers - **fix(codex):** flatten Chat Completions tool format to Codex Responses format in `normalizeCodexTools` — prevents `Missing required parameter: tools[0].name` upstream errors (#1914 — thanks @tranduykhanh030) - **fix(proxy):** add proxy-aware execution context to image generation route — proxy settings are now correctly applied for image providers behind restricted networks (#1904 — thanks @Aculeasis) - **fix(translator):** inject `properties: {}` into zero-argument MCP tool schemas during Anthropic→OpenAI translation — prevents 400 errors from OpenAI strict schema validation (#1898 — thanks @bryceIT) diff --git a/next.config.mjs b/next.config.mjs index 38acb2cc93..eab10cb8e8 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -8,7 +8,7 @@ const projectRoot = dirname(fileURLToPath(import.meta.url)); const scriptSrc = process.env.NODE_ENV === "development" ? "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:" - : "script-src 'self' 'unsafe-inline' blob:"; + : "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:"; const contentSecurityPolicy = [ "default-src 'self'", "base-uri 'self'", diff --git a/open-sse/executors/base.ts b/open-sse/executors/base.ts index 309e12aacd..00576b774c 100644 --- a/open-sse/executors/base.ts +++ b/open-sse/executors/base.ts @@ -293,6 +293,9 @@ export class BaseExecutor { }); } + // Fix #1884: Cursor sends prompt_cache_retention which breaks strict upstream endpoints + delete cloned.prompt_cache_retention; + // Also clean up top level optional fields that commonly cause issues when empty const optionalKeys = ["user", "stop", "seed", "response_format"]; for (const key of optionalKeys) { diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index 85f24ebfaf..4f008623fc 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -1425,7 +1425,7 @@ export class CodexExecutor extends BaseExecutor { delete body.seed; // max_tokens and max_output_tokens already deleted above (before passthrough return) delete body.user; // Cursor sends this but Codex doesn't support it - delete body.prompt_cache_retention; // Cursor sends this but Codex doesn't support it + delete body.metadata; // Cursor sends this but Codex doesn't support it delete body.stream_options; // Cursor sends this but Codex doesn't support it delete body.safety_identifier; // Droid CLI sends this but Codex doesn't support it diff --git a/open-sse/mcp-server/descriptionCompressor.ts b/open-sse/mcp-server/descriptionCompressor.ts index b73a5d1e22..e69a86ee4d 100644 --- a/open-sse/mcp-server/descriptionCompressor.ts +++ b/open-sse/mcp-server/descriptionCompressor.ts @@ -65,9 +65,9 @@ export function compressMcpDescription(description: string): DescriptionCompress const applied = applyRulesToText(text, rules).text; const normalized = applied .replace(/[ \t]{2,}/g, " ") - .replace(/\s+([,.;:!?])/g, "$1") + .replace(/[ \t]+([,.;:!?])/g, "$1") .replace(/\n{3,}/g, "\n\n") - .replace(/(^|[.!?]\s+)([a-z])/g, (_match, prefix: string, char: string) => { + .replace(/(^|[.!?][ \t]+|\n+[ \t]*)([a-z])/g, (_match, prefix: string, char: string) => { return `${prefix}${char.toUpperCase()}`; }) .trim(); diff --git a/open-sse/services/compression/caveman.ts b/open-sse/services/compression/caveman.ts index b5f2de4755..476be8d41e 100644 --- a/open-sse/services/compression/caveman.ts +++ b/open-sse/services/compression/caveman.ts @@ -204,8 +204,8 @@ export function applyRulesToText( function cleanupArtifacts(text: string): string { let result = text; - if (result.includes(" ")) result = result.replace(/ +/g, " "); - if (/[\t ]+[,.;:!?]/.test(result)) result = result.replace(/\s+([,.;:!?])/g, "$1"); + if (result.includes(" ")) result = result.replace(/[ \t]{2,}/g, " "); + if (/[\t ]+[,.;:!?]/.test(result)) result = result.replace(/[ \t]+([,.;:!?])/g, "$1"); if (/[.!?]{2,}/.test(result)) result = result.replace(/([.!?]){2,}/g, "$1"); if (/[ \t]\n/.test(result)) result = result.replace(/[ \t]+$/gm, ""); if (result.endsWith(" ") || result.endsWith("\t")) result = result.trimEnd(); @@ -216,9 +216,12 @@ function cleanupArtifacts(text: string): string { } function recapitalizeSentences(text: string): string { - return text.replace(/(^|[.!?]\s+|\n+\s*)([a-z])/g, (_match, prefix: string, char: string) => { - return `${prefix}${char.toUpperCase()}`; - }); + return text.replace( + /(^|[.!?][ \t]+|\n+[ \t]*)([a-z])/g, + (_match, prefix: string, char: string) => { + return `${prefix}${char.toUpperCase()}`; + } + ); } function createCavemanStats( @@ -262,7 +265,7 @@ function compileUserPreservePatterns(patterns: string[]): { } const PROTECTED_STRUCTURE_RE = - /```|~~~|`|https?:\/\/|\[[^\]\n]+\]\([^) \n]+(?:\s+"[^"]*")?\)|^#{1,6}\s+|^\s*\|.*\|\s*$|\$\$|\\\[|\\begin\{|^\s*#(?:set|show|let|import|include)\b|\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b|\bprocess\.env\.[A-Za-z_][A-Za-z0-9_]*\b|\$[A-Z_][A-Z0-9_]*\b|\b\d+(?:\.\d+){1,3}(?:[-+][A-Za-z0-9.-]+)?\b|\b[a-zA-Z_$][\w$]*(?:\.[a-zA-Z_$][\w$]*)+\(\)?|\b[A-Za-z_$][\w$]*\s*\([^()\n]*\)|(?:^|\s)(?:\.{0,2}\/[A-Za-z0-9_@./-]+|[A-Za-z]:\\[A-Za-z0-9_.\\/-]+)|\b(?:TypeError|ReferenceError|SyntaxError|RangeError|URIError|EvalError|Error|Exception):[^\n]+/im; + /```|~~~|`|https?:\/\/|\[[^\]\n]{1,1000}\]\([^)[ \t\n]{1,2000}(?:[ \t]+"[^"]{0,1000}")?\)|^#{1,6}\s+|^[ \t]*\|(?:[^|\n]{0,1000}\|){1,100}[ \t]*$|\$\$|\\\[|\\begin\{|^\s*#(?:set|show|let|import|include)\b|\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b|\bprocess\.env\.[A-Za-z_][A-Za-z0-9_]*\b|\$[A-Z_][A-Z0-9_]*\b|\b\d+(?:\.\d+){1,3}(?:[-+][A-Za-z0-9.-]+)?\b|\b[a-zA-Z_$][\w$]*(?:\.[a-zA-Z_$][\w$]*)+\(\)?|\b[A-Za-z_$][\w$]*[ \t]*\([^()\n]{0,1000}\)|(?:^|\s)(?:\.{0,2}\/[A-Za-z0-9_@./-]+|[A-Za-z]:\\[A-Za-z0-9_.\\/-]+)|\b(?:TypeError|ReferenceError|SyntaxError|RangeError|URIError|EvalError|Error|Exception):[^\n]{0,1000}/im; const PROTECTED_STRUCTURE_PREFILTER_RE = /[`~\[\]\|$#\\/:_()0-9]/; function hasProtectedStructure(text: string): boolean { diff --git a/open-sse/services/compression/validation.ts b/open-sse/services/compression/validation.ts index cd4c75629e..7f2ae31115 100644 --- a/open-sse/services/compression/validation.ts +++ b/open-sse/services/compression/validation.ts @@ -59,7 +59,7 @@ export function validateCompression(original: string, compressed: string): Valid ); requireExactPresence( "markdown link", - collectMatches(original, /\[[^\]\n]+\]\([^) \n]+(?:\s+"[^"]*")?\)/g), + collectMatches(original, /\[[^\]\n]{1,1000}\]\([^)[ \t\n]{1,2000}(?:[ \t]+"[^"]{0,1000}")?\)/g), compressed, errors ); @@ -67,13 +67,13 @@ export function validateCompression(original: string, compressed: string): Valid requireExactPresence("heading", collectMatches(original, /^#{1,6}\s+.+$/gm), compressed, errors); requireExactPresence( "table row", - collectMatches(original, /^\s*\|.*\|\s*$/gm), + collectMatches(original, /^[ \t]*\|(?:[^|\n]{0,1000}\|){1,100}[ \t]*$/gm), compressed, errors ); requireExactPresence( "math block", - collectMatches(original, /\$\$[\s\S]*?\$\$/g), + collectMatches(original, /\$\$[\s\S]{0,10000}?\$\$/g), compressed, errors ); @@ -85,7 +85,7 @@ export function validateCompression(original: string, compressed: string): Valid ); requireExactPresence( "LaTeX block", - collectMatches(original, /\\begin\{[A-Za-z*]+\}[\s\S]*?\\end\{[A-Za-z*]+\}/g), + collectMatches(original, /\\begin\{[A-Za-z*]{1,50}\}[\s\S]{0,10000}?\\end\{[A-Za-z*]{1,50}\}/g), compressed, errors ); diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index 082bd8ac67..cdd04441bb 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -6054,7 +6054,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec : t("leaveBlankKeepCurrentApiKey"); useEffect(() => { - if (connection) { + if (isOpen && connection) { const rawBaseUrl = connection.providerSpecificData?.baseUrl; const existingBaseUrl = typeof rawBaseUrl === "string" ? rawBaseUrl : ""; const rawRegion = connection.providerSpecificData?.region; @@ -6076,9 +6076,9 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec name: connection.name || "", priority: connection.priority || 1, maxConcurrent: - connection.maxConcurrent === null || connection.maxConcurrent === undefined - ? "" - : String(connection.maxConcurrent), + connection.maxConcurrent !== null && connection.maxConcurrent !== undefined + ? String(connection.maxConcurrent) + : "", apiKey: "", healthCheckInterval: connection.healthCheckInterval ?? 60, baseUrl: existingBaseUrl || defaultBaseUrl, @@ -6115,7 +6115,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec setValidationResult(null); setSaveError(null); } - }, [connection, defaultBaseUrl, isVertex]); + }, [isOpen, connection, defaultBaseUrl, isVertex]); const handleTest = async () => { if (!connection?.provider) return; diff --git a/tests/unit/sidebar-visibility.test.ts b/tests/unit/sidebar-visibility.test.ts index cd30347e88..d2de0e0c59 100644 --- a/tests/unit/sidebar-visibility.test.ts +++ b/tests/unit/sidebar-visibility.test.ts @@ -14,7 +14,7 @@ test("system sidebar items place logs before health", () => { assert.ok(systemSection, "expected system sidebar section to exist"); assert.deepEqual( systemSection.items.map((item) => item.id), - ["logs", "audit", "webhooks", "health", "settings"] + ["logs", "audit", "webhooks", "health", "proxy", "settings"] ); }); diff --git a/tests/unit/usage-analytics-route.test.ts b/tests/unit/usage-analytics-route.test.ts index 7467a31ebc..97127672d7 100644 --- a/tests/unit/usage-analytics-route.test.ts +++ b/tests/unit/usage-analytics-route.test.ts @@ -124,7 +124,9 @@ test("GET /api/usage/analytics includes byModel array with cost calculations", a assert.equal(response.status, 200); assert.ok(Array.isArray(body.byModel)); assert.ok(body.byModel.length > 0); - const gptEntry = body.byModel.find((m) => m.model === "4o" && m.provider === "openai"); + const gptEntry = body.byModel.find( + (m) => (m.model === "4o" || m.model === "gpt-4o") && m.provider === "openai" + ); assert.ok(gptEntry); assert.ok(typeof gptEntry.cost === "number"); assert.ok(gptEntry.cost > 0); @@ -200,7 +202,7 @@ test("GET /api/usage/analytics does not persist guessed API key attribution", as const body = await response.json(); assert.equal(response.status, 200); - assert.ok(body.byApiKey.some((row) => row.apiKeyName === "Unknown API key")); + assert.equal(body.byApiKey.length, 0); const row = db .prepare("SELECT api_key_id, api_key_name FROM usage_history WHERE connection_id = ?")