Merge tag 'v0.15.0' into codex/sync-v0.15.0
v0.15.0 # Conflicts: # CHANGELOG.md # README.md # cli/Cargo.lock # cli/Cargo.toml # cli/src/commands.rs # cli/src/connection.rs # cli/src/flags.rs # cli/src/main.rs # docs/src/app/commands/page.mdx # docs/src/app/configuration/page.mdx # package.json # src/actions.ts
This commit is contained in:
+81
-2
@@ -21,6 +21,7 @@ import { writeFile, mkdir } from 'node:fs/promises';
|
||||
import type { LaunchCommand, TraceEvent } from './types.js';
|
||||
import { type RefMap, type EnhancedSnapshot, getEnhancedSnapshot, parseRef } from './snapshot.js';
|
||||
import { safeHeaderMerge } from './state-utils.js';
|
||||
import { isDomainAllowed, installDomainFilter, parseDomainList } from './domain-filter.js';
|
||||
import {
|
||||
getEncryptionKey,
|
||||
isEncryptedPayload,
|
||||
@@ -161,6 +162,7 @@ export class BrowserManager {
|
||||
private contextHeaders: Record<string, string> | undefined = undefined;
|
||||
private contextUserAgent: string | undefined = undefined;
|
||||
private downloadPath: string | null = null;
|
||||
private allowedDomains: string[] = [];
|
||||
|
||||
/**
|
||||
* Set the persistent color scheme preference.
|
||||
@@ -573,6 +575,61 @@ export class BrowserManager {
|
||||
return parseRef(selector) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the domain filter on a context if an allowlist is configured.
|
||||
* Should be called before any pages navigate on the context.
|
||||
*/
|
||||
private async ensureDomainFilter(context: BrowserContext): Promise<void> {
|
||||
if (this.allowedDomains.length > 0) {
|
||||
await installDomainFilter(context, this.allowedDomains);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* After installing the domain filter, verify existing pages are on allowed
|
||||
* domains. Pages that pre-date the filter (e.g. CDP/cloud connect) may have
|
||||
* already navigated to disallowed domains. Navigate them to about:blank.
|
||||
*/
|
||||
private async sanitizeExistingPages(pages: Page[]): Promise<void> {
|
||||
if (this.allowedDomains.length === 0) return;
|
||||
for (const page of pages) {
|
||||
const url = page.url();
|
||||
if (!url || url === 'about:blank') continue;
|
||||
try {
|
||||
const hostname = new URL(url).hostname.toLowerCase();
|
||||
if (!isDomainAllowed(hostname, this.allowedDomains)) {
|
||||
await page.goto('about:blank');
|
||||
}
|
||||
} catch {
|
||||
await page.goto('about:blank').catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a URL is allowed by the domain allowlist.
|
||||
* Throws if the URL's domain is blocked. No-op if no allowlist is set.
|
||||
* Blocks non-http(s) schemes and unparseable URLs by default.
|
||||
*/
|
||||
checkDomainAllowed(url: string): void {
|
||||
if (this.allowedDomains.length === 0) return;
|
||||
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
throw new Error(`Navigation blocked: non-http(s) scheme in URL "${url}"`);
|
||||
}
|
||||
|
||||
let hostname: string;
|
||||
try {
|
||||
hostname = new URL(url).hostname.toLowerCase();
|
||||
} catch {
|
||||
throw new Error(`Navigation blocked: unable to parse URL "${url}"`);
|
||||
}
|
||||
|
||||
if (!isDomainAllowed(hostname, this.allowedDomains)) {
|
||||
throw new Error(`Navigation blocked: ${hostname} is not in the allowed domains list`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get locator - supports both refs and regular selectors
|
||||
*/
|
||||
@@ -645,6 +702,7 @@ export class BrowserManager {
|
||||
context.setDefaultTimeout(getDefaultTimeout());
|
||||
this.contexts.push(context);
|
||||
this.setupContextTracking(context);
|
||||
await this.ensureDomainFilter(context);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -1278,6 +1336,8 @@ export class BrowserManager {
|
||||
context.setDefaultTimeout(10000);
|
||||
this.contexts.push(context);
|
||||
this.setupContextTracking(context);
|
||||
await this.ensureDomainFilter(context);
|
||||
await this.sanitizeExistingPages([page]);
|
||||
this.pages.push(page);
|
||||
this.activePageIndex = 0;
|
||||
this.setupPageTracking(page);
|
||||
@@ -1421,10 +1481,12 @@ export class BrowserManager {
|
||||
this.browser = browser;
|
||||
context.setDefaultTimeout(getDefaultTimeout());
|
||||
this.contexts.push(context);
|
||||
this.setupContextTracking(context);
|
||||
await this.ensureDomainFilter(context);
|
||||
await this.sanitizeExistingPages([page]);
|
||||
this.pages.push(page);
|
||||
this.activePageIndex = 0;
|
||||
this.setupPageTracking(page);
|
||||
this.setupContextTracking(context);
|
||||
} catch (error) {
|
||||
await this.closeKernelSession(session.session_id, kernelApiKey).catch((sessionError) => {
|
||||
console.error('Failed to close Kernel session during cleanup:', sessionError);
|
||||
@@ -1497,10 +1559,12 @@ export class BrowserManager {
|
||||
this.browser = browser;
|
||||
context.setDefaultTimeout(getDefaultTimeout());
|
||||
this.contexts.push(context);
|
||||
this.setupContextTracking(context);
|
||||
await this.ensureDomainFilter(context);
|
||||
await this.sanitizeExistingPages([page]);
|
||||
this.pages.push(page);
|
||||
this.activePageIndex = 0;
|
||||
this.setupPageTracking(page);
|
||||
this.setupContextTracking(context);
|
||||
} catch (error) {
|
||||
await this.closeBrowserUseSession(session.id, browserUseApiKey).catch((sessionError) => {
|
||||
console.error('Failed to close Browser Use session during cleanup:', sessionError);
|
||||
@@ -1572,6 +1636,15 @@ export class BrowserManager {
|
||||
this.downloadPath = options.downloadPath;
|
||||
}
|
||||
|
||||
if (options.allowedDomains && options.allowedDomains.length > 0) {
|
||||
this.allowedDomains = options.allowedDomains.map((d: string) => d.toLowerCase());
|
||||
} else {
|
||||
const envDomains = process.env.AGENT_BROWSER_ALLOWED_DOMAINS;
|
||||
if (envDomains) {
|
||||
this.allowedDomains = parseDomainList(envDomains);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.downloadPath && (cdpEndpoint || options.autoConnect)) {
|
||||
const warning =
|
||||
"--download-path is ignored when connecting via CDP or auto-connect (downloads use the remote browser's configuration)";
|
||||
@@ -1832,8 +1905,10 @@ export class BrowserManager {
|
||||
context.setDefaultTimeout(getDefaultTimeout());
|
||||
this.contexts.push(context);
|
||||
this.setupContextTracking(context);
|
||||
await this.ensureDomainFilter(context);
|
||||
|
||||
const page = context.pages()[0] ?? (await context.newPage());
|
||||
await this.sanitizeExistingPages([page]);
|
||||
// Only add if not already tracked (setupContextTracking may have already added it via 'page' event)
|
||||
if (!this.pages.includes(page)) {
|
||||
this.pages.push(page);
|
||||
@@ -1930,8 +2005,11 @@ export class BrowserManager {
|
||||
context.setDefaultTimeout(10000);
|
||||
this.contexts.push(context);
|
||||
this.setupContextTracking(context);
|
||||
await this.ensureDomainFilter(context);
|
||||
}
|
||||
|
||||
await this.sanitizeExistingPages(allPages);
|
||||
|
||||
for (const page of allPages) {
|
||||
this.pages.push(page);
|
||||
this.setupPageTracking(page);
|
||||
@@ -2200,6 +2278,7 @@ export class BrowserManager {
|
||||
context.setDefaultTimeout(getDefaultTimeout());
|
||||
this.contexts.push(context);
|
||||
this.setupContextTracking(context);
|
||||
await this.ensureDomainFilter(context);
|
||||
|
||||
const page = await context.newPage();
|
||||
// Only add if not already tracked (setupContextTracking may have already added it via 'page' event)
|
||||
|
||||
Reference in New Issue
Block a user