From 03c531dfebc8768dff428d8d8e245c85f5c1f253 Mon Sep 17 00:00:00 2001 From: Abdulhakeem Mustapha Date: Fri, 6 Nov 2026 19:22:14 +0100 Subject: [PATCH] fix(desktop): run browser_evaluate on pages whose CSP omits 'unsafe-eval' --- .../features/browser-automation/service.ts | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/desktop/src/features/browser-automation/service.ts b/packages/desktop/src/features/browser-automation/service.ts index fe07adecd3..ad77f67b7f 100644 --- a/packages/desktop/src/features/browser-automation/service.ts +++ b/packages/desktop/src/features/browser-automation/service.ts @@ -1,4 +1,5 @@ import { isAbsolute, relative, resolve as resolvePath } from "node:path"; +import { Script } from "node:vm"; import type { BrowserAutomationCommand, @@ -849,6 +850,11 @@ async function executeEvaluate( elementExpression = expression; } + const syntaxError = functionSyntaxError(functionSource); + if (syntaxError !== null) { + return fail(requestId, "browser_unknown_error", capEvaluateErrorMessage(syntaxError)); + } + let rawResult: unknown; try { rawResult = await target.contents.executeJavaScript( @@ -1550,14 +1556,36 @@ function capEvaluateErrorMessage(message: string): string { : message.slice(0, MAX_EVALUATE_ERROR_MESSAGE_LENGTH); } +/** + * The syntax error in the agent's function, or null. It is compiled here because a script that + * fails to compile in the page only reports "Script failed to execute", and a typo deserves the + * compiler's own message. Compiling runs nothing. + */ +function functionSyntaxError(functionSource: string): string | null { + try { + void new Script(`(${functionSource})`); + return null; + } catch (error) { + return error instanceof Error ? error.message : String(error); + } +} + +/** + * The page script that runs the agent's function and returns its result as bounded JSON. + * + * The function goes into the script as code, not as a string for the page to eval(): a page whose + * Content Security Policy omits 'unsafe-eval' refuses eval() but still runs what the host injects. + * It is read through a thunk defined where only the script's own parameter is in scope, so its + * names resolve against the page's globals and never against this wrapper's locals. + */ function buildEvaluateScript( functionSource: string, elementExpression: string | undefined, ): string { - return String.raw`(async () => { + return String.raw`((__paseoEvaluate) => __paseoEvaluate(() => (${functionSource})))(async (readFunction) => { const __PASEO_BROWSER_EVALUATE__ = true; try { - const userFunction = (0, eval)(${JSON.stringify(`(${functionSource})`)}); + const userFunction = readFunction(); if (typeof userFunction !== 'function') { throw new Error('browser_evaluate input must evaluate to a function.'); } @@ -1582,7 +1610,7 @@ function buildEvaluateScript( } catch (error) { return { error: error instanceof Error ? error.message : String(error) }; } - })()`; + })`; } async function focusKeypressTarget(