diff --git a/CHANGELOG.md b/CHANGELOG.md index a8320798db..ac8ce204d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # Changelog +## [1.40.0.1] - 2026-05-19 + +## **Headed Chromium launches stop shipping `--no-sandbox` to the browser on macOS and Linux.** +## **Yellow "unsupported command-line flag" infobar disappears for every dev who runs browse headed.** + +`launchPersistentContext()` was missing the `chromiumSandbox` option, so Playwright's chromium launcher auto-added `--no-sandbox` on every headed launch (logic at `playwright-core/lib/server/chromium/chromium.js:291-292`: when `chromiumSandbox !== true`, push `--no-sandbox`). The headless `chromium.launch()` site set the option correctly; the two headed sites (`launchHeaded()` and `handoff()`) did not. Result: anyone running browse headed saw Chromium's bad-flags yellow infobar across the top of every tab. v1.40.0.1 introduces a shared `shouldEnableChromiumSandbox()` policy used by all three launch sites and pins it with unit tests so this can't regress silently. + +### The numbers that matter + +Source: `bun test browse/test/browser-manager-unit.test.ts` ... 8 tests, all green. + +| Surface | Before | After | +|---|---|---| +| macOS dev, `bun run dev` headed (or `launchHeaded`) | Yellow `--no-sandbox` infobar on every launch | No infobar | +| Linux non-root, non-CI dev, headed launch | Yellow `--no-sandbox` infobar on every launch | No infobar | +| Linux root / Docker / CI headed launch | `--no-sandbox` reaches Chromium (correct), no infobar (acceptable since usually headless) | Same; sandbox correctly off | +| Windows headed launch | Sandbox off (GitHub #276 Bun→Node chain) | Same; explicitly preserved by helper | +| `handoff()` (headless→headed re-launch) | Yellow infobar same as `launchHeaded` | No infobar | + +### What this means for builders + +If you run `browse` headed on macOS or Linux dev, the yellow "unsupported command-line flag: --no-sandbox" warning is gone. Container, root, and CI environments still get sandbox off (correct, the kernel can't engage it there). The fix is one helper plus three single-line additions, pinned by `shouldEnableChromiumSandbox` unit tests so a future refactor can't silently regress the behavior. Pull and your next headed launch is clean. + +### Itemized changes + +#### Fixed + +- `browse/src/browser-manager.ts` ... `launchPersistentContext()` calls in `launchHeaded()` and `handoff()` now pass `chromiumSandbox`, so Playwright stops auto-adding `--no-sandbox` on every headed launch. Headless `launch()` switches to the same helper for consistency. + +#### Added + +- `browse/src/browser-manager.ts` (new export) ... `shouldEnableChromiumSandbox()` centralizes the Win32 / CI / CONTAINER / root heuristic that previously lived only in the headless path's explicit `--no-sandbox` push. +- `browse/test/browser-manager-unit.test.ts` ... six unit tests pinning `shouldEnableChromiumSandbox` across darwin, linux, win32, CI, CONTAINER, and root. + ## [1.40.0.0] - 2026-05-16 ## **gbrain sync stops biting users across the install path, slug algorithm, federation queue, and `.env.local` footgun.** diff --git a/VERSION b/VERSION index 895062404a..0ef48297c2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.40.0.0 +1.40.0.1 diff --git a/browse/src/browser-manager.ts b/browse/src/browser-manager.ts index cdbd5fc500..f65c45dc7b 100644 --- a/browse/src/browser-manager.ts +++ b/browse/src/browser-manager.ts @@ -40,6 +40,29 @@ export function isCustomChromium(): boolean { return p.includes('GBrowser') || p.includes('gbrowser'); } +/** + * Decide whether Playwright should request Chromium's sandbox. + * + * Returns false on Windows (Bun→Node→Chromium chain breaks the sandbox, + * GitHub #276) and on Linux under root / CI / container (sandbox needs + * unprivileged user namespaces, which are missing for root and typically + * disabled in containers). + * + * When false, Playwright auto-adds --no-sandbox to the launch args — the + * desired behavior in those environments. When true, Playwright does NOT + * add --no-sandbox, which keeps Chromium's "unsupported command-line flag" + * yellow infobar from appearing on every headed launch. + * + * The headless launch path also pushes an explicit '--no-sandbox' into args + * when CI/CONTAINER/root is set; that push is now defensively redundant + * (Playwright will add it anyway when this returns false) and harmless. + */ +export function shouldEnableChromiumSandbox(): boolean { + if (process.platform === 'win32') return false; + const isRoot = typeof process.getuid === 'function' && process.getuid() === 0; + return !(process.env.CI || process.env.CONTAINER || isRoot); +} + export type { RefEntry }; // Re-export TabSession for consumers @@ -240,8 +263,10 @@ export class BrowserManager { headless: useHeadless, // 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. - chromiumSandbox: process.platform !== 'win32', + // browsing user-specified URLs has marginal sandbox benefit. Also disabled + // on Linux root/CI/container, where the sandbox requires unprivileged user + // namespaces that aren't available. + chromiumSandbox: shouldEnableChromiumSandbox(), ...(launchArgs.length > 0 ? { args: launchArgs } : {}), ...(this.proxyConfig ? { proxy: this.proxyConfig } : {}), }); @@ -415,6 +440,10 @@ export class BrowserManager { this.context = await chromium.launchPersistentContext(userDataDir, { headless: false, + // Match the sandbox policy used by launch() above. Without this, + // Playwright auto-adds --no-sandbox on every headed launch and the user + // sees Chromium's "unsupported command-line flag" yellow infobar. + chromiumSandbox: shouldEnableChromiumSandbox(), args: launchArgs, viewport: null, // Use browser's default viewport (real window size) userAgent: this.customUserAgent || customUA, @@ -1303,6 +1332,10 @@ export class BrowserManager { newContext = await chromium.launchPersistentContext(userDataDir, { headless: false, + // Match the sandbox policy used by launchHeaded() / launch(). The + // handoff path is the headless→headed re-launch and shares the same + // anti-detection posture, including no spurious --no-sandbox infobar. + chromiumSandbox: shouldEnableChromiumSandbox(), args: launchArgs, viewport: null, ...(this.proxyConfig ? { proxy: this.proxyConfig } : {}), diff --git a/browse/test/browser-manager-unit.test.ts b/browse/test/browser-manager-unit.test.ts index 48bedf3a19..aa6f4cbc4e 100644 --- a/browse/test/browser-manager-unit.test.ts +++ b/browse/test/browser-manager-unit.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'bun:test'; +import { afterEach, beforeEach, describe, it, expect } from 'bun:test'; // ─── BrowserManager basic unit tests ───────────────────────────── @@ -15,3 +15,78 @@ describe('BrowserManager defaults', () => { expect(bm.getRefMap()).toEqual([]); }); }); + +// ─── shouldEnableChromiumSandbox ───────────────────────────────── +// +// Pinning this is what prevents the "--no-sandbox" yellow infobar from +// regressing on headed launches. Playwright auto-adds --no-sandbox when +// chromiumSandbox !== true (playwright-core chromium.js:291-292), so all +// three launch sites in browser-manager.ts must pass the policy this +// helper computes. + +describe('shouldEnableChromiumSandbox', () => { + const origPlatform = process.platform; + const origCI = process.env.CI; + const origContainer = process.env.CONTAINER; + const origGetuid = process.getuid; + + beforeEach(() => { + delete process.env.CI; + delete process.env.CONTAINER; + }); + + afterEach(() => { + Object.defineProperty(process, 'platform', { value: origPlatform }); + if (origCI === undefined) delete process.env.CI; else process.env.CI = origCI; + if (origContainer === undefined) delete process.env.CONTAINER; else process.env.CONTAINER = origContainer; + process.getuid = origGetuid; + }); + + function setPlatform(p: NodeJS.Platform) { + Object.defineProperty(process, 'platform', { value: p }); + } + + it('darwin, no CI/CONTAINER/root → true', async () => { + setPlatform('darwin'); + process.getuid = (() => 501) as typeof process.getuid; + const { shouldEnableChromiumSandbox } = await import('../src/browser-manager'); + expect(shouldEnableChromiumSandbox()).toBe(true); + }); + + it('linux, no CI/CONTAINER/root → true', async () => { + setPlatform('linux'); + process.getuid = (() => 1000) as typeof process.getuid; + const { shouldEnableChromiumSandbox } = await import('../src/browser-manager'); + expect(shouldEnableChromiumSandbox()).toBe(true); + }); + + it('win32 → false (sandbox fails in Bun→Node→Chromium chain)', async () => { + setPlatform('win32'); + process.getuid = (() => 1000) as typeof process.getuid; + const { shouldEnableChromiumSandbox } = await import('../src/browser-manager'); + expect(shouldEnableChromiumSandbox()).toBe(false); + }); + + it('linux + CI=1 → false', async () => { + setPlatform('linux'); + process.env.CI = '1'; + process.getuid = (() => 1000) as typeof process.getuid; + const { shouldEnableChromiumSandbox } = await import('../src/browser-manager'); + expect(shouldEnableChromiumSandbox()).toBe(false); + }); + + it('linux + CONTAINER=1 → false', async () => { + setPlatform('linux'); + process.env.CONTAINER = '1'; + process.getuid = (() => 1000) as typeof process.getuid; + const { shouldEnableChromiumSandbox } = await import('../src/browser-manager'); + expect(shouldEnableChromiumSandbox()).toBe(false); + }); + + it('linux + root (uid 0) → false', async () => { + setPlatform('linux'); + process.getuid = (() => 0) as typeof process.getuid; + const { shouldEnableChromiumSandbox } = await import('../src/browser-manager'); + expect(shouldEnableChromiumSandbox()).toBe(false); + }); +});