Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.**
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.40.0.0
1.40.0.1
37 changes: 35 additions & 2 deletions browse/src/browser-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 } : {}),
});
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 } : {}),
Expand Down
77 changes: 76 additions & 1 deletion browse/test/browser-manager-unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'bun:test';
import { afterEach, beforeEach, describe, it, expect } from 'bun:test';

// ─── BrowserManager basic unit tests ─────────────────────────────

Expand All @@ -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);
});
});
Loading