fix(browser): label popups from committed document origins (#30540)

* fix(browser): label popup chrome from committed documents

* fix(browser): keep opaque popup documents neutral

* test(browser): prepare real popup identity validation

* test(browser): apply background policy in popup probes

* test(browser): fix popup title and view-lifetime checks

* fix(browser): ignore subframe failures in popup chrome

* rm draft docs

* fix(browser): finish popup probe teardown after signal exit

* test(browser): use typed popup probe window access

* fix(browser): prevent automatic popup title updates

* commit

* fix test
This commit is contained in:
Jinjing
2026-10-19 16:13:54 -07:00
committed by GitHub
parent 30d1e8dba5
commit f14475caa6
24 changed files with 1397 additions and 127 deletions
+2
View File
@@ -1182,6 +1182,8 @@ jobs:
src/main/browser/browser-route-h3-egress.electron.test.ts
src/main/browser/browser-route-dns-prefetch.electron.test.ts
src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts
src/main/browser/popup-origin-chrome.electron.test.ts
src/main/browser/browser-link-materialization.electron.test.ts
src/main/browser/browser-popup-compatibility.electron.test.ts
src/main/browser/browser-popup-adapter-compatibility.electron.test.ts
src/main/browser/browser-popup-native-compatibility.electron.test.ts
+2
View File
@@ -36,6 +36,8 @@ export const UNIT_EXCLUDE = [
'tests/e2e/relay-region-compatibility.unit.test.ts',
'tests/e2e/relay-region-correction.unit.test.ts',
// Electron probes with in-process deadlines; the package job runs them serially.
'src/main/browser/popup-origin-chrome.electron.test.ts',
'src/main/browser/browser-link-materialization.electron.test.ts',
'src/main/browser/browser-client-page-renderer-lifecycle.electron.test.ts',
'src/main/browser/browser-route-tcp-egress.electron.test.ts',
'src/main/browser/browser-route-webrtc-egress.electron.test.ts',
+2
View File
@@ -1,4 +1,6 @@
export const LINUX_PACKAGE_TESTS = [
'src/main/browser/popup-origin-chrome.electron.test.ts',
'src/main/browser/browser-link-materialization.electron.test.ts',
'src/main/browser/browser-client-page-renderer-lifecycle.electron.test.ts',
'src/main/browser/browser-route-tcp-egress.electron.test.ts',
'src/main/browser/browser-route-webrtc-egress.electron.test.ts',
@@ -65,6 +65,8 @@ export const NODE_RUNTIME_INCLUDE = [
'src/main/browser/*cookie*.test.ts',
// Playwright's CDP transport needs Node's WebSocket implementation.
'src/main/browser/browser-popup-adapter-compatibility.electron.test.ts',
'src/main/browser/popup-origin-chrome.electron.test.ts',
'src/main/browser/browser-link-materialization.electron.test.ts',
'src/main/browser/browser-popup-compatibility.electron.test.ts',
'src/main/browser/browser-popup-native-compatibility.electron.test.ts',
'src/main/browser/browser-popup-background-compatibility.electron.test.ts',
-44
View File
@@ -1,44 +0,0 @@
# Structured worktree status validation
Validated on September 7, 2026 in a background Electron dev instance of
`pr19217-review-r2`, based on `ce1024096b` with the source-adapter refactor.
CDP app identity confirmed the checkout; CDP screenshots of the full hidden renderer
were reviewed at the time and are not retained here.
The command output is the real `orca worktree ps --json` response reduced to status,
agent state, provider, and pane key for readability.
## Functional correctness
A real Codex structured session appeared as `working` in `worktree.ps` while the
sidebar showed working. Closing its chat tab removed that exact session's row and
returned the worktree to `active`. A different completed chat remained present,
confirming that closure removed only the selected session.
The disappearing session is `codex_40677067_f492_4d7d_86dd_ec566ede04c3`.
The host's held-session roster controls eligibility; its retained broadcast cache
is history, not a roster. Failed eviction intentionally keeps an entry for retry.
## Architecture
PTY reconciliation and process admission belong to the PTY source adapter.
Structured input comes from the current host's held-session projections. One
admitted collection feeds row shaping and worktree aggregation, with no structured
boolean bypass. PTY hooks and retained reports still arrive independently, so their
precedence and conservative remote evidence rules remain necessary. No second
persistent status store or provider polling was introduced.
## Validation and limits
Independent final review found no proven issues. Runtime, host lifecycle, status
feed and source-admission suites passed: 1,344 tests, one skipped. Node typecheck,
targeted lint and diff checks passed. Ablating the runtime call to enumerate
retained history caused the executable call-site test to fail with two rows where
one was expected; restoring the live accessor passed both call-site tests.
Codex working and closure were observed live on macOS. Claude provider turns,
approval/input states, live Windows/Linux/WSL/SSH/relay/mobile scenarios and
release-scale latency/heap measurements remain unverified. Existing tests cover
remote/WSL evidence, monitoring precedence and lifecycle cases. The existing
30-minute freshness rule and CLI activity timestamps are preserved; complete
CLI/sidebar timing parity is not claimed. The wire keeps its existing row shape
and status vocabulary; mobile receives the new rows without a new opcode.
@@ -0,0 +1,62 @@
import { app, BrowserWindow, ipcMain, session, webContents } from 'electron'
import { join } from 'node:path'
import { applyBackgroundActivationPolicy } from '../window/foreground-activation-policy'
if (process.env.ORCA_BACKGROUND_LAUNCH !== '1') {
throw new Error('Popup identity probes require a background launch')
}
applyBackgroundActivationPolicy()
const root = process.env.ORCA_POPUP_IDENTITY_ROOT
if (!root) {
throw new Error('Missing probe root')
}
app.whenReady().then(async () => {
const window = new BrowserWindow({
show: false,
width: 1000,
height: 700,
webPreferences: {
preload: join(root, 'preload.cjs'),
sandbox: false,
contextIsolation: true,
nodeIntegration: false,
webviewTag: true
}
})
ipcMain.handle('browser:setGrabMode', () => undefined)
ipcMain.handle('link-probe:open', (_event, event) =>
window.webContents.send('browser:open-link-in-orca-tab', event)
)
ipcMain.handle('link-probe:register', (_event, args) => {
const guest = webContents.fromId(args.webContentsId)
return guest?.hostWebContents === window.webContents
})
ipcMain.handle('link-probe:guests', () =>
webContents
.getAllWebContents()
.filter((guest) => guest.hostWebContents === window.webContents)
.map((guest) => ({
id: guest.id,
url: guest.mainFrame.url,
isolated: guest.session === session.fromPartition('persist:link-probe-isolated'),
other: guest.session === session.fromPartition('persist:link-probe-default')
}))
)
ipcMain.handle('link-probe:seed', async (_event, origin) => {
await session
.fromPartition('persist:link-probe-isolated')
.cookies.set({ url: origin, name: 'synthetic_profile', value: 'isolated' })
await session
.fromPartition('persist:link-probe-default')
.cookies.set({ url: origin, name: 'synthetic_profile', value: 'other' })
})
window.webContents.on('will-attach-webview', (_event, preferences) => {
preferences.nodeIntegration = false
preferences.contextIsolation = true
preferences.sandbox = true
})
await window.loadFile(join(root, 'index.html'))
})
setTimeout(() => app.exit(1), 120_000).unref()
@@ -0,0 +1,222 @@
import { createServer } from 'node:http'
import { writeFileSync } from 'node:fs'
import { join } from 'node:path'
import type { Page } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
import {
buildPopupIdentityProbe,
createPopupIdentityProbeRoot,
launchPopupIdentityProbe
} from './popup-identity-electron-probe'
const root = createPopupIdentityProbeRoot()
let probe: Awaited<ReturnType<typeof launchPopupIdentityProbe>>
let page: Page
let origin: string
const rendererErrors: string[] = []
type LinkSnapshot = {
tabs: { id: string; sessionProfileId: string | null; sessionPartition: string | null }[]
sourceWorkspaceId: string
selected: string | null
selectedUnified: string[]
unified: { id: string; entityId: string }[]
}
const receipts: { path: string; cookie: string }[] = []
const server = createServer((request, response) => {
receipts.push({ path: request.url ?? '', cookie: request.headers.cookie ?? '' })
response
.writeHead(200, { 'content-type': 'text/html' })
.end('<title>Synthetic destination</title><p>One document, no subresources</p>')
})
beforeAll(async () => {
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
const address = server.address()
if (!address || typeof address === 'string') {
throw new Error('Missing HTTP port')
}
origin = `http://127.0.0.1:${address.port}`
writeFileSync(
join(root, 'preload.ts'),
`
import { contextBridge, ipcRenderer } from 'electron'
import { browserPageInteractionAndSessionsApi } from ${JSON.stringify(join(process.cwd(), 'src/preload/api/browser-bridge-page-interaction-and-sessions.ts'))}
let heldOpen = null
contextBridge.exposeInMainWorld('probeApi', {
browser: { ...browserPageInteractionAndSessionsApi, onOpenLinkInOrcaTab: (callback) => browserPageInteractionAndSessionsApi.onOpenLinkInOrcaTab((event) => { heldOpen = () => callback(event) }), registerGuest: (args) => ipcRenderer.invoke('link-probe:register', args) },
fixture: {
open: (event) => ipcRenderer.invoke('link-probe:open', event),
pending: () => heldOpen !== null,
release: () => { const consume = heldOpen; heldOpen = null; consume?.() },
guests: () => ipcRenderer.invoke('link-probe:guests'),
seed: (origin) => ipcRenderer.invoke('link-probe:seed', origin)
}
})
`
)
writeFileSync(
join(root, 'bootstrap.ts'),
`
import { installBrowserLinkMaterializationProbe } from ${JSON.stringify(join(process.cwd(), 'src/renderer/src/hooks/ipc-events/browser-link-materialization.test-fixture.tsx'))}
try { window.linkProbe = installBrowserLinkMaterializationProbe() } catch (error) { window.linkProbeError = String(error.stack || error) }
`
)
writeFileSync(
join(root, 'index.html'),
`<!doctype html><meta charset="utf-8"><title>Real link materialization probe</title><body><script>
const noSubscribe = () => () => {}
const category = (base = {}) => new Proxy(base, { get: (target, property) => property in target ? target[property] : String(property).startsWith('on') ? noSubscribe : async () => null })
Object.defineProperty(window, 'api', { value: new Proxy({ browser: category(window.probeApi.browser), e2e: undefined, ui: category(), app: category() }, { get: (target, property) => property in target ? target[property] : category() }) })
</script><script src="./renderer.js"></script></body>`
)
await buildPopupIdentityProbe(root, join(root, 'preload.ts'), 'preload.cjs')
await buildPopupIdentityProbe(root, join(root, 'bootstrap.ts'), 'renderer.js', true)
await buildPopupIdentityProbe(
root,
join(process.cwd(), 'src/main/browser/browser-link-materialization-electron-fixture.ts'),
'main.cjs'
)
probe = await launchPopupIdentityProbe(root)
await vi.waitFor(
() =>
expect(
probe.browser
.contexts()
.flatMap((context) => context.pages())
.some((candidate) => candidate.url().endsWith('/index.html'))
).toBe(true),
{ timeout: 10_000 }
)
const found = probe.browser
.contexts()
.flatMap((context) => context.pages())
.find((candidate) => candidate.url().endsWith('/index.html'))
if (!found) {
throw new Error('Missing host renderer')
}
page = found
page.on('pageerror', (error) => rendererErrors.push(String(error)))
}, 90_000)
afterAll(async () => {
await probe?.stop()
server.closeAllConnections()
await new Promise<void>((resolve) => server.close(() => resolve()))
})
async function ready(): Promise<void> {
await expect
.poll(
() =>
page.evaluate('window.linkProbeError || Boolean(window.linkProbe)').then((ready) => {
if (rendererErrors.length) {
throw new Error(rendererErrors.join('\n'))
}
return ready
}),
{ timeout: 10_000 }
)
.toBe(true)
await page.evaluate(`window.probeApi.fixture.seed(${JSON.stringify(origin)})`)
}
async function open(activate: boolean, path: string, hold = false): Promise<void> {
await page.evaluate(
`window.probeApi.fixture.open({ browserPageId: window.linkProbe.sourcePageId, url: ${JSON.stringify(origin + path)}, activate: ${activate} })`
)
await expect.poll(() => page.evaluate('window.probeApi.fixture.pending()')).toBe(true)
if (!hold) {
await page.evaluate('window.probeApi.fixture.release()')
}
}
describe('production link bridge, store and mounted guest', () => {
it.each([false, true])(
'materializes an isolated profile with activation=%s',
async (activate) => {
rendererErrors.length = 0
await page.reload()
await ready()
const path = `/control-${activate}`
await open(activate, path)
const state = await page.evaluate<LinkSnapshot>('window.linkProbe.snapshot()')
const created = state.tabs.find((tab) => tab.id !== state.sourceWorkspaceId)
if (!created) {
throw new Error('No replacement tab materialized')
}
expect(created).toMatchObject({
sessionProfileId: 'isolated-profile',
sessionPartition: 'persist:link-probe-isolated'
})
expect(state.selected).toBe(activate ? created.id : state.sourceWorkspaceId)
const selectedEntity = activate ? created.id : state.sourceWorkspaceId
const selectedUnified = state.unified.find((tab) => tab.entityId === selectedEntity)
expect(state.selectedUnified).toContain(selectedUnified?.id)
await page.evaluate('window.linkProbe.mountTabs()')
await expect
.poll(() => receipts.filter((receipt) => receipt.path === path), { timeout: 10_000 })
.toEqual([{ path, cookie: 'synthetic_profile=isolated' }])
await expect
.poll(() => page.evaluate('window.probeApi.fixture.guests()'), { timeout: 10_000 })
.toEqual([
expect.objectContaining({ url: `${origin}${path}`, isolated: true, other: false })
])
},
25_000
)
it('rejects a removed source page before real store materialization', async () => {
rendererErrors.length = 0
await page.reload()
await ready()
await open(true, '/removed-page', true)
await page.evaluate("window.linkProbe.mutate('remove-page')")
await page.evaluate('window.probeApi.fixture.release()')
await page.evaluate('window.linkProbe.mountTabs()')
expect((await page.evaluate<LinkSnapshot>('window.linkProbe.snapshot()')).tabs).toHaveLength(1)
expect(await page.evaluate('window.probeApi.fixture.guests()')).toEqual([])
expect(receipts.some((receipt) => receipt.path === '/removed-page')).toBe(false)
})
// Known defects until the claim handoff repair; these assertions deliberately describe baseline evidence.
it.each(['remove-tab', 'profile-switch', 'delete-profile'])(
'records unsafe stale-source baseline through the real mount: %s',
async (transition) => {
rendererErrors.length = 0
await page.reload()
await ready()
const path = `/baseline-${transition}`
await open(false, path, true)
await page.evaluate(`window.linkProbe.mutate(${JSON.stringify(transition)})`)
await page.evaluate('window.probeApi.fixture.release()')
await page.evaluate('window.linkProbe.mountTabs()')
await expect
.poll(() => receipts.some((receipt) => receipt.path === path), { timeout: 10_000 })
.toBe(true)
const state = await page.evaluate<LinkSnapshot>('window.linkProbe.snapshot()')
const replacement = state.tabs.find((tab) => tab.id !== state.sourceWorkspaceId)
expect(replacement).toBeDefined()
const expectedProfile = transition === 'delete-profile' ? 'isolated' : 'other'
expect(receipts.filter((receipt) => receipt.path === path)).toEqual([
{ path, cookie: `synthetic_profile=${expectedProfile}` }
])
await expect
.poll(() => page.evaluate('window.probeApi.fixture.guests()'), { timeout: 10_000 })
.toEqual([
expect.objectContaining({
url: `${origin}${path}`,
isolated: expectedProfile === 'isolated',
other: expectedProfile === 'other'
})
])
},
25_000
)
it.todo(
'rejects removed source tabs, profile switches and deleted profiles without a tab, mount, default fallback or destination request'
)
it.todo(
'validates main-owned document/registration claims during delayed IPC consumption, async validation and deferred mount'
)
})
@@ -87,7 +87,6 @@ export abstract class BrowserManagerGuestPopupPolicy extends BrowserManagerNavig
if (isNativeBrowserPopupRequest({ disposition, features, frameName })) {
return allowNativeBrowserPopup({
opener: guest,
targetUrl: url,
options: SAFE_POPUP_WINDOW_OPTIONS,
background: disposition === 'background-tab',
canPresent: () => Boolean(this.resolvePopupOwnerContext(guest.id)),
@@ -98,7 +98,6 @@ export class BrowserPopupAdapterProbeFixture {
unrelatedDuringAdmission = false
const response = allowNativeBrowserPopup({
opener: guest,
targetUrl: details.url,
options: { show: false },
onCreated: ({ nativeWindow }) => {
if (!nativeWindow) {
@@ -186,7 +186,7 @@ export async function observeBackgroundPopupProbe(
expect(status().native.creation.at(-1)).toMatchObject({
firstNavigationPolicy: 'disable_non_proxied_udp',
firstNavigationOwner: expect.any(Number),
firstNavigationTitle: targetOrigin
firstNavigationTitle: 'about:blank'
})
expect(await child.evaluate('window.open("about:blank")===null')).toBe(true)
}
@@ -109,7 +109,7 @@ describe('hidden Chromium native popup request compatibility', () => {
if (name === 'route') {
expect(status().native.creation.at(-1)).toMatchObject({
firstNavigationPolicy: 'disable_non_proxied_udp',
firstNavigationTitle: origin,
firstNavigationTitle: 'about:blank',
firstNavigationOwner: expect.any(Number)
})
}
@@ -21,6 +21,7 @@ afterEach(() => {
function nativeWindow() {
const contents = Object.assign(new EventEmitter(), {
session: {},
mainFrame: { url: '', origin: '' },
isDestroyed: vi.fn(() => false),
close: vi.fn(),
setWindowOpenHandler: vi.fn()
@@ -64,7 +65,6 @@ describe('native popup admission', () => {
const onCreated = vi.fn()
const response = allowNativeBrowserPopup({
opener,
targetUrl: 'https://example.com/post?code=synthetic',
options: { show: true, frame: true },
onCreated
})
@@ -72,7 +72,7 @@ describe('native popup admission', () => {
expect(response.overrideBrowserWindowOptions).toMatchObject({
show: false,
fullscreenable: false,
title: 'https://example.com',
title: 'about:blank',
useContentSize: true,
minWidth: 360,
minHeight: 200,
@@ -91,13 +91,12 @@ describe('native popup admission', () => {
linked.contents.session = opener.contents.session
scripted.contents.session = opener.contents.session
const onScripted = vi.fn(() => {
prepareNativeBrowserPopup(scripted.browserWindow, 'https://example.com/scripted', () => true)
prepareNativeBrowserPopup(scripted.browserWindow, () => true)
return scripted.browserWindow.webContents
})
const admit = (onCreated: () => Electron.WebContents) =>
allowNativeBrowserPopup({
opener: opener.browserWindow.webContents,
targetUrl: 'https://example.com/scripted',
options: {},
onCreated
})
@@ -107,7 +106,6 @@ describe('native popup admission', () => {
const onLinked = vi.fn(() => linked.browserWindow.webContents)
allowNativeBrowserPopup({
opener: opener.browserWindow.webContents,
targetUrl: 'https://example.com/linked',
options: {},
onCreated: onLinked
})
@@ -134,7 +132,6 @@ describe('native popup admission', () => {
const onCreated = vi.fn(() => first.browserWindow.webContents)
allowNativeBrowserPopup({
opener: opener.browserWindow.webContents,
targetUrl: 'https://example.com/scripted',
options: {},
onCreated
})
@@ -159,7 +156,6 @@ describe('native popup admission', () => {
for (const onCreated of [earlier, later]) {
allowNativeBrowserPopup({
opener: opener.browserWindow.webContents,
targetUrl: 'https://example.com/identical',
options: { show: false },
onCreated
})
@@ -184,7 +180,6 @@ describe('native popup admission', () => {
for (const onCreated of [onA, onB]) {
allowNativeBrowserPopup({
opener: opener.browserWindow.webContents,
targetUrl: 'https://example.com/identical',
options: { show: false },
onCreated
})
@@ -208,7 +203,6 @@ describe('native popup admission', () => {
] as const) {
allowNativeBrowserPopup({
opener,
targetUrl: 'https://example.com/identical',
options: { show: false },
onCreated
})
@@ -229,7 +223,6 @@ describe('native popup admission', () => {
const onCreated = vi.fn(() => unrelated.browserWindow.webContents)
allowNativeBrowserPopup({
opener: opener.browserWindow.webContents,
targetUrl: 'https://example.com/identical',
options: { show: false },
onCreated
})
@@ -248,7 +241,6 @@ describe('native popup admission', () => {
const onCreated = vi.fn(() => child.browserWindow.webContents)
allowNativeBrowserPopup({
opener: opener.browserWindow.webContents,
targetUrl: 'https://example.com/identical',
options: { show: false },
onCreated
})
@@ -279,13 +271,11 @@ describe('native popup admission', () => {
const options = { show: state !== 'explicit-hidden' }
const prepare = vi.fn(() =>
state === 'preparation-rejected'
? prepareNativeBrowserPopup(browserWindow, 'https://example.com/post', () => false)
.contentWebContents
? prepareNativeBrowserPopup(browserWindow, () => false).contentWebContents
: browserWindow.webContents
)
const response = allowNativeBrowserPopup({
opener: opener.browserWindow.webContents,
targetUrl: 'https://example.com/post',
options,
background: true,
canPresent: () => {
@@ -326,7 +316,7 @@ describe('native popup admission', () => {
}
])('blocks native navigation and descendants when preparation fails', (prepare) => {
const { browserWindow, contents, window } = nativeWindow()
const popup = prepareNativeBrowserPopup(browserWindow, 'https://example.com/', prepare)
const popup = prepareNativeBrowserPopup(browserWindow, prepare)
for (const event of ['will-navigate', 'will-frame-navigate', 'will-redirect']) {
const preventDefault = vi.fn()
contents.emit(event, { preventDefault })
@@ -340,9 +330,66 @@ describe('native popup admission', () => {
expect(closed).toHaveBeenCalledOnce()
})
it('initializes only from the committed frame and retains it through failed redirects', () => {
const { browserWindow, contents, window } = nativeWindow()
contents.mainFrame.url = 'https://committed.example/private?secret=synthetic'
prepareNativeBrowserPopup(browserWindow)
expect(window.setTitle).toHaveBeenLastCalledWith('https://committed.example')
contents.emit('did-start-navigation', {}, 'https://requested.example/auth')
contents.emit('will-redirect', {}, 'https://redirect.example/auth')
contents.emit('did-fail-load', {}, -3, 'ERR_ABORTED', 'https://redirect.example/auth', true)
contents.emit('did-finish-load')
expect(window.setTitle).toHaveBeenLastCalledWith('https://committed.example')
contents.mainFrame.url = 'chrome-error://chromewebdata/'
contents.emit(
'did-fail-load',
{},
-105,
'ERR_NAME_NOT_RESOLVED',
'https://failed.example/',
true
)
expect(window.setTitle).toHaveBeenLastCalledWith('chrome-error:')
})
it.each([1, 1000])(
'ignores %i subframe failures but still updates main-frame errors',
(count) => {
const { browserWindow, contents, window } = nativeWindow()
contents.mainFrame.url = 'https://committed.example/'
contents.mainFrame.origin = 'https://committed.example'
prepareNativeBrowserPopup(browserWindow)
window.setTitle.mockClear()
for (let index = 0; index < count; index += 1) {
contents.emit('did-fail-load', {}, -2, 'ERR_FAILED', 'https://iframe.example/', false)
}
expect(window.setTitle).not.toHaveBeenCalled()
contents.mainFrame.url = 'https://failed.example/'
contents.mainFrame.origin = 'null'
contents.emit('did-fail-load', {}, -2, 'ERR_FAILED', contents.mainFrame.url, true)
expect(window.setTitle).toHaveBeenCalledExactlyOnceWith('unknown')
}
)
it('uses a neutral title for an opaque network document and retains a surviving origin on abort', () => {
const { browserWindow, contents, window } = nativeWindow()
contents.mainFrame.url = 'https://committed.example/'
contents.mainFrame.origin = 'https://committed.example'
prepareNativeBrowserPopup(browserWindow)
contents.emit('did-fail-load', {}, -3, 'ERR_ABORTED', 'https://failed.example/', true)
expect(window.setTitle).toHaveBeenLastCalledWith('https://committed.example')
contents.mainFrame.url = 'https://failed.example/'
contents.mainFrame.origin = 'null'
contents.emit('did-fail-load', {}, -2, 'ERR_FAILED', 'https://failed.example/', true)
expect(window.setTitle).toHaveBeenLastCalledWith('unknown')
})
it('protects the title and updates only main-frame origins, including redirects', () => {
const { browserWindow, contents, window } = nativeWindow()
prepareNativeBrowserPopup(browserWindow, 'https://example.com/post?code=synthetic', () => true)
prepareNativeBrowserPopup(browserWindow, () => true)
expect(window.setTitle).toHaveBeenLastCalledWith('about:blank')
const preventDefault = vi.fn()
window.emit('page-title-updated', { preventDefault }, 'Spoofed title')
expect(preventDefault).toHaveBeenCalledOnce()
@@ -356,6 +403,7 @@ describe('native popup admission', () => {
true
)
expect(window.setTitle).toHaveBeenCalledTimes(1)
contents.mainFrame.url = 'https://destination.example.net/other'
contents.emit('did-navigate', {}, 'https://destination.example.net/other')
expect(window.setTitle).toHaveBeenLastCalledWith('https://destination.example.net')
})
+13 -10
View File
@@ -1,12 +1,12 @@
import { app, type BrowserWindow, type WebContents } from 'electron'
import { isBackgroundLaunch } from '../window/foreground-activation-policy'
import { safeOrigin } from './browser-manager-types'
import type { PopupChildWindowOptions, PopupOriginBarWindow } from './popup-origin-bar-window'
import {
isNativeBrowserPopupWindow,
markNativeBrowserPopupWindow
} from './native-browser-popup-windows'
import { MIN_POPUP_CONTENT_HEIGHT, MIN_POPUP_CONTENT_WIDTH } from './popup-window-minimum-size'
import { describeCommittedPopupOrigin } from './popup-committed-origin'
const rejectedNativePopups = new WeakSet<BrowserWindow>()
// Why: like Electron, an opener's latest script admission answers the next child it shows.
@@ -34,7 +34,6 @@ export function isNativeBrowserPopupRequest(input: {
*/
export function allowNativeBrowserPopup(input: {
opener: WebContents
targetUrl: string
options: Electron.BrowserWindowConstructorOptions
background?: boolean
canPresent?: () => boolean
@@ -90,7 +89,7 @@ export function allowNativeBrowserPopup(input: {
action: 'allow',
overrideBrowserWindowOptions: {
...input.options,
title: safeOrigin(input.targetUrl),
title: 'about:blank',
fullscreenable: false,
show: mayPresent && !input.background,
// Content-sized like origin-bar popups, so resizeTo cannot hide the origin title.
@@ -159,7 +158,6 @@ function rejectNativeBrowserPopup(window: BrowserWindow): void {
/** The native frame is the trust surface; public APIs cannot reparent its primary contents. */
export function prepareNativeBrowserPopup(
window: BrowserWindow,
initialUrl: string,
prepareContent?: (contents: WebContents) => boolean
): PopupOriginBarWindow {
const contents = window.webContents
@@ -195,19 +193,24 @@ export function prepareNativeBrowserPopup(
rejectNativeBrowserPopup(window)
return popup
}
const updateOrigin = (url: string): void => {
const updateOrigin = (): void => {
if (!window.isDestroyed()) {
window.setTitle(safeOrigin(url))
window.setTitle(describeCommittedPopupOrigin(contents).label)
}
}
window.on('page-title-updated', (event) => event.preventDefault())
// A blocked/uncommitted redirect must never lend its origin to the old document.
contents.on('did-navigate', (_event, url) => updateOrigin(url))
contents.on('did-navigate-in-page', (_event, url, isMainFrame) => {
contents.on('did-navigate', updateOrigin)
contents.on('did-navigate-in-page', (_event, _url, isMainFrame) => {
if (isMainFrame) {
updateOrigin(url)
updateOrigin()
}
})
updateOrigin(initialUrl)
contents.on('did-fail-load', (_event, _errorCode, _errorDescription, _url, isMainFrame) => {
if (isMainFrame) {
updateOrigin()
}
})
updateOrigin()
return popup
}
@@ -203,7 +203,6 @@ export function createBrowserRouteGuestPopupController(input: {
return allowNativeBrowserPopup({
// Electron reports a script-opened child to the contents whose handler answered.
opener: source,
targetUrl: details?.url ?? normalized,
options,
background: details?.disposition === 'background-tab',
canPresent: () => !disposed && navigationAllowed(ORCA_BROWSER_BLANK_URL),
@@ -0,0 +1,19 @@
import { describePopupOrigin } from './popup-origin-description'
/** RenderFrameHost's last committed URL excludes pending destinations and page titles. */
export function describeCommittedPopupOrigin(contents: Electron.WebContents): {
label: string
insecure: boolean
} {
try {
const frame = contents.mainFrame
const url = frame.url || 'about:blank'
// Error and sandboxed documents can retain a network URL with an opaque security origin.
if (frame.origin === 'null' && URL.canParse(url) && new URL(url).origin !== 'null') {
return { label: 'unknown', insecure: true }
}
return describePopupOrigin(url)
} catch {
return { label: 'about:blank', insecure: false }
}
}
@@ -0,0 +1,53 @@
import { EventEmitter } from 'node:events'
import { existsSync, rmSync } from 'node:fs'
import { afterEach, expect, it, vi } from 'vitest'
import {
createPopupIdentityProbeRoot,
launchPopupIdentityProbe
} from './popup-identity-electron-probe'
const mocks = vi.hoisted(() => ({ spawn: vi.fn(), closeBrowser: vi.fn() }))
vi.mock('@orca/process-host', () => ({ spawnProcess: mocks.spawn }))
vi.mock('playwright', () => ({
chromium: { connectOverCDP: vi.fn(async () => ({ close: mocks.closeBrowser })) }
}))
vi.mock('./browser-session-ua-cdp-collector', () => ({
waitForBrowserCdpEndpoint: vi.fn(async () => undefined)
}))
const roots: string[] = []
afterEach(() => {
for (const root of roots.splice(0)) {
rmSync(root, { recursive: true, force: true })
}
vi.clearAllMocks()
})
it.each([
{ exitCode: 0, signalCode: null },
{ exitCode: null, signalCode: 'SIGTERM' }
] as const)(
'cleans up a probe whose child already exited with $exitCode / $signalCode',
async ({ exitCode, signalCode }) => {
const child = Object.assign(new EventEmitter(), {
stderr: new EventEmitter(),
exitCode,
signalCode,
kill: vi.fn()
})
mocks.spawn.mockReturnValue(child)
const root = createPopupIdentityProbeRoot()
roots.push(root)
const probe = await launchPopupIdentityProbe(root)
child.emit('exit', exitCode, signalCode)
await probe.stop()
expect(mocks.closeBrowser).toHaveBeenCalledOnce()
expect(child.listenerCount('exit')).toBe(0)
expect(existsSync(root)).toBe(false)
},
2_000
)
@@ -0,0 +1,107 @@
import { spawnProcess } from '@orca/process-host'
import { mkdtempSync, rmSync } from 'node:fs'
import { createServer } from 'node:net'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { chromium } from 'playwright'
import { build } from 'vite'
import { waitForBrowserCdpEndpoint } from './browser-session-ua-cdp-collector'
import { resolveElectronProbeLaunch } from './electron-probe-display-launch'
export function createPopupIdentityProbeRoot(): string {
return mkdtempSync(join(tmpdir(), 'orca-popup-identity-'))
}
export async function buildPopupIdentityProbe(
root: string,
entry: string,
fileName: string,
renderer = false
): Promise<void> {
await build({
configFile: false,
logLevel: 'silent',
oxc: { jsx: { development: false } },
resolve: { alias: { '@': join(process.cwd(), 'src/renderer/src') } },
define: {
ORCA_FEATURE_WALL_ENABLED: 'true',
ORCA_BUILD_COMMIT: JSON.stringify('0'.repeat(40)),
'import.meta.env.DEV': 'false',
'process.env.NODE_ENV': JSON.stringify('production')
},
build: {
emptyOutDir: false,
lib: {
entry,
formats: [renderer ? 'iife' : 'cjs'],
name: 'PopupIdentityProbe',
fileName: () => fileName
},
outDir: root,
target: renderer ? 'chrome140' : 'node22',
rollupOptions: {
external: renderer ? [] : ['electron', /^node:/],
output: { inlineDynamicImports: true }
}
}
})
}
export async function launchPopupIdentityProbe(root: string, electronArgs: readonly string[] = []) {
const portServer = createServer()
await new Promise<void>((resolve) => portServer.listen(0, '127.0.0.1', resolve))
const address = portServer.address()
if (!address || typeof address === 'string') {
throw new Error('Missing probe port')
}
const port = address.port
await new Promise<void>((resolve) => portServer.close(() => resolve()))
const electronBinary: unknown = createRequire(import.meta.url)('electron')
if (typeof electronBinary !== 'string') {
throw new Error('Missing Electron executable')
}
const { ELECTRON_RUN_AS_NODE: _runAsNode, ...env } = process.env
const { executable, args } = resolveElectronProbeLaunch({
electronBinary,
electronArgs: [
...electronArgs,
join(root, 'main.cjs'),
`--user-data-dir=${join(root, 'profile')}`,
`--remote-debugging-port=${port}`
],
platform: process.platform,
display: env.DISPLAY
})
const child = spawnProcess({
program: executable,
args,
env: { ...env, ORCA_BACKGROUND_LAUNCH: '1', ORCA_POPUP_IDENTITY_ROOT: root },
stdio: ['ignore', 'pipe', 'pipe']
})
const diagnostics: string[] = []
child.stderr?.on('data', (data: Buffer) => diagnostics.push(data.toString()))
try {
await waitForBrowserCdpEndpoint(port)
const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`)
return {
browser,
diagnostics,
stop: async (): Promise<void> => {
await browser.close()
child.kill()
await new Promise<void>((resolve) => {
if (child.exitCode !== null || child.signalCode !== null) {
resolve()
} else {
child.once('exit', () => resolve())
}
})
rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
}
}
} catch (error) {
child.kill()
throw new Error(`${String(error)}\n${diagnostics.join('')}`)
}
}
@@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
type Handler = (...args: unknown[]) => void
type FakeWebContents = {
mainFrame: { url: string; origin: string }
on: ReturnType<typeof vi.fn>
once: ReturnType<typeof vi.fn>
off: ReturnType<typeof vi.fn>
@@ -21,7 +22,9 @@ const { fakeElectron } = vi.hoisted(() => {
const add = (event: string, handler: Handler): void => {
handlers.set(event, [...(handlers.get(event) ?? []), handler])
}
const mainFrame = { url: '', origin: '' }
return {
mainFrame,
on: vi.fn(add),
once: vi.fn(add),
off: vi.fn((event: string, handler: Handler) => {
@@ -38,6 +41,9 @@ const { fakeElectron } = vi.hoisted(() => {
// 'destroyed' handler, which is what the double-close guard relies on.
isDestroyed: vi.fn(() => destroyed),
emit: (event: string, ...args: unknown[]) => {
if (event === 'did-navigate' || (event === 'did-navigate-in-page' && args[2])) {
mainFrame.url = String(args[1])
}
if (event === 'destroyed') {
destroyed = true
}
@@ -304,20 +310,22 @@ describe('openPopupWithOriginBar', () => {
})
})
it('renders only the origin in the bar, never path or query', () => {
it('renders only the committed origin in the bar, never path or query', () => {
const adopted = createFakeWebContents()
adopted.mainFrame.url = 'https://accounts.example.com/oauth?code=SECRET'
openPopupWithOriginBar(
{ webContents: createFakeWebContents() as never },
{ webContents: adopted as never },
'https://accounts.example.com/oauth?code=SECRET'
)
const { bar } = lastViews()
bar.webContents.emit('did-finish-load')
expect(bar.webContents.executeJavaScript).toHaveBeenCalledTimes(1)
expect(bar.webContents.executeJavaScript).toHaveBeenCalledTimes(2)
const script = bar.webContents.executeJavaScript.mock.calls[0][0] as string
expect(script).toContain('"https://accounts.example.com"')
expect(script).not.toContain('SECRET')
expect(script).not.toContain('/oauth')
expect(lastWindow().options.title).toBe('https://accounts.example.com')
expect(lastWindow().options.title).toBe('about:blank')
})
it('updates the origin bar and title when the popup navigates', () => {
@@ -342,10 +350,16 @@ describe('openPopupWithOriginBar', () => {
{ webContents: adopted as never },
'https://accounts.example.com/oauth?code=SECRET'
)
// Until the page supplies a title, the window title is the origin.
expect(lastWindow().options.title).toBe('https://accounts.example.com')
expect(lastWindow().options.title).toBe('about:blank')
const blankTitleEvent = { preventDefault: vi.fn() }
adopted.emit('page-title-updated', blankTitleEvent, 'Uncommitted destination')
expect(blankTitleEvent.preventDefault).toHaveBeenCalledOnce()
expect(lastWindow().setTitle).toHaveBeenLastCalledWith('about:blank')
adopted.emit('did-navigate', {}, 'https://accounts.example.com/oauth')
adopted.emit('page-title-updated', {}, 'Sign in to Example')
const committedTitleEvent = { preventDefault: vi.fn() }
adopted.emit('page-title-updated', committedTitleEvent, 'Sign in to Example')
expect(committedTitleEvent.preventDefault).toHaveBeenCalledOnce()
expect(lastWindow().setTitle).toHaveBeenLastCalledWith('Sign in to Example')
// A stale title must not survive a cross-origin navigation.
@@ -353,6 +367,128 @@ describe('openPopupWithOriginBar', () => {
expect(lastWindow().setTitle).toHaveBeenLastCalledWith('https://evil.example.net')
})
it.each(['about:blank', 'about:blank#state', 'about:blank?state=1'])(
'keeps opener-written %s content neutral across failed navigation and delayed bar loading',
(url) => {
const adopted = createFakeWebContents()
adopted.mainFrame.url = url
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The fixture implements the presenter's WebContents surface.
openPopupWithOriginBar({ webContents: adopted as never }, 'https://requested.example/auth')
const { bar } = lastViews()
lastWindow().setTitle.mockClear()
adopted.emit('page-title-updated', { preventDefault: vi.fn() }, 'https://requested.example')
expect(lastWindow().setTitle).not.toHaveBeenCalled()
adopted.emit('did-start-navigation', {}, 'https://requested.example/auth')
adopted.emit('will-redirect', {}, 'https://redirect.example/auth')
adopted.emit('did-fail-load', {}, -3, 'ERR_ABORTED', 'https://redirect.example/auth', true)
adopted.emit('did-finish-load')
bar.webContents.emit('did-finish-load')
expect(lastWindow().setTitle).toHaveBeenLastCalledWith('about:blank')
expect(bar.webContents.executeJavaScript.mock.calls.at(-1)?.[0]).toContain('"about:blank"')
expect(bar.webContents.executeJavaScript.mock.calls.flat().join('')).not.toContain(
'requested.example'
)
}
)
it('checks the committed document when a title arrives before its navigation event', () => {
const adopted = createFakeWebContents()
adopted.mainFrame.url = 'https://committed.example/'
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The fixture implements the presenter's WebContents surface.
openPopupWithOriginBar({ webContents: adopted as never }, 'https://requested.example/')
lastWindow().setTitle.mockClear()
adopted.mainFrame.url = 'about:blank#state'
adopted.emit(
'page-title-updated',
{ preventDefault: vi.fn() },
'https://accounts.trusted.example'
)
expect(lastWindow().setTitle).not.toHaveBeenCalled()
lastViews().bar.webContents.emit('did-finish-load')
expect(lastWindow().setTitle).toHaveBeenLastCalledWith('about:blank')
})
it('retains the committed origin through pending redirects and renders the latest commit when the bar loads', () => {
const adopted = createFakeWebContents()
adopted.mainFrame.url = 'https://committed.example/private'
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The fixture implements the presenter's WebContents surface.
openPopupWithOriginBar({ webContents: adopted as never }, 'https://requested.example/')
const { bar } = lastViews()
adopted.emit('will-redirect', {}, 'https://pending.example/')
adopted.emit('did-fail-load', {}, -3, 'ERR_ABORTED', 'https://pending.example/', true)
adopted.emit('did-finish-load')
expect(bar.webContents.executeJavaScript.mock.calls.at(-1)?.[0]).toContain(
'"https://committed.example"'
)
adopted.emit('did-navigate', {}, 'http://next.example/path?secret=synthetic')
bar.webContents.emit('did-finish-load')
expect(bar.webContents.executeJavaScript.mock.calls.at(-1)?.[0]).toContain(
'"http://next.example"'
)
adopted.mainFrame.url = 'chrome-error://chromewebdata/'
adopted.emit(
'did-fail-load',
{},
-105,
'ERR_NAME_NOT_RESOLVED',
'https://failed.example/',
true
)
expect(bar.webContents.executeJavaScript.mock.calls.at(-1)?.[0]).toContain('"chrome-error:"')
})
it.each([1, 1000])('preserves the page title through %i subframe failures', (count) => {
const adopted = createFakeWebContents()
adopted.mainFrame.url = 'https://committed.example/'
adopted.mainFrame.origin = 'https://committed.example'
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The fixture implements the presenter's WebContents surface.
openPopupWithOriginBar({ webContents: adopted as never }, adopted.mainFrame.url)
const { bar } = lastViews()
bar.webContents.emit('did-finish-load')
adopted.emit('page-title-updated', { preventDefault: vi.fn() }, 'Stable popup title')
bar.webContents.executeJavaScript.mockClear()
const titleUpdates = lastWindow().setTitle.mock.calls.length
for (let index = 0; index < count; index += 1) {
adopted.emit('did-fail-load', {}, -2, 'ERR_FAILED', 'https://iframe.example/', false)
}
expect(lastWindow().setTitle).toHaveBeenCalledTimes(titleUpdates)
expect(lastWindow().setTitle).toHaveBeenLastCalledWith('Stable popup title')
expect(bar.webContents.executeJavaScript).not.toHaveBeenCalled()
adopted.mainFrame.url = 'https://failed.example/'
adopted.mainFrame.origin = 'null'
adopted.emit('did-fail-load', {}, -2, 'ERR_FAILED', adopted.mainFrame.url, true)
expect(lastWindow().setTitle).toHaveBeenCalledTimes(titleUpdates + 1)
expect(lastWindow().setTitle).toHaveBeenLastCalledWith('unknown')
expect(bar.webContents.executeJavaScript).toHaveBeenCalledTimes(1)
expect(bar.webContents.executeJavaScript.mock.calls[0][0]).toContain('"unknown"')
})
it('never labels an opaque committed error or sandboxed document with its network URL', () => {
const adopted = createFakeWebContents()
adopted.mainFrame.url = 'https://opaque.example/document'
adopted.mainFrame.origin = 'null'
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The fixture implements the presenter's WebContents surface.
openPopupWithOriginBar({ webContents: adopted as never }, 'https://requested.example/')
const { bar } = lastViews()
bar.webContents.emit('did-finish-load')
expect(bar.webContents.executeJavaScript.mock.calls.at(-1)?.[0]).toContain('"unknown"')
adopted.mainFrame.origin = 'https://committed.example'
adopted.emit('did-navigate', {}, 'https://committed.example/')
adopted.emit('did-fail-load', {}, -3, 'ERR_ABORTED', 'https://failed.example/', true)
expect(bar.webContents.executeJavaScript.mock.calls.at(-1)?.[0]).toContain(
'"https://committed.example"'
)
adopted.mainFrame.url = 'https://failed.example/'
adopted.mainFrame.origin = 'null'
adopted.emit('did-fail-load', {}, -2, 'ERR_FAILED', 'https://failed.example/', true)
expect(bar.webContents.executeJavaScript.mock.calls.at(-1)?.[0]).toContain('"unknown"')
expect(bar.webContents.executeJavaScript.mock.calls.at(-1)?.[0]).toContain(
"classList.toggle('insecure', true)"
)
})
it('closes the window when the popup content is destroyed, without re-closing the contents', () => {
const adopted = createFakeWebContents()
openPopupWithOriginBar({ webContents: adopted as never }, 'https://example.com/')
@@ -374,6 +510,8 @@ describe('openPopupWithOriginBar', () => {
bar.webContents.emit('did-finish-load')
bar.webContents.executeJavaScript.mockClear()
adopted.emit('did-navigate', {}, 'https://example.com/login')
bar.webContents.executeJavaScript.mockClear()
adopted.emit('did-finish-load')
expect(bar.webContents.executeJavaScript).toHaveBeenCalledTimes(1)
+39 -42
View File
@@ -5,6 +5,8 @@ import {
prepareNativeBrowserPopup
} from './browser-popup-native-window'
import { MIN_POPUP_CONTENT_HEIGHT, MIN_POPUP_CONTENT_WIDTH } from './popup-window-minimum-size'
import { describeCommittedPopupOrigin } from './popup-committed-origin'
export { describePopupOrigin } from './popup-origin-description'
// Why: Electron passes the pre-created popup WebContents through the
// createWindow options (present at runtime, absent from the published d.ts).
@@ -28,37 +30,6 @@ export const POPUP_ORIGIN_BAR_HEIGHT = 34
const DEFAULT_POPUP_CONTENT_WIDTH = 800
const DEFAULT_POPUP_CONTENT_HEIGHT = 600
// Why: http on loopback is a secure context (local OAuth callback servers are
// common); only flag plain http to a real remote host.
function isLoopbackHost(hostname: string): boolean {
return (
hostname === 'localhost' ||
hostname.endsWith('.localhost') ||
hostname === '127.0.0.1' ||
hostname === '[::1]'
)
}
// Why: the bar must show only the origin — popup URLs routinely carry OAuth
// codes and one-time tokens in path/query that must never reach UI surfaces.
export function describePopupOrigin(rawUrl: string): { label: string; insecure: boolean } {
try {
const parsed = new URL(rawUrl)
if (parsed.origin !== 'null') {
return {
label: parsed.origin,
insecure: parsed.protocol === 'http:' && !isLoopbackHost(parsed.hostname)
}
}
if (parsed.protocol === 'about:') {
return { label: 'about:blank', insecure: false }
}
return { label: parsed.protocol, insecure: false }
} catch {
return { label: 'unknown', insecure: true }
}
}
// Colors mirror the canonical tokens in src/renderer/src/assets/main.css
// (--background/--foreground/--border/--destructive); a data: URL page cannot
// import that stylesheet, so the values are inlined per theme here.
@@ -149,10 +120,9 @@ export function openPopupWithOriginBar(
prepareContent?: (contents: Electron.WebContents) => boolean
): PopupOriginBarWindow {
if (options.nativeWindow) {
return prepareNativeBrowserPopup(options.nativeWindow, initialUrl, prepareContent)
return prepareNativeBrowserPopup(options.nativeWindow, prepareContent)
}
const { width, height } = clampPopupContentSize(options)
const initialOrigin = describePopupOrigin(initialUrl)
const window = new BaseWindow({
width,
height: height + POPUP_ORIGIN_BAR_HEIGHT,
@@ -164,7 +134,7 @@ export function openPopupWithOriginBar(
: {}),
minWidth: MIN_POPUP_CONTENT_WIDTH,
minHeight: MIN_POPUP_CONTENT_HEIGHT + POPUP_ORIGIN_BAR_HEIGHT,
title: initialOrigin.label,
title: 'about:blank',
// Why: Windows/Linux attach Orca's app menu to every window, including web popups.
autoHideMenuBar: true,
show:
@@ -215,9 +185,8 @@ export function openPopupWithOriginBar(
) {
return closeUnpreparedPopup(window, contentWebContents, originBarWebContents)
}
let currentUrl = initialUrl
const renderOrigin = (): void => {
const { label, insecure } = describePopupOrigin(currentUrl)
const { label, insecure } = describeCommittedPopupOrigin(contentWebContents)
// Why: origin is the title only until the page supplies one — the bar
// below stays the trust surface, so the native title bar can show the
// page title (Chrome popup behavior) instead of doubling the origin.
@@ -239,21 +208,47 @@ export function openPopupWithOriginBar(
`data:text/html;charset=utf-8,${encodeURIComponent(ORIGIN_BAR_HTML)}`
)
const handleDidNavigate = (_event: Electron.Event, url: string): void => {
currentUrl = url
contentWebContents.on('did-navigate', renderOrigin)
const handleInPageNavigate = (
_event: Electron.Event,
_url: string,
isMainFrame: boolean
): void => {
if (isMainFrame) {
renderOrigin()
}
}
contentWebContents.on('did-navigate-in-page', handleInPageNavigate)
const handleLoadFailure = (
_event: Electron.Event,
_errorCode: number,
_errorDescription: string,
_url: string,
isMainFrame: boolean
): void => {
if (!isMainFrame) {
return
}
// Error documents omit did-navigate; read the committed frame, never the failed target.
renderOrigin()
}
contentWebContents.on('did-navigate', handleDidNavigate)
contentWebContents.on('did-fail-load', handleLoadFailure)
// Why: origin writes fail silently if the bar is mid-load; re-asserting at
// load completion means a dropped write can never leave a stale origin up
// for the lifetime of the page.
contentWebContents.on('did-finish-load', renderOrigin)
const handlePageTitleUpdated = (_event: Electron.Event, title: string): void => {
if (!window.isDestroyed() && title) {
const handlePageTitleUpdated = (event: Electron.Event, title: string): void => {
event.preventDefault()
if (
!window.isDestroyed() &&
title &&
describeCommittedPopupOrigin(contentWebContents).label !== 'about:blank'
) {
window.setTitle(title)
}
}
contentWebContents.on('page-title-updated', handlePageTitleUpdated)
renderOrigin()
if (options.loadInitialUrl && !options.webContents) {
void contentWebContents.loadURL(initialUrl).catch(() => {})
@@ -269,7 +264,9 @@ export function openPopupWithOriginBar(
window.once('closed', () => {
if (!contentWebContents.isDestroyed()) {
contentWebContents.off('destroyed', handleContentDestroyed)
contentWebContents.off('did-navigate', handleDidNavigate)
contentWebContents.off('did-navigate', renderOrigin)
contentWebContents.off('did-navigate-in-page', handleInPageNavigate)
contentWebContents.off('did-fail-load', handleLoadFailure)
contentWebContents.off('did-finish-load', renderOrigin)
contentWebContents.off('page-title-updated', handlePageTitleUpdated)
// Why: close() (not destroy) so the page's unload handlers run — OAuth
@@ -0,0 +1,199 @@
import { app, BaseWindow, BrowserWindow, WebContentsView } from 'electron'
import { mkdirSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { openPopupWithOriginBar } from './popup-origin-bar-window'
import { prepareNativeBrowserPopup } from './browser-popup-native-window'
import { applyBackgroundActivationPolicy } from '../window/foreground-activation-policy'
if (process.env.ORCA_BACKGROUND_LAUNCH !== '1') {
throw new Error('Popup identity probes require a background launch')
}
applyBackgroundActivationPolicy()
app.commandLine.appendSwitch('host-resolver-rules', 'MAP popup-error.invalid 127.0.0.1')
app.commandLine.appendSwitch('no-proxy-server')
const root = process.env.ORCA_POPUP_IDENTITY_ROOT
if (!root) {
throw new Error('Missing probe root')
}
const statusPath = join(root, 'status.json')
const attachedSourceViews = new Set<WebContentsView>()
let active: Electron.WebContents | undefined
let presenter: BaseWindow | undefined
let parent: BrowserWindow
let kind = 'adopted'
let pageTitle: string | undefined
function trackPageTitle(contents: Electron.WebContents): void {
pageTitle = undefined
contents.on('page-title-updated', (_event, title) => {
pageTitle = title
})
}
function adoptedWindow(contents: Electron.WebContents): BaseWindow | undefined {
return BaseWindow.getAllWindows().find((window) =>
window.contentView.children.some(
(view) => view instanceof WebContentsView && view.webContents === contents
)
)
}
async function command(input: {
id: number
kind?: string
url?: string
action: string
stage?: string
}) {
if (input.action === 'collect-garbage') {
if (typeof globalThis.gc !== 'function') {
throw new Error('Popup lifetime checks require --js-flags=--expose-gc')
}
globalThis.gc()
await new Promise<void>((resolve) => setImmediate(resolve))
globalThis.gc()
await new Promise<void>((resolve) => setImmediate(resolve))
}
if (input.action === 'kind') {
kind = input.kind ?? 'adopted'
}
if (input.action === 'attach') {
if (input.kind === 'native') {
const window = new BrowserWindow({
show: false,
webPreferences: { sandbox: true, contextIsolation: true, nodeIntegration: false }
})
await window.loadURL(input.url ?? 'about:blank')
prepareNativeBrowserPopup(window)
active = window.webContents
trackPageTitle(active)
presenter = window
} else {
const sourceView = new WebContentsView({
webPreferences: { sandbox: true, contextIsolation: true, nodeIntegration: false }
})
// Collecting the source view would destroy its contents even after adoption.
attachedSourceViews.add(sourceView)
const contents = sourceView.webContents
contents.once('destroyed', () => attachedSourceViews.delete(sourceView))
await contents.loadURL(input.url ?? 'about:blank')
openPopupWithOriginBar(
{
webContents: contents,
webPreferences: { sandbox: true, contextIsolation: true, nodeIntegration: false },
show: false
},
'https://uncommitted.invalid/'
)
active = contents
trackPageTitle(active)
presenter = adoptedWindow(contents)
}
}
if (input.action === 'block') {
active?.on('will-navigate', (event, url) => {
if (url.endsWith('/blocked')) {
event.preventDefault()
}
})
}
if (input.action === 'navigate') {
void active?.loadURL(input.url ?? 'about:blank').catch(() => {})
}
if (input.action === 'stop') {
active?.stop()
}
if (input.action === 'close') {
presenter?.close()
active = undefined
presenter = undefined
}
const bar = presenter?.contentView.children.find(
(view) => view instanceof WebContentsView && view.webContents !== active
)
if (input.action === 'capture' && bar instanceof WebContentsView) {
const artifacts = process.env.ORCA_POPUP_IDENTITY_ARTIFACT_DIR
if (
artifacts &&
(input.stage === 'pending' || input.stage === 'committed' || input.stage === 'opaque-error')
) {
mkdirSync(artifacts, { recursive: true })
bar.webContents.debugger.attach('1.3')
try {
await bar.webContents.debugger.sendCommand('Page.enable')
const screenshot = await bar.webContents.debugger.sendCommand('Page.captureScreenshot')
writeFileSync(
join(artifacts, `adopted-origin-${input.stage}.png`),
Buffer.from(screenshot.data, 'base64')
)
} finally {
bar.webContents.debugger.detach()
}
}
}
let barState: unknown = null
if (bar instanceof WebContentsView) {
barState = await bar.webContents
.executeJavaScript(
"({label: document.querySelector('#origin')?.textContent, insecure: document.body.classList.contains('insecure')})"
)
.catch(() => null)
}
writeFileSync(
statusPath,
JSON.stringify({
id: input.id,
electron: process.versions.electron,
kind,
title: presenter?.getTitle(),
pageTitle,
committedUrl: active?.mainFrame.url,
committedOrigin: active?.mainFrame.origin,
bar: barState,
windowsHidden: BaseWindow.getAllWindows().every((window) => !window.isVisible())
})
)
}
app.whenReady().then(async () => {
parent = new BrowserWindow({
show: false,
webPreferences: { sandbox: true, contextIsolation: true, nodeIntegration: false }
})
parent.webContents.setWindowOpenHandler(() => {
if (kind === 'native') {
return { action: 'allow', overrideBrowserWindowOptions: { show: false } }
}
return {
action: 'allow',
createWindow: (options) => {
const popup = openPopupWithOriginBar(
{ ...options, show: false },
'https://uncommitted.invalid/'
)
active = popup.contentWebContents
trackPageTitle(active)
presenter = adoptedWindow(active)
return active
}
}
})
parent.webContents.on('did-create-window', (window) => {
active = window.webContents
trackPageTitle(active)
presenter = window
prepareNativeBrowserPopup(window)
})
parent.webContents.on('console-message', (_event, _level, message) => {
if (!message.startsWith('origin-probe:')) {
return
}
void command(JSON.parse(message.slice('origin-probe:'.length))).catch((error) =>
writeFileSync(statusPath, JSON.stringify({ error: String(error) }))
)
})
await parent.loadURL('data:text/html,<title>Origin presenter probe</title>')
})
setTimeout(() => app.exit(1), 90_000).unref()
@@ -0,0 +1,265 @@
import { createServer, type ServerResponse } from 'node:http'
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import type { Page } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
import {
buildPopupIdentityProbe,
createPopupIdentityProbeRoot,
launchPopupIdentityProbe
} from './popup-identity-electron-probe'
import { startHiddenRendererFrameCapture } from './hidden-renderer-frame-capture-fixture'
type OriginProbeWindow = Window & { originProbeChild?: Window }
type Snapshot = {
id: number
error?: string
electron: string
title?: string
pageTitle?: string
committedUrl?: string
committedOrigin?: string
windowsHidden: boolean
bar: { label: string; insecure: boolean } | null
}
const held: ServerResponse[] = []
const requestedPaths: string[] = []
const root = createPopupIdentityProbeRoot()
const server = createServer((request, response) => {
requestedPaths.push(request.url ?? '')
if (request.url?.startsWith('/hold')) {
held.push(response)
return
}
if (request.url === '/redirect') {
setTimeout(() => response.writeHead(302, { location: '/hold-redirect' }).end(), 100)
return
}
if (request.url === '/failure') {
response.destroy()
return
}
response
.writeHead(200, { 'content-type': 'text/html' })
.end('<title>Page controlled title</title><p>Committed document</p>')
})
let origin: string
let probe: Awaited<ReturnType<typeof launchPopupIdentityProbe>>
let page: Page
let id = 0
const observations: Snapshot[] = []
let stopCapture: (() => Promise<void>) | undefined
async function command(
action: string,
options: { url?: string; kind?: string; stage?: string } = {}
): Promise<Snapshot> {
const commandId = ++id
await page.evaluate((input) => console.log(`origin-probe:${JSON.stringify(input)}`), {
id: commandId,
action,
...options
})
const path = join(root, 'status.json')
await expect
.poll(
() => {
if (!existsSync(path)) {
return false
}
const result = JSON.parse(readFileSync(path, 'utf8'))
if (result.error) {
throw new Error(result.error)
}
return result.id
},
{ timeout: 10_000 }
)
.toBe(commandId)
const result: Snapshot = JSON.parse(readFileSync(path, 'utf8'))
observations.push(result)
return result
}
async function expectOrigin(label: string, kind: string): Promise<Snapshot> {
let snapshot: Snapshot | undefined
await expect
.poll(
async () => {
snapshot = await command('snapshot')
return kind === 'native' ? snapshot.title : snapshot.bar?.label
},
{ timeout: 10_000 }
)
.toBe(label)
if (!snapshot) {
throw new Error('Missing snapshot')
}
expect(snapshot.windowsHidden).toBe(true)
return snapshot
}
async function captureBar(stage: string): Promise<void> {
if (process.env.ORCA_POPUP_IDENTITY_ARTIFACT_DIR) {
await command('capture', { stage })
}
}
beforeAll(async () => {
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
const address = server.address()
if (!address || typeof address === 'string') {
throw new Error('Missing HTTP port')
}
origin = `http://127.0.0.1:${address.port}`
await buildPopupIdentityProbe(
root,
join(process.cwd(), 'src/main/browser/popup-origin-chrome-electron-fixture.ts'),
'main.cjs'
)
probe = await launchPopupIdentityProbe(root, ['--js-flags=--expose-gc'])
await vi.waitFor(
() =>
expect(
probe.browser
.contexts()
.flatMap((context) => context.pages())
.some((candidate) => candidate.url().startsWith('data:text/html,'))
).toBe(true),
{ timeout: 10_000 }
)
const found = probe.browser
.contexts()
.flatMap((context) => context.pages())
.find((candidate) => candidate.url().startsWith('data:text/html,'))
if (!found) {
throw new Error('Missing parent renderer')
}
page = found
stopCapture = await startHiddenRendererFrameCapture(page, (hasFrame) =>
vi.waitFor(() => expect(hasFrame()).toBe(true), { timeout: 10_000 })
)
}, 60_000)
afterAll(async () => {
for (const response of held) {
response.destroy()
}
const artifacts = process.env.ORCA_POPUP_IDENTITY_ARTIFACT_DIR
if (artifacts) {
mkdirSync(artifacts, { recursive: true })
writeFileSync(
join(artifacts, 'origin-observations.json'),
JSON.stringify(observations, null, 2)
)
}
await stopCapture?.()
await probe?.stop()
server.closeAllConnections()
await new Promise<void>((resolve) => server.close(() => resolve()))
})
describe('committed popup origin chrome under Electron', () => {
it.each(['adopted', 'native'])(
'keeps %s chrome neutral for written blank content and retained through pending, canceled and failed redirects',
async (kind) => {
await command('kind', { kind })
await page.evaluate((url) => {
const child = window.open('about:blank#state', '', 'width=600,height=400')
if (!child) {
throw new Error('No child handle')
}
const probeWindow: OriginProbeWindow = window
probeWindow.originProbeChild = child
child.document.write('<title>https://requested.invalid</title><p>Written by opener</p>')
child.location.href = url
}, `${origin}/hold-initial`)
const initial = await expectOrigin('about:blank', kind)
expect(initial.committedUrl).toBe('about:blank#state')
expect(initial.title).toBe('about:blank')
expect(initial.bar?.insecure ?? false).toBe(false)
await page.evaluate(() => {
const probeWindow: OriginProbeWindow = window
const child = probeWindow.originProbeChild
if (!child) {
throw new Error('No child handle')
}
child.document.title = 'https://accounts.trusted.example'
})
await expect
.poll(async () => (await command('snapshot')).pageTitle, { timeout: 10_000 })
.toBe('https://accounts.trusted.example')
expect((await command('snapshot')).title).toBe('about:blank')
if (kind === 'adopted') {
await captureBar('pending')
}
await command('stop')
await expectOrigin('about:blank', kind)
await command('block')
await page.evaluate((url) => {
const probeWindow: OriginProbeWindow = window
const child = probeWindow.originProbeChild
if (!child) {
throw new Error('No child handle')
}
child.location.href = url
}, `${origin}/blocked`)
await expectOrigin('about:blank', kind)
expect(requestedPaths).not.toContain('/blocked')
await command('navigate', { url: `${origin}/committed` })
await expectOrigin(origin, kind)
const otherOrigin = origin.replace('127.0.0.1', 'localhost')
const redirectStart = requestedPaths.length
await command('navigate', { url: `${otherOrigin}/redirect` })
await expect
.poll(() => requestedPaths.slice(redirectStart), { timeout: 10_000 })
.toContain('/hold-redirect')
const redirect = await expectOrigin(origin, kind)
expect(redirect.committedUrl).toBe(`${origin}/committed`)
await command('stop')
const canceled = await expectOrigin(origin, kind)
expect(canceled.committedUrl).toBe(`${origin}/committed`)
await command('navigate', { url: `${otherOrigin}/committed` })
await expectOrigin(otherOrigin, kind)
if (kind === 'adopted') {
await captureBar('committed')
}
await command('navigate', { url: `${otherOrigin}/failure` })
await expect
.poll(async () => (await command('snapshot')).committedUrl, { timeout: 10_000 })
.not.toBe(`${otherOrigin}/committed`)
const failure = await expectOrigin('unknown', kind)
expect(failure.committedOrigin).toBe('null')
const httpsFailure = `${origin.replace('http://127.0.0.1', 'https://popup-error.invalid')}/tls-failure`
await command('navigate', { url: `${origin}/recovered` })
await expectOrigin(origin, kind)
await command('navigate', { url: httpsFailure })
const opaque = await expectOrigin('unknown', kind)
expect(opaque.committedUrl).toBe(httpsFailure)
expect(opaque.committedOrigin).toBe('null')
expect(opaque.bar?.insecure ?? true).toBe(true)
if (kind === 'adopted') {
await captureBar('opaque-error')
}
await command('close')
},
30_000
)
it.each(['adopted', 'native'])(
'retains a %s presenter attached to an already committed frame through garbage collection',
async (kind) => {
const url = `${origin}/already-committed`
await command('attach', { kind, url })
await command('collect-garbage')
const attached = await expectOrigin(origin, kind)
expect(attached.committedUrl).toBe(url)
const nextUrl = `${origin.replace('127.0.0.1', 'localhost')}/after-collection`
await command('navigate', { url: nextUrl })
const navigated = await expectOrigin(new URL(nextUrl).origin, kind)
expect(navigated.committedUrl).toBe(nextUrl)
await command('close')
}
)
})
@@ -0,0 +1,30 @@
// Why: http on loopback is a secure context (local OAuth callback servers are
// common); only flag plain http to a real remote host.
function isLoopbackHost(hostname: string): boolean {
return (
hostname === 'localhost' ||
hostname.endsWith('.localhost') ||
hostname === '127.0.0.1' ||
hostname === '[::1]'
)
}
// Why: the bar must show only the origin — popup URLs routinely carry OAuth
// codes and one-time tokens in path/query that must never reach UI surfaces.
export function describePopupOrigin(rawUrl: string): { label: string; insecure: boolean } {
try {
const parsed = new URL(rawUrl)
if (parsed.origin !== 'null') {
return {
label: parsed.origin,
insecure: parsed.protocol === 'http:' && !isLoopbackHost(parsed.hostname)
}
}
if (parsed.protocol === 'about:') {
return { label: 'about:blank', insecure: false }
}
return { label: parsed.protocol, insecure: false }
} catch {
return { label: 'unknown', insecure: true }
}
}
@@ -0,0 +1,166 @@
import { buildDefaultSettings } from '../../../../shared/default-global-settings'
import { createRoot } from 'react-dom/client'
import { useAppStore } from '@/store'
import { TooltipProvider } from '@/components/ui/tooltip'
import { BrowserPagePane } from '@/components/browser-pane/assemble-chrome/browser-page-pane'
import { registerBrowserOverlaySlotViewport } from '@/components/browser-pane/host-guest/browser-page-viewport'
import { registerBrowserStateIpcBridge } from './browser-state-ipc-bridge'
const worktreeId = 'link-probe-folder'
const profilePartition = 'persist:link-probe-isolated'
const defaultPartition = 'persist:link-probe-default'
let sourceWorkspaceId = ''
let sourcePageId = ''
const mounted = new Map<string, ReturnType<typeof createRoot>>()
function mountTabs(): void {
const state = useAppStore.getState()
for (const tab of state.browserTabsByWorktree[worktreeId] ?? []) {
if (mounted.has(tab.id) || tab.id === sourceWorkspaceId) {
continue
}
const page = state.browserPagesByWorkspace[tab.id]?.[0]
if (!page) {
throw new Error('Created tab has no page')
}
const slot = document.createElement('div')
slot.style.cssText = 'width:800px;height:500px;position:relative'
document.body.appendChild(slot)
registerBrowserOverlaySlotViewport(tab.id, slot)
const root = createRoot(slot)
mounted.set(tab.id, root)
root.render(
<TooltipProvider>
<BrowserPagePane
browserTab={page}
workspaceId={tab.id}
worktreeId={worktreeId}
sessionProfileId={tab.sessionProfileId ?? null}
sessionPartition={tab.sessionPartition ?? null}
isActive={state.activeBrowserTabIdByWorktree[worktreeId] === tab.id}
chromeShortcutScope="inactive"
isAutomationVisible={false}
isMobileDriven={false}
isRemotelyViewed={false}
inputLocked={false}
onUpdatePageState={state.updateBrowserPageState}
onSetUrl={state.setBrowserPageUrl}
/>
</TooltipProvider>
)
}
}
export function installBrowserLinkMaterializationProbe() {
const initial = useAppStore.getState()
const settings =
initial.settings ??
buildDefaultSettings({
workspaceDir: '',
appFontFamily: 'sans-serif',
editorAutoSaveDelayMs: 1000,
primarySelectionMiddleClickPaste: false,
primarySelectionDefaultedForLinux: false,
terminalFontFamily: 'monospace',
terminalInactivePaneOpacity: 1,
terminalRightClickToPaste: false,
notifications: {
enabled: false,
agentTaskComplete: false,
terminalBell: false,
suppressWhenFocused: true,
customSoundId: 'system',
customSoundPath: null,
customSoundVolume: 0,
mutedNotificationSourceIds: [],
notifyHiddenWorkspaces: false
},
voice: {
enabled: false,
sttModel: '',
modelsDir: '',
language: 'en',
dictationMode: 'hold',
terminalConfirmBeforeInsert: true,
userModels: [],
openAiApiKeyConfigured: false,
microphoneDeviceId: null,
microphoneDeviceLabel: null
}
})
useAppStore.setState({
activeWorktreeId: worktreeId,
settings: { ...settings, activeRuntimeEnvironmentId: null },
defaultBrowserSessionProfileId: 'other-profile',
browserSessionProfiles: [
{
id: 'isolated-profile',
label: 'Isolated synthetic profile',
partition: profilePartition,
scope: 'isolated',
source: null
},
{
id: 'other-profile',
label: 'Other synthetic profile',
partition: defaultPartition,
scope: 'isolated',
source: null
}
]
})
const source = useAppStore.getState().createBrowserTab(worktreeId, 'about:blank', {
sessionProfileId: 'isolated-profile',
sessionPartition: profilePartition,
activate: true
})
sourceWorkspaceId = source.id
if (!source.activePageId) {
throw new Error('Source has no active page')
}
sourcePageId = source.activePageId
registerBrowserStateIpcBridge([], () => false)
return {
sourcePageId,
snapshot: () => {
const state = useAppStore.getState()
return {
tabs: state.browserTabsByWorktree[worktreeId],
pages: state.browserPagesByWorkspace,
selected: state.activeBrowserTabIdByWorktree[worktreeId],
selectedUnified: state.groupsByWorktree[worktreeId]?.map((group) => group.activeTabId),
unified: state.unifiedTabsByWorktree[worktreeId],
sourceWorkspaceId
}
},
mountTabs,
mutate: (transition: string) => {
const state = useAppStore.getState()
const tabs = state.browserTabsByWorktree[worktreeId] ?? []
if (transition === 'remove-tab') {
useAppStore.setState({
browserTabsByWorktree: {
[worktreeId]: tabs.filter((tab) => tab.id !== sourceWorkspaceId)
}
})
}
if (transition === 'remove-page') {
useAppStore.setState({ browserPagesByWorkspace: {} })
}
if (transition === 'profile-switch') {
useAppStore.setState({
browserTabsByWorktree: {
[worktreeId]: tabs.map((tab) =>
tab.id === sourceWorkspaceId
? { ...tab, sessionProfileId: 'other-profile', sessionPartition: defaultPartition }
: tab
)
}
})
}
if (transition === 'delete-profile') {
useAppStore.setState({ browserSessionProfiles: [] })
}
}
}
}
@@ -102,7 +102,8 @@ describe('link-opened Orca tabs', () => {
)
})
it('leaves the profile unset when the opener tab is gone, so the user default still applies', () => {
// Baseline defect: remove .fails when the captured-claim handoff rejects this event.
it.fails('rejects a missing source tab without default-profile fallback', () => {
storeState.value = {
browserPagesByWorkspace: {
'workspace-1': [{ id: 'page-1', workspaceId: 'missing', worktreeId: 'worktree-1' }]
@@ -113,8 +114,7 @@ describe('link-opened Orca tabs', () => {
captureOpenLinkHandler()({ browserPageId: 'page-1', url: 'https://docs.example.com/guide' })
const options = createBrowserTabMock.mock.calls[0][2] as Record<string, unknown>
expect('sessionProfileId' in options).toBe(false)
expect(createBrowserTabMock).not.toHaveBeenCalled()
})
it('creates modifier-click links in the background', () => {