feat(desktop): import Chrome browser data · simstudioai/sim@8f45b0c · GitHub
Skip to content

Commit 8f45b0c

Browse files
committed
feat(desktop): import Chrome browser data
1 parent 6839966 commit 8f45b0c

50 files changed

Lines changed: 7631 additions & 61 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/desktop/scripts/build.ts‎

Lines changed: 13 additions & 1 deletion

‎apps/desktop/src/main/browser-agent/driver.test.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ describe('executeTool', () => {
5757
onPageState: vi.fn(),
5858
onTabsState: vi.fn(),
5959
onSessionStatus: vi.fn(),
60+
onFillAvailability: vi.fn(),
6061
},
6162
() => win
6263
)
@@ -108,6 +109,7 @@ describe('executeTool', () => {
108109
onPageState: vi.fn(),
109110
onTabsState: vi.fn(),
110111
onSessionStatus: vi.fn(),
112+
onFillAvailability: vi.fn(),
111113
},
112114
() => win
113115
)
@@ -151,7 +153,12 @@ describe('credential protection', () => {
151153
async function openPage() {
152154
const win = new BrowserWindow()
153155
driver.initDriver(
154-
{ onPageState: vi.fn(), onTabsState: vi.fn(), onSessionStatus: vi.fn() },
156+
{
157+
onPageState: vi.fn(),
158+
onTabsState: vi.fn(),
159+
onSessionStatus: vi.fn(),
160+
onFillAvailability: vi.fn(),
161+
},
155162
() => win
156163
)
157164
await driver.executeTool('browser_open_tab', {})

‎apps/desktop/src/main/browser-agent/driver.ts‎

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
* is bounded by a watchdog so the Sim side always gets a response instead of
1616
* waiting out its own timeout against silence.
1717
*/
18-
import type {
19-
BrowserKnownSessionsState,
20-
BrowserPageState,
21-
BrowserPanelAction,
22-
BrowserTabsState,
23-
BrowserToolName,
18+
import {
19+
BROWSER_DATA_KINDS,
20+
type BrowserDataKind,
21+
type BrowserKnownSessionsState,
22+
type BrowserPageState,
23+
type BrowserPanelAction,
24+
type BrowserTabsState,
25+
type BrowserToolName,
2426
} from '@sim/browser-protocol'
2527
import { createLogger } from '@sim/logger'
2628
import { getErrorMessage } from '@sim/utils/errors'
@@ -53,6 +55,7 @@ import {
5355
} from '@/main/browser-agent/page-functions'
5456
import * as session from '@/main/browser-agent/session'
5557
import { checkAgentUrl } from '@/main/browser-agent/url-guard'
58+
import { clearCredentials, fillCoordinator, initFillCoordinator } from '@/main/browser-credentials'
5659
import type { ConfigStore } from '@/main/config'
5760

5861
const logger = createLogger('BrowserAgentDriver')
@@ -76,6 +79,8 @@ export interface DriverCallbacks {
7679
onPageState: (state: BrowserPageState) => void
7780
onTabsState: (state: BrowserTabsState) => void
7881
onSessionStatus: (alive: boolean) => void
82+
/** Whether the active tab shows a login form Sim holds a credential for. */
83+
onFillAvailability: (available: boolean) => void
7984
}
8085

8186
let driverCallbacks: DriverCallbacks | null = null
@@ -185,13 +190,23 @@ export function initDriver(
185190
): void {
186191
driverCallbacks = callbacks
187192
knownSessions = config ? new BrowserKnownSessionRegistry(config) : null
193+
initFillCoordinator({
194+
getActiveContents: () => session.activeTab()?.view.webContents ?? null,
195+
onAvailabilityChanged: (available) => callbacks.onFillAvailability(available),
196+
})
188197
session.initSession(
189198
{
190199
onSessionClosed: () => {
191200
driverCallbacks?.onSessionStatus(false)
192201
},
193202
onTabCreated: instrumentTab,
194-
onActiveTabChanged: pushPageState,
203+
onTabNavigated: (contents) => fillCoordinator()?.noteNavigation(contents),
204+
onTabClosed: (contents) => fillCoordinator()?.forget(contents),
205+
onActiveTabChanged: (contents) => {
206+
pushPageState(contents)
207+
// The fill affordance belongs to whichever page is in front.
208+
void fillCoordinator()?.refreshAvailability()
209+
},
195210
onTabsChanged: pushTabsState,
196211
onTabThemeChanged: (contents, theme) => {
197212
void cdp.setColorScheme(contents, theme).catch((error) => {
@@ -223,13 +238,32 @@ export async function getKnownSessions(): Promise<BrowserKnownSessionsState> {
223238
}
224239

225240
/**
226-
* Everything the embedded browser remembers about the signed-in user, cleared
227-
* as one unit on sign-out. Lives here because the browsing-trail registry and
228-
* the session profile are owned by different modules.
241+
* Cookies, site storage, cache, and the remembered browsing trail.
242+
*
243+
* Saved passwords are deliberately NOT touched. "Clear browsing data" is about
244+
* signing out of websites, and a user who wanted to erase their password vault
245+
* would have to say so separately — silently taking their credentials with it
246+
* would be a destructive surprise.
247+
*/
248+
export async function clearBrowsingData(
249+
kinds: readonly BrowserDataKind[] = BROWSER_DATA_KINDS
250+
): Promise<void> {
251+
// The remembered browsing trail is the local mirror of the cookie jar, so it
252+
// goes when cookies do and stays when they do not.
253+
if (kinds.includes('cookies')) knownSessions?.clear()
254+
await session.clearAgentData(kinds)
255+
}
256+
257+
/**
258+
* Everything the embedded browser holds for the signed-in user, including the
259+
* credential vault. This is the Sim sign-out path: a different account signing
260+
* in on the same machine must not inherit the previous user's sessions or
261+
* passwords.
229262
*/
230263
export async function clearBrowserProfile(): Promise<void> {
231264
knownSessions?.clear()
232265
await session.clearProfileStorage()
266+
await clearCredentials()
233267
}
234268

235269
function str(params: Record<string, unknown>, key: string): string | undefined {

‎apps/desktop/src/main/browser-agent/session.test.ts‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ async function freshSession(
6161
onTabsChanged: vi.fn(),
6262
onTabThemeChanged: vi.fn(),
6363
onDownloadBlocked: vi.fn(),
64+
onTabNavigated: vi.fn(),
65+
onTabClosed: vi.fn(),
6466
...eventOverrides,
6567
},
6668
() => win,
@@ -671,6 +673,8 @@ describe('browser-agent session', () => {
671673
onTabsChanged: vi.fn(),
672674
onTabThemeChanged: vi.fn(),
673675
onDownloadBlocked: vi.fn(),
676+
onTabNavigated: vi.fn(),
677+
onTabClosed: vi.fn(),
674678
},
675679
() => replacement
676680
)
@@ -1160,3 +1164,60 @@ describe('reopening a closed tab', () => {
11601164
expect((reopened?.view as unknown as MockView).webContents.loadURL).not.toHaveBeenCalled()
11611165
})
11621166
})
1167+
1168+
describe('importAgentCookies', () => {
1169+
/** Points the mocked partition at a cookie jar and returns its `set` spy. */
1170+
async function withCookieJar(
1171+
set: ReturnType<typeof vi.fn>
1172+
): Promise<typeof import('@/main/browser-agent/session')> {
1173+
vi.mocked(electronSession.fromPartition).mockReturnValue({
1174+
cookies: { set },
1175+
} as unknown as ReturnType<typeof electronSession.fromPartition>)
1176+
vi.resetModules()
1177+
return import('@/main/browser-agent/session')
1178+
}
1179+
1180+
const cookie = (name: string) => ({
1181+
url: 'https://example.com/',
1182+
name,
1183+
value: 'v',
1184+
path: '/',
1185+
secure: true,
1186+
httpOnly: true,
1187+
sameSite: 'lax' as const,
1188+
})
1189+
1190+
it('writes every cookie into the dedicated browser profile', async () => {
1191+
const set = vi.fn(async () => {})
1192+
const session = await withCookieJar(set)
1193+
1194+
const result = await session.importAgentCookies([cookie('a'), cookie('b')])
1195+
1196+
expect(result).toEqual({ imported: 2, failed: 0 })
1197+
expect(electronSession.fromPartition).toHaveBeenCalledWith('persist:sim-browser-agent')
1198+
expect(set).toHaveBeenCalledTimes(2)
1199+
expect(set).toHaveBeenNthCalledWith(1, cookie('a'))
1200+
})
1201+
1202+
it('counts a rejected cookie without losing the rest', async () => {
1203+
// Chromium refuses cookies whose attributes are inconsistent. That
1204+
// rejection must cost one cookie, not the whole import.
1205+
const set = vi.fn(async (details: { name: string }) => {
1206+
if (details.name === 'bad') throw new Error('Failed to set cookie')
1207+
})
1208+
const session = await withCookieJar(set)
1209+
1210+
const result = await session.importAgentCookies([cookie('a'), cookie('bad'), cookie('c')])
1211+
1212+
expect(result).toEqual({ imported: 2, failed: 1 })
1213+
expect(set).toHaveBeenCalledTimes(3)
1214+
})
1215+
1216+
it('does nothing when there is nothing to import', async () => {
1217+
const set = vi.fn(async () => {})
1218+
const session = await withCookieJar(set)
1219+
1220+
await expect(session.importAgentCookies([])).resolves.toEqual({ imported: 0, failed: 0 })
1221+
expect(set).not.toHaveBeenCalled()
1222+
})
1223+
})

‎apps/desktop/src/main/browser-agent/session.ts‎

Lines changed: 86 additions & 1 deletion

0 commit comments

Comments
 (0)