From 8fc64ddd21ecda7924a366aaa841f3f5523be558 Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Tue, 30 Jun 2026 11:42:19 -0400 Subject: [PATCH 1/5] Fix ADE browser popup crashes --- .../builtInBrowserService.test.ts | 54 ++++++++++++- .../builtInBrowser/builtInBrowserService.ts | 76 +++++++++++++++++-- 2 files changed, 120 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts b/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts index 2865a7a9b..8050dc6bc 100644 --- a/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts +++ b/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts @@ -989,6 +989,53 @@ describe("createBuiltInBrowserService — bounds and status dedupe", () => { expect(fakes.openExternal).not.toHaveBeenCalled(); }); + it("recovers crashed browser renderers to a blank tab with an error event", async () => { + const logger = { + debug: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + }; + const service = createBuiltInBrowserService({ getLogger: () => logger, onEvent: collector.onEvent }); + service.attachToWindow(fakeBrowserWindow() as unknown as Parameters[0]); + await service.createTab({ url: "https://linear.app/integrations/agents", activate: true }); + collector.events.length = 0; + + const wc = fakes.webContentsInstances[0]; + expect(wc, "browser tab webContents exists").toBeTruthy(); + const originalLoadURL = wc.loadURL; + let resolveRecovery: () => void = () => undefined; + const recovered = new Promise((resolve) => { + resolveRecovery = resolve; + }); + wc.loadURL = vi.fn(async (url: string) => { + await originalLoadURL(url); + if (url === "about:blank") resolveRecovery(); + }); + + wc.emit("render-process-gone", {}, { + reason: "crashed", + exitCode: 133, + }); + await recovered; + + expect(service.getStatus().url).toBe("about:blank"); + expect(service.getStatus().tabs[0]).toMatchObject({ + url: "about:blank", + isLoading: false, + }); + expect(collector.events.some((event) => ( + event.type === "error" + && event.message.includes("renderer exited (crashed, exit code 133)") + && event.message.includes("Recovered the tab to a blank page") + ))).toBe(true); + expect(logger.warn).toHaveBeenCalledWith("built_in_browser.render_process_gone", expect.objectContaining({ + reason: "crashed", + exitCode: 133, + url: "https://linear.app/integrations/agents", + })); + }); + it("does not impersonate Chrome or rewrite browser request headers", async () => { const service = createBuiltInBrowserService({ onEvent: collector.onEvent }); await service.createTab({ url: "https://example.test", activate: true }); @@ -1029,7 +1076,7 @@ describe("createBuiltInBrowserService — bounds and status dedupe", () => { })).toBe(false); }); - it("opens popup requests as real ADE browser tabs", async () => { + it("intercepts popup requests as real ADE browser tabs", async () => { const service = createBuiltInBrowserService({ onEvent: collector.onEvent }); await service.createTab({ url: "https://example.test", @@ -1042,11 +1089,12 @@ describe("createBuiltInBrowserService — bounds and status dedupe", () => { const response = firstWc?.openWindow("https://accounts.google.com/gsi/select"); - expect(response?.action).toBe("allow"); + expect(response?.action).toBe("deny"); expect(service.getStatus().tabs).toHaveLength(2); expect(service.getStatus().activeTabId).not.toBe(firstTabId); - expect(response?.createWindow?.({})).toBe(fakes.webContentsInstances[1]); + expect(response?.createWindow).toBeUndefined(); expect(service.getStatus().tabs.at(-1)).toMatchObject({ + url: "https://accounts.google.com/gsi/select", ownerLaneId: "lane-1", ownerChatSessionId: "chat-1", }); diff --git a/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts b/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts index faa79e37b..f715ddbf6 100644 --- a/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts +++ b/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts @@ -575,6 +575,7 @@ function createBuiltInBrowserWindowService(args: { let browserSessionConfigured = false; let lastEmittedStatusKey: string | null = null; const configuredWebContents = new WeakSet(); + const renderProcessRecoveryTabs = new Set(); let configuredBrowserSession: ReturnType | null = null; const logger = (): Logger | null => { @@ -1057,6 +1058,55 @@ function createBuiltInBrowserWindowService(args: { } }; + const recoverTabAfterRenderProcessGone = async ( + tab: BrowserTabState, + details: Electron.RenderProcessGoneDetails, + ): Promise => { + if (renderProcessRecoveryTabs.has(tab.id) || tab.webContents.isDestroyed()) return; + const crashedUrl = emptyToNull(tab.webContents.getURL()) ?? "about:blank"; + const reason = details.reason || "unknown"; + if (reason === "clean-exit") { + notifyTabActivity(tab); + emitStatus(); + return; + } + renderProcessRecoveryTabs.add(tab.id); + const exitCode = Number.isFinite(details.exitCode) ? `, exit code ${details.exitCode}` : ""; + const message = `ADE browser tab renderer exited (${reason}${exitCode}). Recovered the tab to a blank page.`; + try { + tab.pendingNetworkRequests.clear(); + pushNetworkDiagnostic(tab, { + url: crashedUrl, + method: null, + resourceType: "mainFrame", + statusCode: null, + error: message, + startedAt: null, + endedAt: new Date().toISOString(), + durationMs: null, + }); + if (tab.id === activeTabId) { + clearSelectionInternal(); + await stopInspectQuietly("built_in_browser.render_process_gone_stop_inspect_failed"); + } + emitError(new Error(message)); + if (!tab.webContents.isDestroyed()) { + await tab.webContents.loadURL("about:blank"); + } + } catch (error) { + logger()?.warn("built_in_browser.render_process_recovery_failed", { + tabId: tab.id, + reason, + err: errorMessage(error), + }); + emitError(new Error(`ADE browser tab renderer exited and recovery failed: ${errorMessage(error)}`)); + } finally { + renderProcessRecoveryTabs.delete(tab.id); + notifyTabActivity(tab); + emitStatus(); + } + }; + const configureBrowserWebContents = (wc: WebContents): void => { if (configuredWebContents.has(wc)) return; configuredWebContents.add(wc); @@ -1075,11 +1125,15 @@ function createBuiltInBrowserWindowService(args: { }); wc.setWindowOpenHandler(({ url }) => { const tab = createPopupTabState(url, tabForWebContents(wc) ?? activeTab()); - if (!tab) return { action: "deny" }; - return { - action: "allow", - createWindow: () => tab.webContents, - }; + if (tab) { + const popupUrl = stringOrNull(url) ?? "about:blank"; + // Own popup navigation; Electron cannot attach an existing WebContentsView as a native guest window. + void tab.webContents.loadURL(popupUrl).catch((error) => { + emitError(new Error(`Could not open browser popup: ${errorMessage(error)}`)); + emitStatus(); + }); + } + return { action: "deny" }; }); wc.on("will-navigate", (event, url) => { if (isAllowedNavigationUrl(url)) return; @@ -1109,12 +1163,20 @@ function createBuiltInBrowserWindowService(args: { emitStatus(); }); wc.on("render-process-gone", (_event, details) => { + const tab = tabForWebContents(wc); logger()?.warn("built_in_browser.render_process_gone", { reason: details.reason, exitCode: details.exitCode, + tabId: tab?.id ?? null, + url: tab ? emptyToNull(tab.webContents.getURL()) : null, + }); + if (!tab) { + emitStatus(); + return; + } + void recoverTabAfterRenderProcessGone(tab, details).catch((error) => { + emitError(new Error(`ADE browser tab renderer recovery failed: ${errorMessage(error)}`)); }); - notifyTabActivity(tabForWebContents(wc)); - emitStatus(); }); wc.on("did-fail-load", (_event, errorCode, errorDescription, validatedURL, isMainFrame) => { if (!isMainFrame || errorCode === -3) return; From 446f9a754a269a52b75b5c39386259666f18f2b4 Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Tue, 30 Jun 2026 12:00:06 -0400 Subject: [PATCH 2/5] Address browser recovery review feedback --- .../builtInBrowserService.test.ts | 27 ++++++++++---- .../builtInBrowser/builtInBrowserService.ts | 37 +++++++++++++++---- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts b/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts index 8050dc6bc..d1468a368 100644 --- a/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts +++ b/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts @@ -996,21 +996,32 @@ describe("createBuiltInBrowserService — bounds and status dedupe", () => { info: vi.fn(), warn: vi.fn(), }; - const service = createBuiltInBrowserService({ getLogger: () => logger, onEvent: collector.onEvent }); + let resolveRecovery: () => void = () => undefined; + const recovered = new Promise((resolve) => { + resolveRecovery = resolve; + }); + const service = createBuiltInBrowserService({ + getLogger: () => logger, + onEvent: (event) => { + collector.onEvent(event); + if ( + event.type === "error" + && event.message.includes("renderer exited (crashed, exit code 133)") + && event.message.includes("Recovered the tab to a blank page") + ) { + resolveRecovery(); + } + }, + }); service.attachToWindow(fakeBrowserWindow() as unknown as Parameters[0]); - await service.createTab({ url: "https://linear.app/integrations/agents", activate: true }); + await service.createTab({ url: "https://linear.app/integrations/agents?code=secret", activate: true }); collector.events.length = 0; const wc = fakes.webContentsInstances[0]; expect(wc, "browser tab webContents exists").toBeTruthy(); const originalLoadURL = wc.loadURL; - let resolveRecovery: () => void = () => undefined; - const recovered = new Promise((resolve) => { - resolveRecovery = resolve; - }); wc.loadURL = vi.fn(async (url: string) => { await originalLoadURL(url); - if (url === "about:blank") resolveRecovery(); }); wc.emit("render-process-gone", {}, { @@ -1032,7 +1043,7 @@ describe("createBuiltInBrowserService — bounds and status dedupe", () => { expect(logger.warn).toHaveBeenCalledWith("built_in_browser.render_process_gone", expect.objectContaining({ reason: "crashed", exitCode: 133, - url: "https://linear.app/integrations/agents", + url: "https://linear.app", })); }); diff --git a/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts b/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts index f715ddbf6..4814ab190 100644 --- a/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts +++ b/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts @@ -1062,8 +1062,9 @@ function createBuiltInBrowserWindowService(args: { tab: BrowserTabState, details: Electron.RenderProcessGoneDetails, ): Promise => { - if (renderProcessRecoveryTabs.has(tab.id) || tab.webContents.isDestroyed()) return; - const crashedUrl = emptyToNull(tab.webContents.getURL()) ?? "about:blank"; + const crashedWebContents = tab.webContents; + if (renderProcessRecoveryTabs.has(tab.id) || crashedWebContents.isDestroyed()) return; + const crashedUrl = emptyToNull(crashedWebContents.getURL()) ?? "about:blank"; const reason = details.reason || "unknown"; if (reason === "clean-exit") { notifyTabActivity(tab); @@ -1072,7 +1073,7 @@ function createBuiltInBrowserWindowService(args: { } renderProcessRecoveryTabs.add(tab.id); const exitCode = Number.isFinite(details.exitCode) ? `, exit code ${details.exitCode}` : ""; - const message = `ADE browser tab renderer exited (${reason}${exitCode}). Recovered the tab to a blank page.`; + const exitMessage = `ADE browser tab renderer exited (${reason}${exitCode}).`; try { tab.pendingNetworkRequests.clear(); pushNetworkDiagnostic(tab, { @@ -1080,7 +1081,7 @@ function createBuiltInBrowserWindowService(args: { method: null, resourceType: "mainFrame", statusCode: null, - error: message, + error: exitMessage, startedAt: null, endedAt: new Date().toISOString(), durationMs: null, @@ -1089,10 +1090,16 @@ function createBuiltInBrowserWindowService(args: { clearSelectionInternal(); await stopInspectQuietly("built_in_browser.render_process_gone_stop_inspect_failed"); } - emitError(new Error(message)); - if (!tab.webContents.isDestroyed()) { - await tab.webContents.loadURL("about:blank"); + if (tab.webContents !== crashedWebContents) { + emitError(new Error(`${exitMessage} Recovery skipped because the tab target changed.`)); + return; + } + if (crashedWebContents.isDestroyed()) { + emitError(new Error(`${exitMessage} Recovery skipped because the browser tab was destroyed.`)); + return; } + await crashedWebContents.loadURL("about:blank"); + emitError(new Error(`${exitMessage} Recovered the tab to a blank page.`)); } catch (error) { logger()?.warn("built_in_browser.render_process_recovery_failed", { tabId: tab.id, @@ -1168,7 +1175,7 @@ function createBuiltInBrowserWindowService(args: { reason: details.reason, exitCode: details.exitCode, tabId: tab?.id ?? null, - url: tab ? emptyToNull(tab.webContents.getURL()) : null, + url: tab && !wc.isDestroyed() ? urlForBrowserLog(wc.getURL()) : null, }); if (!tab) { emitStatus(); @@ -1176,6 +1183,7 @@ function createBuiltInBrowserWindowService(args: { } void recoverTabAfterRenderProcessGone(tab, details).catch((error) => { emitError(new Error(`ADE browser tab renderer recovery failed: ${errorMessage(error)}`)); + emitStatus(); }); }); wc.on("did-fail-load", (_event, errorCode, errorDescription, validatedURL, isMainFrame) => { @@ -2886,6 +2894,19 @@ function emptyToNull(value: string): string | null { return trimmed.length ? trimmed : null; } +function urlForBrowserLog(value: string): string | null { + const url = emptyToNull(value); + if (!url) return null; + try { + const parsed = new URL(url); + if (parsed.protocol === "http:" || parsed.protocol === "https:") return parsed.origin; + if (parsed.protocol === "about:") return parsed.href === "about:blank" ? parsed.href : "about:"; + return parsed.protocol; + } catch { + return null; + } +} + function tabStatus(tab: BrowserTabState): BuiltInBrowserTab { const wc = tab.webContents; return { From 830c2415d2ceb08526fb3b0658d530359cb870f8 Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Tue, 30 Jun 2026 12:16:35 -0400 Subject: [PATCH 3/5] Preserve browser popup POST requests --- .../builtInBrowserService.test.ts | 34 ++++++++++++++++--- .../builtInBrowser/builtInBrowserService.ts | 25 +++++++++++--- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts b/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts index d1468a368..611e6dcb4 100644 --- a/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts +++ b/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts @@ -11,7 +11,16 @@ const fakes = vi.hoisted(() => { action: "allow" | "deny"; createWindow?: (options: Record) => FakeWebContents; }; - type WindowOpenHandler = (details: { url: string }) => WindowOpenHandlerResponse; + type WindowOpenDetails = { + url: string; + referrer?: { url: string; policy: string }; + postBody?: { + boundary?: string; + contentType: string; + data: Array>; + }; + }; + type WindowOpenHandler = (details: WindowOpenDetails) => WindowOpenHandlerResponse; type BeforeSendHeadersHandler = ( details: { requestHeaders: Record }, callback: (response: { requestHeaders: Record }) => void, @@ -77,10 +86,12 @@ const fakes = vi.hoisted(() => { session: unknown = null; audioMutedCalls: boolean[] = []; userAgentCalls: string[] = []; + loadURLCalls: Array<{ url: string; options?: Record }> = []; currentUrl = ""; private listeners: Record void>> = {}; private windowOpenHandler: WindowOpenHandler | null = null; - loadURL = async (url: string): Promise => { + loadURL = async (url: string, options?: Record): Promise => { + this.loadURLCalls.push({ url, options }); const event = { preventDefault: vi.fn() }; this.emit("will-navigate", event, url); if (event.preventDefault.mock.calls.length === 0) { @@ -116,7 +127,7 @@ const fakes = vi.hoisted(() => { setWindowOpenHandler = (handler: WindowOpenHandler): void => { this.windowOpenHandler = handler; }; - openWindow = (url: string): WindowOpenHandlerResponse | null => this.windowOpenHandler?.({ url }) ?? null; + openWindow = (url: string, details: Partial = {}): WindowOpenHandlerResponse | null => this.windowOpenHandler?.({ ...details, url }) ?? null; on = (event: string, fn: (...args: unknown[]) => void): void => { (this.listeners[event] ??= []).push(fn); }; @@ -1097,8 +1108,15 @@ describe("createBuiltInBrowserService — bounds and status dedupe", () => { }); const firstTabId = service.getStatus().activeTabId; const firstWc = fakes.webContentsInstances[0]; + const postData = [{ bytes: Buffer.from("token=abc"), type: "rawData" }]; - const response = firstWc?.openWindow("https://accounts.google.com/gsi/select"); + const response = firstWc?.openWindow("https://accounts.google.com/gsi/select", { + referrer: { url: "https://example.test/sign-in", policy: "strict-origin-when-cross-origin" }, + postBody: { + contentType: "application/x-www-form-urlencoded", + data: postData, + }, + }); expect(response?.action).toBe("deny"); expect(service.getStatus().tabs).toHaveLength(2); @@ -1109,6 +1127,14 @@ describe("createBuiltInBrowserService — bounds and status dedupe", () => { ownerLaneId: "lane-1", ownerChatSessionId: "chat-1", }); + expect(fakes.webContentsInstances.at(-1)?.loadURLCalls.at(-1)).toEqual({ + url: "https://accounts.google.com/gsi/select", + options: { + httpReferrer: { url: "https://example.test/sign-in", policy: "strict-origin-when-cross-origin" }, + postData, + extraHeaders: "content-type: application/x-www-form-urlencoded\n", + }, + }); const openEvent = collector.events.findLast((event) => event.type === "open-request"); expect(openEvent).toMatchObject({ diff --git a/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts b/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts index 4814ab190..269c49c8b 100644 --- a/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts +++ b/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts @@ -1130,12 +1130,13 @@ function createBuiltInBrowserWindowService(args: { timestamp: new Date().toISOString(), }); }); - wc.setWindowOpenHandler(({ url }) => { - const tab = createPopupTabState(url, tabForWebContents(wc) ?? activeTab()); + wc.setWindowOpenHandler((details) => { + const tab = createPopupTabState(details.url, tabForWebContents(wc) ?? activeTab()); if (tab) { - const popupUrl = stringOrNull(url) ?? "about:blank"; + const popupUrl = stringOrNull(details.url) ?? "about:blank"; + const popupLoadOptions = loadUrlOptionsForWindowOpen(details); // Own popup navigation; Electron cannot attach an existing WebContentsView as a native guest window. - void tab.webContents.loadURL(popupUrl).catch((error) => { + void tab.webContents.loadURL(popupUrl, popupLoadOptions).catch((error) => { emitError(new Error(`Could not open browser popup: ${errorMessage(error)}`)); emitStatus(); }); @@ -2907,6 +2908,22 @@ function urlForBrowserLog(value: string): string | null { } } +function loadUrlOptionsForWindowOpen(details: Electron.HandlerDetails): Electron.LoadURLOptions | undefined { + const options: Electron.LoadURLOptions = {}; + if (emptyToNull(details.referrer?.url ?? "")) { + options.httpReferrer = details.referrer; + } + if (details.postBody?.data.length) { + options.postData = details.postBody.data; + let contentType = details.postBody.contentType; + if (details.postBody.boundary && !/;\s*boundary=/i.test(contentType)) { + contentType = `${contentType}; boundary=${details.postBody.boundary}`; + } + options.extraHeaders = `content-type: ${contentType}\n`; + } + return Object.keys(options).length > 0 ? options : undefined; +} + function tabStatus(tab: BrowserTabState): BuiltInBrowserTab { const wc = tab.webContents; return { From 0a6c4c25a10eb2113a2dfdb4584ec55a3aa17454 Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Tue, 30 Jun 2026 12:33:23 -0400 Subject: [PATCH 4/5] Preserve browser popup opener semantics --- .../builtInBrowserService.test.ts | 29 ++++--- .../builtInBrowser/builtInBrowserService.ts | 81 +++++++++---------- 2 files changed, 60 insertions(+), 50 deletions(-) diff --git a/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts b/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts index 611e6dcb4..3849853eb 100644 --- a/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts +++ b/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts @@ -1118,22 +1118,33 @@ describe("createBuiltInBrowserService — bounds and status dedupe", () => { }, }); - expect(response?.action).toBe("deny"); + expect(response?.action).toBe("allow"); + expect(response?.createWindow).toEqual(expect.any(Function)); + const popupWc = response?.createWindow?.({ + webPreferences: { + additionalArguments: ["--popup"], + nodeIntegration: true, + partition: "persist:other", + }, + }); + expect(popupWc).toBe(fakes.webContentsInstances.at(-1)); + await popupWc?.loadURL("https://accounts.google.com/gsi/select"); + expect(service.getStatus().tabs).toHaveLength(2); expect(service.getStatus().activeTabId).not.toBe(firstTabId); - expect(response?.createWindow).toBeUndefined(); expect(service.getStatus().tabs.at(-1)).toMatchObject({ url: "https://accounts.google.com/gsi/select", ownerLaneId: "lane-1", ownerChatSessionId: "chat-1", }); - expect(fakes.webContentsInstances.at(-1)?.loadURLCalls.at(-1)).toEqual({ - url: "https://accounts.google.com/gsi/select", - options: { - httpReferrer: { url: "https://example.test/sign-in", policy: "strict-origin-when-cross-origin" }, - postData, - extraHeaders: "content-type: application/x-www-form-urlencoded\n", - }, + expect(fakes.webContentsViewInstances.at(-1)?.webPreferences).toMatchObject({ + additionalArguments: ["--popup"], + partition: service.getStatus().partition, + nodeIntegration: false, + contextIsolation: true, + sandbox: true, + webSecurity: true, + backgroundThrottling: false, }); const openEvent = collector.events.findLast((event) => event.type === "open-request"); diff --git a/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts b/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts index 269c49c8b..27fae28be 100644 --- a/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts +++ b/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts @@ -1131,17 +1131,18 @@ function createBuiltInBrowserWindowService(args: { }); }); wc.setWindowOpenHandler((details) => { - const tab = createPopupTabState(details.url, tabForWebContents(wc) ?? activeTab()); - if (tab) { - const popupUrl = stringOrNull(details.url) ?? "about:blank"; - const popupLoadOptions = loadUrlOptionsForWindowOpen(details); - // Own popup navigation; Electron cannot attach an existing WebContentsView as a native guest window. - void tab.webContents.loadURL(popupUrl, popupLoadOptions).catch((error) => { - emitError(new Error(`Could not open browser popup: ${errorMessage(error)}`)); - emitStatus(); - }); - } - return { action: "deny" }; + const opener = tabForWebContents(wc) ?? activeTab(); + const popupUrl = popupUrlForOpen(details.url); + if (!popupUrl) return { action: "deny" }; + return { + action: "allow", + createWindow: (options) => { + const tab = createPopupTabStateFromView(popupUrl, opener, new WebContentsView({ + webPreferences: browserWebPreferences(options.webPreferences), + })); + return tab.webContents; + }, + }; }); wc.on("will-navigate", (event, url) => { if (isAllowedNavigationUrl(url)) return; @@ -1213,19 +1214,17 @@ function createBuiltInBrowserWindowService(args: { }); }; - const createTabState = (): BrowserTabState => { - configureBrowserSession(); + const browserWebPreferences = (overrides: Electron.WebPreferences = {}): Electron.WebPreferences => ({ + ...overrides, + partition: args.profile.partition, + nodeIntegration: false, + contextIsolation: true, + sandbox: true, + webSecurity: true, + backgroundThrottling: false, + }); - const nextView = new WebContentsView({ - webPreferences: { - partition: args.profile.partition, - nodeIntegration: false, - contextIsolation: true, - sandbox: true, - webSecurity: true, - backgroundThrottling: false, - }, - }); + const createTabStateForView = (nextView: WebContentsView): BrowserTabState => { nextView.setBackgroundColor("#111827"); nextView.setBounds(toElectronRect(bounds)); nextView.setVisible(false); @@ -1250,7 +1249,14 @@ function createBuiltInBrowserWindowService(args: { }; }; - const createPopupTabState = (url: string, opener: BrowserTabState | null = activeTab()): BrowserTabState | null => { + const createTabState = (): BrowserTabState => { + configureBrowserSession(); + return createTabStateForView(new WebContentsView({ + webPreferences: browserWebPreferences(), + })); + }; + + const popupUrlForOpen = (url: string): string | null => { const popupUrl = stringOrNull(url) ?? "about:blank"; if (!isAllowedNavigationUrl(popupUrl)) { emitError(new Error(`Blocked unsupported browser popup protocol: ${url}`)); @@ -1260,7 +1266,16 @@ function createBuiltInBrowserWindowService(args: { emitError(new Error(`ADE browser is limited to ${MAX_BROWSER_TABS} tabs. Close a tab before opening another.`)); return null; } - const tab = createTabState(); + return popupUrl; + }; + + const createPopupTabStateFromView = ( + popupUrl: string, + opener: BrowserTabState | null, + nextView: WebContentsView, + ): BrowserTabState => { + configureBrowserSession(); + const tab = createTabStateForView(nextView); copyTabOwner(opener, tab); tabs = [...tabs, tab]; activeTabId = tab.id; @@ -2908,22 +2923,6 @@ function urlForBrowserLog(value: string): string | null { } } -function loadUrlOptionsForWindowOpen(details: Electron.HandlerDetails): Electron.LoadURLOptions | undefined { - const options: Electron.LoadURLOptions = {}; - if (emptyToNull(details.referrer?.url ?? "")) { - options.httpReferrer = details.referrer; - } - if (details.postBody?.data.length) { - options.postData = details.postBody.data; - let contentType = details.postBody.contentType; - if (details.postBody.boundary && !/;\s*boundary=/i.test(contentType)) { - contentType = `${contentType}; boundary=${details.postBody.boundary}`; - } - options.extraHeaders = `content-type: ${contentType}\n`; - } - return Object.keys(options).length > 0 ? options : undefined; -} - function tabStatus(tab: BrowserTabState): BuiltInBrowserTab { const wc = tab.webContents; return { From e6a63a248cd053936961a38e3187a71afd8dba55 Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Tue, 30 Jun 2026 12:48:04 -0400 Subject: [PATCH 5/5] Ignore untrusted popup web preferences --- .../builtInBrowser/builtInBrowserService.test.ts | 9 +++++++-- .../services/builtInBrowser/builtInBrowserService.ts | 7 +++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts b/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts index 3849853eb..dd95bf0c6 100644 --- a/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts +++ b/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.test.ts @@ -1123,8 +1123,10 @@ describe("createBuiltInBrowserService — bounds and status dedupe", () => { const popupWc = response?.createWindow?.({ webPreferences: { additionalArguments: ["--popup"], + javascript: false, nodeIntegration: true, partition: "persist:other", + webviewTag: true, }, }); expect(popupWc).toBe(fakes.webContentsInstances.at(-1)); @@ -1137,8 +1139,8 @@ describe("createBuiltInBrowserService — bounds and status dedupe", () => { ownerLaneId: "lane-1", ownerChatSessionId: "chat-1", }); - expect(fakes.webContentsViewInstances.at(-1)?.webPreferences).toMatchObject({ - additionalArguments: ["--popup"], + const popupWebPreferences = fakes.webContentsViewInstances.at(-1)?.webPreferences as Record; + expect(popupWebPreferences).toMatchObject({ partition: service.getStatus().partition, nodeIntegration: false, contextIsolation: true, @@ -1146,6 +1148,9 @@ describe("createBuiltInBrowserService — bounds and status dedupe", () => { webSecurity: true, backgroundThrottling: false, }); + expect(popupWebPreferences.additionalArguments).toBeUndefined(); + expect(popupWebPreferences.javascript).toBeUndefined(); + expect(popupWebPreferences.webviewTag).toBeUndefined(); const openEvent = collector.events.findLast((event) => event.type === "open-request"); expect(openEvent).toMatchObject({ diff --git a/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts b/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts index 27fae28be..231987f25 100644 --- a/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts +++ b/apps/desktop/src/main/services/builtInBrowser/builtInBrowserService.ts @@ -1136,9 +1136,9 @@ function createBuiltInBrowserWindowService(args: { if (!popupUrl) return { action: "deny" }; return { action: "allow", - createWindow: (options) => { + createWindow: () => { const tab = createPopupTabStateFromView(popupUrl, opener, new WebContentsView({ - webPreferences: browserWebPreferences(options.webPreferences), + webPreferences: browserWebPreferences(), })); return tab.webContents; }, @@ -1214,8 +1214,7 @@ function createBuiltInBrowserWindowService(args: { }); }; - const browserWebPreferences = (overrides: Electron.WebPreferences = {}): Electron.WebPreferences => ({ - ...overrides, + const browserWebPreferences = (): Electron.WebPreferences => ({ partition: args.profile.partition, nodeIntegration: false, contextIsolation: true,