feat: require explicit browser provider consent

This commit is contained in:
Sinabina
2026-07-21 12:07:01 -07:00
parent 75b3576670
commit e3effb3fc4
48 changed files with 2798 additions and 220 deletions
+32 -2
View File
@@ -44,6 +44,30 @@ export function isCustomChromium(): boolean {
return p.includes('GBrowser') || p.includes('gbrowser');
}
/**
* Return the explicitly selected Chromium executable for both headless and
* headed launches. Keeping this opt-in preserves the managed browser fallback
* while allowing the lightweight playwright-core adapter to reuse a system or
* host-managed Chrome without downloading Playwright's browser package.
*/
export function configuredChromiumExecutable(
env: NodeJS.ProcessEnv = process.env,
): string | undefined {
const value = env.GSTACK_CHROMIUM_PATH?.trim();
return value || undefined;
}
/** Installed-system Chromium is supported only for headless automation. */
export function assertHeadedBrowserProvider(
env: NodeJS.ProcessEnv = process.env,
): void {
if (env.GSTACK_BROWSER_PROVIDER === 'installed') {
throw new Error(
'Visible GStack Browser requires managed Chromium; installed Chrome-family browsers are headless-only',
);
}
}
/**
* Decide whether Playwright should request Chromium's sandbox.
*
@@ -361,6 +385,7 @@ export class BrowserManager {
const { STEALTH_LAUNCH_ARGS, buildGStackLaunchArgs } = await import('./stealth');
const launchArgs: string[] = [...STEALTH_LAUNCH_ARGS, ...buildGStackLaunchArgs()];
let useHeadless = true;
const executablePath = configuredChromiumExecutable();
// Docker/CI/root: Chromium sandbox requires unprivileged user namespaces which
// are typically disabled in containers and are never available for the root
@@ -387,7 +412,11 @@ export class BrowserManager {
this.browser = await chromium.launch({
headless: useHeadless,
...(useHeadless && managedHeadlessChannel() ? { channel: 'chromium' as const } : {}),
...(executablePath
? { executablePath }
: useHeadless && managedHeadlessChannel()
? { channel: 'chromium' as const }
: {}),
// On Windows, Chromium's sandbox fails when the server is spawned through
// the Bun→Node process chain (GitHub #276). Disable it — local daemon
// browsing user-specified URLs has marginal sandbox benefit. Also disabled
@@ -447,6 +476,7 @@ export class BrowserManager {
* every action Claude takes in real time.
*/
async launchHeaded(authToken?: string): Promise<void> {
assertHeadedBrowserProvider();
// Clear old state before repopulating
this.pages.clear();
this.tabSessions.clear();
@@ -515,7 +545,7 @@ export class BrowserManager {
// Support custom Chromium binary via GSTACK_CHROMIUM_PATH env var.
// Used by GStack Browser.app to point at the bundled Chromium.
const executablePath = process.env.GSTACK_CHROMIUM_PATH || undefined;
const executablePath = configuredChromiumExecutable();
// Rebrand Chromium → GStack Browser in macOS menu bar / Dock / Cmd+Tab.
// Patch the Chromium .app's Info.plist so macOS shows our name.
+20 -1
View File
@@ -7,7 +7,7 @@
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { startTestServer } from './test-server';
import { BrowserManager } from '../src/browser-manager';
import { BrowserManager, assertHeadedBrowserProvider, configuredChromiumExecutable } from '../src/browser-manager';
import { resolveServerScript } from '../src/cli';
import { handleReadCommand as _handleReadCommand, parseOutArgs, hasOutArg, resultToString } from '../src/read-commands';
import { handleWriteCommand as _handleWriteCommand } from '../src/write-commands';
@@ -23,6 +23,25 @@ const handleReadCommand = (cmd: string, args: string[], b: BrowserManager) =>
const handleWriteCommand = (cmd: string, args: string[], b: BrowserManager) =>
_handleWriteCommand(cmd, args, b.getActiveSession(), b);
describe('configuredChromiumExecutable', () => {
test('returns and trims an explicitly selected system browser', () => {
expect(configuredChromiumExecutable({
GSTACK_CHROMIUM_PATH: ' /Applications/Google Chrome.app/Contents/MacOS/Google Chrome ',
})).toBe('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome');
});
test('keeps the managed-browser path when no override is selected', () => {
expect(configuredChromiumExecutable({})).toBeUndefined();
expect(configuredChromiumExecutable({ GSTACK_CHROMIUM_PATH: ' ' })).toBeUndefined();
});
test('rejects headed launch when setup selected an installed system browser', () => {
expect(() => assertHeadedBrowserProvider({ GSTACK_BROWSER_PROVIDER: 'installed' }))
.toThrow('Visible GStack Browser requires managed Chromium');
expect(() => assertHeadedBrowserProvider({ GSTACK_BROWSER_PROVIDER: 'managed' })).not.toThrow();
});
});
// ─── Pure arg-parser + result-conversion unit tests (no browser) ───
describe('parseOutArgs / hasOutArg', () => {
test('--out <path> splits the flag from the positional', () => {