chore(release): v3.7.9 — all changes in ONE commit

This commit is contained in:
diegosouzapw
2026-05-03 18:37:40 -03:00
parent 9e1d4885b5
commit fb2fb7c6f4
10 changed files with 34 additions and 22 deletions

View File

@@ -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)

View File

@@ -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'",

View File

@@ -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) {

View File

@@ -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

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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
);

View File

@@ -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;

View File

@@ -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"]
);
});

View File

@@ -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 = ?")