Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/editor/utils/aiCodingAgentBuildOutput.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function normalizeOutputMessage(message = "", workspaceFiles = []) {

const pathRegexes = [
/\/[^\s:'"`)]+(?:\/[^\s:'"`)]+)*\.[A-Za-z0-9]+/g,
/[A-Za-z]:\\[^\s:'"`)]+(?:\\[^\s:'"`)]+)*\.[A-Za-z0-9]+/g,
/[A-Za-z]:\\[^\\/\s:'"`)]+(?:\\[^\\/\s:'"`)]+)*\.[A-Za-z0-9]+/g,
];

pathRegexes.forEach((regex) => {
Expand Down
4 changes: 4 additions & 0 deletions src/components/plugin/utils/pluginRequestBridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export function createPluginRequestBridge(target = window) {
const requestId = event.data.requestId;
const resolve = pending.get(requestId);
if (!resolve || !isValidPluginUiResponseEvent(event.data, requestId)) return;
if (typeof resolve !== 'function') {
pending.delete(requestId);
return;
}
pending.delete(requestId);
resolve(event.data.error ? {success: false, error: event.data.error} : event.data.content);
};
Expand Down
32 changes: 20 additions & 12 deletions src/components/settings/panels/UpdatesPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,26 @@ import React, {useCallback, useEffect, useState} from 'react';
import {Button, Callout, Card, HTMLSelect, H4, Intent, Spinner, Switch} from '@blueprintjs/core';
import Markdown from 'markdown-to-jsx';

const formatReleaseNotes = value => String(value || '')
.replace(/<!--[\s\S]*?-->/g, '')
.replace(/\*\*Unsigned prerelease builds:\*\*\s*win-x64\.\s*macOS Gatekeeper and Windows SmartScreen may block these builds\./gi, 'Windows installers are published unsigned.')
.replace(/<details\b[^>]*>\s*<summary\b[^>]*>([\s\S]*?)<\/summary>/gi, '\n### $1\n')
.replace(/<\/details\s*>/gi, '\n')
.replace(/<\/?summary\b[^>]*>/gi, '')
.replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>')
.replace(/&quot;/gi, '"')
.replace(/&#39;|&apos;/gi, "'")
.replace(/&amp;/gi, '&')
.trim();
const formatReleaseNotes = value => {
let notes = String(value || '');
let previous;
do {
previous = notes;
notes = notes.replace(/<!--[\s\S]*?-->/g, '');
} while (notes !== previous);

return notes
.replace(/\*\*Unsigned prerelease builds:\*\*\s*win-x64\.\s*macOS Gatekeeper and Windows SmartScreen may block these builds\./gi, 'Windows installers are published unsigned.')
.replace(/<details\b[^>]*>\s*<summary\b[^>]*>([\s\S]*?)<\/summary>/gi, '\n### $1\n')
.replace(/<\/details\s*>/gi, '\n')
.replace(/<\/?summary\b[^>]*>/gi, '')
.replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>')
.replace(/&quot;/gi, '"')
.replace(/&#39;|&apos;/gi, "'")
.replace(/&amp;/gi, '&')
.trim();
};

export const UpdatesPanel = ({checkOnOpen = false, checkRequestId}) => {
const [checking, setChecking] = useState(false);
Expand Down
2 changes: 1 addition & 1 deletion src/ipc/ai/ai_chat_core.js
Original file line number Diff line number Diff line change
Expand Up @@ -2995,7 +2995,7 @@ async function toolFollowUp(llm, toolCalls) {
const followUpResult = {
content: sanitizeAssistantText(
reply
.replace(/(?:\n|\r|^|\s)*sources used:?\s*[\s\S]*$/i, "")
.replace(/\s*sources used:?\s*[\s\S]*$/i, "")
.trim()
),
sources: uniqueSources,
Expand Down
2 changes: 1 addition & 1 deletion src/ipc/ai/tools/search_web.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import {SEARCH_WEB_ECOSYSTEMS} from "./search_web_ecosystems.js";
function stripHtml(value = "") {
return String(value)
.replace(/<[^>]+>/g, " ")
.replace(/&amp;/g, "&")
.replace(/&quot;/g, "\"")
.replace(/&#39;/g, "'")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&amp;/g, "&")
.replace(/\s+/g, " ")
.trim();
}
Expand Down
4 changes: 2 additions & 2 deletions src/utils/ai/assistant/externalReferenceKnowledge.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ function stripTags(html = "") {
function decodeEntities(text = "") {
return String(text || "")
.replace(/&nbsp;/g, " ")
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'");
.replace(/&#39;/g, "'")
.replace(/&amp;/g, "&");
}

export function extractReferenceUrls(text = "") {
Expand Down
3 changes: 2 additions & 1 deletion src/utils/hostServices/gitInvocation.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ function helperTarget(args) {
const isLfs=command[1].startsWith('git-lfs-');if(isLfs!==!!command[5])throw invalid('HELPER_ARGUMENTS_REJECTED');
const url=repo.startsWith('/')?`ssh://${username}@${host.includes(':')?'['+host+']':host}:${port}${repo}`:`${username}@${host}:${repo}`;
const parsed=target(url);if(parsed.port!==port)throw invalid('HELPER_ARGUMENTS_REJECTED');
return {url,service:command[1],direction:command[5],repository:repo,command:command[1]+" '"+repo+"'"+(command[5]?' '+command[5]:''),...parsed};
const quotedRepo=repo.replace(/'/g, `'\"'\"'`);
return {url,service:command[1],direction:command[5],repository:repo,command:command[1]+" '"+quotedRepo+"'"+(command[5]?' '+command[5]:''),...parsed};
}
async function childDirectory(root,relative) {
if(!folder(relative))throw invalid('UNSAFE_REPOSITORY_CONFIG');
Expand Down
2 changes: 1 addition & 1 deletion src/utils/plugins/authoring/liveUi/backendActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export function planBackendResult(files,element,request) {
if(outputs.length || oldRows.length) {
if(marker !== config.marker || request.display !== mapping.display || changedOutputs) {
let callback=backendFrontendSource(`[data-fdo-result="${marker}"]`,config.handler,config.text,config.inputMarker ? `[data-fdo-input="${config.inputMarker}"]` : '',config.inputTag,request.display,preset,outputs);
if(mapping.comments?.length) callback=callback.replace('{','{\n'+mapping.comments.join('\n')+'\n');
if(mapping.comments?.length) callback=callback.replace('async () => {','async () => {\n'+mapping.comments.join('\n')+'\n');
operations.push({file:mapping.file,...mapping.callback,replacement:callback});
}
} else {
Expand Down
19 changes: 18 additions & 1 deletion src/utils/plugins/authoring/liveUi/localActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ const markerPattern = /^visual-result-\d+$/;
const inputMarkerPattern = /^visual-input-\d+$/;
const resultTags = new Set(['p','pre','span','output']);
const resultAttributes = new Set(['class','className','style','role','aria-label','aria-description','aria-live','title',markerName]);
const unsafeJsCharMap = {
'<': '\\u003C',
'>': '\\u003E',
'/': '\\u002F',
'\\': '\\\\',
'\b': '\\b',
'\f': '\\f',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
'\0': '\\0',
'\u2028': '\\u2028',
'\u2029': '\\u2029'
};
const escapeUnsafeChars = value => value.replace(/[<>\/\\\b\f\n\r\t\0\u2028\u2029]/g,char=>unsafeJsCharMap[char]);
const literalFields = node => {
if (node?.type !== 'ObjectExpression' || node.properties.some(p => p.type !== 'ObjectProperty' || p.computed)) return null;
const entries = node.properties.map(p => [p.key.name ?? p.key.value,p]);
Expand Down Expand Up @@ -121,9 +136,11 @@ export function planLocalAction(files,element,request) {
while (occupied.includes('visual-result-'+number)) number++;
const marker = target.marker || 'visual-result-'+number;
const selector = `[${markerName}="${marker}"]`;
const safeSelectorLiteral = escapeUnsafeChars(JSON.stringify(selector));
const safeTextLiteral = escapeUnsafeChars(JSON.stringify(request.text));
// No backend closure, tool execution or interpolation. The deployed handler
// writes textContent, including for strings that resemble HTML or code.
const handler = `${JSON.stringify(name)}: () => { const target = document.querySelector(${JSON.stringify(selector)}); if (target) target.textContent = ${JSON.stringify(request.text)}; }, `;
const handler = `${JSON.stringify(name)}: () => { const target = document.querySelector(${safeSelectorLiteral}); if (target) target.textContent = ${safeTextLiteral}; }, `;
const connection = planButtonAction(files,{...element,actionConnection:{...creation.connection,handlers:[{name,preventDefault:true}]}},{kind:'connect-action',handler:name});
const operations = connection.patches.map(({file,start,end,replacement})=>({file,start,end,replacement}));
operations.push({file:'/index.ts',start:creation.handlerInsert,end:creation.handlerInsert,replacement:handler});
Expand Down
8 changes: 7 additions & 1 deletion src/utils/plugins/authoring/liveUi/sdkComponentInsertion.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,13 @@ export function describeSdkInsertions(files, elements, calls, html, contract, en
}
}
}
const root = html.replace(/<style\b[^>]*>[\s\S]*?<\/style\s*>/gi,'').trim();
let root = html;
let previous;
do {
previous = root;
root = root.replace(/<style\b[^>]*>[\s\S]*?<\/style\s*>/gi,'');
} while (root !== previous);
root = root.trim();
const rootId = /^<main\b[^>]*\bdata-fdo-ui-id="([^"]+)"/i.exec(root)?.[1];
if (!rootId) return;
const targets = insertionTargets(html, rootId, elements, calls);
Expand Down
3 changes: 2 additions & 1 deletion src/utils/plugins/runtime/pluginHostAuth.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {randomBytes} from "crypto";
import {isNetworkTargetAllowed} from "../security/networkScopeRegistry";
import {
buildRuntimeSecurityPolicy,
Expand Down Expand Up @@ -715,7 +716,7 @@ export async function handleHostAuthBrokerStartRequest(payload = {}, options = {
const accountHint = toTrimmedString(payload?.accountHint);
const accountId = accountHint || "";
const payloadSessionId = toTrimmedString(payload?.sessionId);
const sessionId = payloadSessionId || `sess_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
const sessionId = payloadSessionId || `sess_${Date.now()}_${randomBytes(8).toString("hex")}`;
const raw = transportResult.raw;
const credentials = extractSessionCredentials(raw, payload);
const authTxnStore = getPluginAuthTxnStore(pluginId);
Expand Down
36 changes: 35 additions & 1 deletion src/utils/plugins/validation/unfencedWorkspaceSource.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
import {parse} from '@babel/parser';

function startsWithModuleKeywordAfterLeadingComments(text) {
let i = 0;

while (i < text.length) {
while (i < text.length && /\s/.test(text[i])) i++;

if (text.startsWith('//', i)) {
i += 2;
while (i < text.length && text[i] !== '\n') i++;
continue;
}

if (text.startsWith('/*', i)) {
i += 2;
const end = text.indexOf('*/', i);
if (end === -1) return false;
i = end + 2;
continue;
}

break;
}

if (text.startsWith('import', i)) {
const next = text[i + 6];
return !(next && /[A-Za-z0-9_$]/.test(next));
}
if (text.startsWith('export', i)) {
const next = text[i + 6];
return !(next && /[A-Za-z0-9_$]/.test(next));
}
return false;
}

// Only recognize an entire completed module. Never extract a plausible code
// fragment from prose, repair syntax, execute source, or infer its target path.
export function isUnfencedWorkspaceModule(text, path) {
Expand All @@ -20,7 +54,7 @@ export function inspectUnfencedWorkspaceModule(text, path) {
} catch (error) {
// Report syntax only for a recognizable module start, not arbitrary
// prose. This is diagnostic evidence, never permission to apply it.
const moduleStart = /^\s*(?:(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/)\s*)*(?:import|export)\b/.test(text);
const moduleStart = startsWithModuleKeywordAfterLeadingComments(text);
return {valid: false, ...(moduleStart ? {syntaxError: String(error.message).slice(0, 500)} : {})};
}
}
Loading