UNPKG

chrome-devtools-frontend

Version:
223 lines (193 loc) • 7.19 kB
// Copyright 2026 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Host from '../../../core/host/host.js'; import * as i18n from '../../../core/i18n/i18n.js'; import * as Platform from '../../../core/platform/platform.js'; import * as Root from '../../../core/root/root.js'; import * as SDK from '../../../core/sdk/sdk.js'; import type {ChangeManager} from '../ChangeManager.js'; import {debugLog} from '../debug.js'; import {EvaluateAction, formatError, SideEffectError} from '../EvaluateAction.js'; import {FREESTYLER_WORLD_NAME} from '../injected.js'; import type {AgentOptions as BaseAgentOptions, FunctionCallHandlerResult, FunctionHandlerOptions,} from './AiAgent.js'; const lockedString = i18n.i18n.lockedString; export type CreateExtensionScopeFunction = (changes: ChangeManager) => { install(): Promise<void>, uninstall(): Promise<void>, }; export interface ExecuteJsAgentOptions extends BaseAgentOptions { changeManager?: ChangeManager; createExtensionScope?: CreateExtensionScopeFunction; execJs?: typeof executeJsCode; } export async function executeJsCode( functionDeclaration: string, {throwOnSideEffect, contextNode}: {throwOnSideEffect: boolean, contextNode: SDK.DOMModel.DOMNode|null}): Promise<string> { if (!contextNode) { throw new Error('Cannot execute JavaScript because of missing context node'); } const target = contextNode.domModel().target(); if (!target) { throw new Error('Target is not found for executing code'); } const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel); const frameId = contextNode.frameId() ?? resourceTreeModel?.mainFrame?.id; if (!frameId) { throw new Error('Main frame is not found for executing code'); } const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel); const pageAgent = target.pageAgent(); // This returns previously created world if it exists for the frame. const {executionContextId} = await pageAgent.invoke_createIsolatedWorld({frameId, worldName: FREESTYLER_WORLD_NAME}); const executionContext = runtimeModel?.executionContext(executionContextId); if (!executionContext) { throw new Error('Execution context is not found for executing code'); } if (executionContext.debuggerModel.selectedCallFrame()) { return formatError('Cannot evaluate JavaScript because the execution is paused on a breakpoint.'); } const remoteObject = await contextNode.resolveToObject(undefined, executionContextId); if (!remoteObject) { throw new Error('Cannot execute JavaScript because remote object cannot be resolved'); } return await EvaluateAction.execute(functionDeclaration, [remoteObject], executionContext, {throwOnSideEffect}); } const MAX_OBSERVATION_BYTE_LENGTH = 25_000; const OBSERVATION_TIMEOUT = 5_000; export interface JavascriptExecutorOptions { readonly executionMode: Root.Runtime.HostConfigFreestylerExecutionMode; readonly getContextNode: () => SDK.DOMModel.DOMNode | null; readonly createExtensionScope: (changes: ChangeManager) => { install(): Promise<void>, uninstall(): Promise<void>, }; readonly changes: ChangeManager; } export class JavascriptExecutor { #options: JavascriptExecutorOptions; #execJs: typeof executeJsCode; constructor(options: JavascriptExecutorOptions, execJs: typeof executeJsCode = executeJsCode) { this.#options = options; this.#execJs = execJs; } async executeAction(action: string, options?: FunctionHandlerOptions): Promise<FunctionCallHandlerResult<unknown>> { debugLog(`Action to execute: ${action}`); if (options?.approved === false) { return { error: 'Error: User denied code execution with side effects.', }; } if (this.#options.executionMode === Root.Runtime.HostConfigFreestylerExecutionMode.NO_SCRIPTS) { return { error: 'Error: JavaScript execution is currently disabled.', }; } const selectedNode = this.#options.getContextNode(); if (!selectedNode) { return {error: 'Error: no selected node found.'}; } const target = selectedNode.domModel().target(); if (target.model(SDK.DebuggerModel.DebuggerModel)?.selectedCallFrame()) { return { error: 'Error: Cannot evaluate JavaScript because the execution is paused on a breakpoint.', }; } const scope = this.#options.createExtensionScope(this.#options.changes); await scope.install(); try { let throwOnSideEffect = true; if (options?.approved) { throwOnSideEffect = false; } const result = await this.generateObservation(action, {throwOnSideEffect}); debugLog(`Action result: ${JSON.stringify(result)}`); if (result.sideEffect) { if (this.#options.executionMode === Root.Runtime.HostConfigFreestylerExecutionMode.SIDE_EFFECT_FREE_SCRIPTS_ONLY) { return { error: 'Error: JavaScript execution that modifies the page is currently disabled.', }; } if (options?.signal?.aborted) { return { error: 'Error: evaluation has been cancelled', }; } return { requiresApproval: true, description: lockedString('This code may modify page content. Continue?'), }; } if (result.canceled) { return { error: result.observation, }; } return { result: result.observation, }; } finally { await scope.uninstall(); } } async generateObservation( action: string, { throwOnSideEffect, }: { throwOnSideEffect: boolean, }, ): Promise<{ observation: string, sideEffect: boolean, canceled: boolean, }> { const functionDeclaration = `async function ($0) { try { ${action} ; return ((typeof data !== "undefined") ? data : undefined); } catch (error) { return error; } }`; try { const result = await Promise.race([ this.#execJs( functionDeclaration, { throwOnSideEffect, contextNode: this.#options.getContextNode(), }, ), new Promise<never>((_, reject) => { setTimeout( () => reject(new Error('Script execution exceeded the maximum allowed time.')), OBSERVATION_TIMEOUT); }), ]); const byteCount = Platform.StringUtilities.countWtf8Bytes(result); Host.userMetrics.freestylerEvalResponseSize(byteCount); if (byteCount > MAX_OBSERVATION_BYTE_LENGTH) { throw new Error('Output exceeded the maximum allowed length.'); } return { observation: result, sideEffect: false, canceled: false, }; } catch (error) { if (error instanceof SideEffectError) { return { observation: error.message, sideEffect: true, canceled: false, }; } return { observation: `Error: ${error.message}`, sideEffect: false, canceled: false, }; } } }