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
5 changes: 5 additions & 0 deletions .changeset/local-browser-launch-gaps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@browserbasehq/stagehand": patch
---

Wire `chromiumSandbox: false` into local Chromium launches and document `ignoreDefaultArgs` plus site-isolation/renderer-limit guidance.
5 changes: 5 additions & 0 deletions .changeset/remove-site-per-process-default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@browserbasehq/stagehand": patch
---

Stop injecting `--site-per-process` into local Chromium launches so user-supplied flags like `--disable-features=site-per-process` and `--renderer-process-limit` take effect.
1 change: 1 addition & 0 deletions packages/cli/src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default class Doctor extends BrowseCommand {
"browse doctor --auto-connect",
"browse doctor --cdp 9222",
"browse doctor --session research --json",
"browse doctor --local --chrome-path /opt/chrome/chrome",
];

static override flags = {
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/commands/open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export default class Open extends BrowseCommand {
"browse open https://example.com --cdp 9222",
"browse open https://example.com --cdp ws://127.0.0.1:9222/devtools/browser/<id> --target-id <target-id>",
"browse open https://example.com --session research",
"browse open https://example.com --local --chrome-path /opt/chrome/chrome",
"browse open https://example.com --local --connect-timeout 30000 --chrome-arg --renderer-process-limit=6",
"browse open https://example.com --wait networkidle --timeout 45000",
];

Expand Down
30 changes: 30 additions & 0 deletions packages/cli/src/lib/driver/chrome-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import fs from "node:fs";

const DEFAULT_CHROME_CANDIDATES = [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/usr/bin/google-chrome-stable",
"/usr/bin/google-chrome",
"/usr/bin/chromium-browser",
"/usr/bin/chromium",
];

export function resolveChromeExecutablePath(options?: {
explicit?: string;
env?: string;
}): string | undefined {
if (options?.explicit) {
return fs.existsSync(options.explicit) ? options.explicit : undefined;
}

if (options?.env && fs.existsSync(options.env)) {
return options.env;
}

for (const candidate of DEFAULT_CHROME_CANDIDATES) {
if (fs.existsSync(candidate)) {
return candidate;
}
}

return undefined;
}
6 changes: 6 additions & 0 deletions packages/cli/src/lib/driver/command-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import type { DriverCommandName } from "./commands/types.js";
import {
autoConnectFlag,
cdpFlag,
chromeArgFlag,
chromePathFlag,
connectTimeoutFlag,
headedFlag,
headlessFlag,
localFlag,
Expand All @@ -20,7 +23,10 @@ import { runDriverCommandWithTarget } from "./runtime.js";

export const driverCommandFlags = {
"auto-connect": autoConnectFlag,
"chrome-arg": chromeArgFlag,
"chrome-path": chromePathFlag,
cdp: cdpFlag,
"connect-timeout": connectTimeoutFlag,
headed: headedFlag,
headless: headlessFlag,
local: localFlag,
Expand Down
28 changes: 25 additions & 3 deletions packages/cli/src/lib/driver/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
discoverLocalCdp,
type LocalCdpDiscovery,
} from "./local-cdp-discovery.js";
import { resolveChromeExecutablePath } from "./chrome-path.js";
import { hasExplicitDriverTarget, type DriverFlags } from "./command-cli.js";
import { resolveConnectionTarget, targetsCompatible } from "./mode.js";
import { getRemote } from "./remote-binding.js";
Expand Down Expand Up @@ -270,10 +271,31 @@ async function checkTargetPrerequisite(
deps: DoctorDeps,
): Promise<DoctorCheck | null> {
if (target.kind === "managed-local") {
const executablePath = resolveChromeExecutablePath({
explicit: target.launch?.executablePath,
env: env.CHROME_PATH,
});
if (!executablePath) {
return {
fix: "install Chrome/Chromium or pass --chrome-path / set CHROME_PATH",
message: "no Chrome executable found",
name: "browser",
status: "fail",
};
}

const mode = target.headless ? "headless" : "headed";
const details: Record<string, unknown> = { executablePath, mode };
if (target.launch?.connectTimeoutMs !== undefined) {
details.connectTimeoutMs = target.launch.connectTimeoutMs;
}
if (target.launch?.args?.length) {
details.args = target.launch.args;
}

return {
message: target.headless
? "managed local browser, headless"
: "managed local browser, headed",
details,
message: `managed local browser, ${mode}, ${executablePath}`,
name: "browser",
status: "ok",
};
Expand Down
19 changes: 19 additions & 0 deletions packages/cli/src/lib/driver/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,25 @@ export const targetIdFlag = Flags.string({
helpValue: "<target-id>",
});

export const chromePathFlag = Flags.string({
description:
"Path to the Chrome or Chromium executable for managed local sessions. Falls back to CHROME_PATH.",
helpValue: "<path>",
});

export const connectTimeoutFlag = Flags.integer({
description:
"Timeout in milliseconds when launching or connecting to a managed local browser.",
helpValue: "<ms>",
});

export const chromeArgFlag = Flags.string({
description:
"Extra Chromium flag for managed local sessions. Repeat for multiple flags.",
helpValue: "<flag>",
multiple: true,
});

export function sessionName(value?: string): string {
return value ?? process.env.BROWSE_SESSION ?? "default";
}
17 changes: 17 additions & 0 deletions packages/cli/src/lib/driver/launch-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { LocalBrowserLaunchOptions } from "@browserbasehq/stagehand";

import type { ManagedLocalLaunchOptions } from "./types.js";

export function buildManagedLocalLaunchOptions(
launch?: ManagedLocalLaunchOptions,
): LocalBrowserLaunchOptions {
return {
...(launch?.executablePath
? { executablePath: launch.executablePath }
: {}),
...(typeof launch?.connectTimeoutMs === "number"
? { connectTimeoutMs: launch.connectTimeoutMs }
: {}),
...(launch?.args?.length ? { args: launch.args } : {}),
};
}
87 changes: 82 additions & 5 deletions packages/cli/src/lib/driver/mode.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { fail } from "../errors.js";
import { getRemote } from "./remote-binding.js";
import { resolveWsTarget } from "./resolve-ws.js";
import type { ConnectionTarget } from "./types.js";
import type { ConnectionTarget, ManagedLocalLaunchOptions } from "./types.js";

export interface DriverModeFlags {
"auto-connect"?: boolean;
"chrome-arg"?: string[];
"chrome-path"?: string;
cdp?: string;
"connect-timeout"?: number;
headed?: boolean;
headless?: boolean;
local?: boolean;
Expand All @@ -28,6 +31,8 @@ function resolveHeadless(
export async function resolveConnectionTarget(
flags: DriverModeFlags,
): Promise<ConnectionTarget> {
failOnConflictingLocalLaunchFlags(flags);

if (flags.cdp) {
failOnConflictingFlags("--cdp", [
flags["auto-connect"] ? "--auto-connect" : null,
Expand Down Expand Up @@ -70,7 +75,7 @@ export async function resolveConnectionTarget(
}

if (flags.local) {
return { kind: "managed-local", headless: resolveHeadless(flags) };
return buildManagedLocalTarget(flags);
}

const autoRemote = (await getRemote()).autoSelectRemoteTarget();
Expand All @@ -82,7 +87,61 @@ export async function resolveConnectionTarget(
return autoRemote;
}

return { kind: "managed-local", headless: resolveHeadless(flags) };
return buildManagedLocalTarget(flags);
}

function buildManagedLocalTarget(
flags: DriverModeFlags,
): Extract<ConnectionTarget, { kind: "managed-local" }> {
const launch = resolveManagedLocalLaunch(flags);
return {
headless: resolveHeadless(flags),
kind: "managed-local",
...(launch ? { launch } : {}),
};
}

function resolveManagedLocalLaunch(
flags: DriverModeFlags,
): ManagedLocalLaunchOptions | undefined {
const executablePath = flags["chrome-path"] ?? process.env.CHROME_PATH;
const connectTimeoutMs = flags["connect-timeout"];
const args = flags["chrome-arg"]?.filter(Boolean);

if (!executablePath && connectTimeoutMs === undefined && !args?.length) {
return undefined;
}

return {
...(executablePath ? { executablePath } : {}),
...(connectTimeoutMs !== undefined ? { connectTimeoutMs } : {}),
...(args?.length ? { args } : {}),
};
}

function failOnConflictingLocalLaunchFlags(flags: DriverModeFlags): void {
const hasLaunchFlags = Boolean(
flags["chrome-path"] ||
flags["connect-timeout"] !== undefined ||
flags["chrome-arg"]?.length,
);
if (!hasLaunchFlags) return;

failOnConflictingFlags("--chrome-path", [
flags.cdp ? "--cdp" : null,
flags["auto-connect"] ? "--auto-connect" : null,
flags.remote ? "--remote" : null,
]);
failOnConflictingFlags("--connect-timeout", [
flags.cdp ? "--cdp" : null,
flags["auto-connect"] ? "--auto-connect" : null,
flags.remote ? "--remote" : null,
]);
failOnConflictingFlags("--chrome-arg", [
flags.cdp ? "--cdp" : null,
flags["auto-connect"] ? "--auto-connect" : null,
flags.remote ? "--remote" : null,
]);
}

function failOnConflictingFlags(
Expand All @@ -101,10 +160,28 @@ export function targetsCompatible(
right: ConnectionTarget,
): boolean {
if (left.kind !== right.kind) return false;
if (left.kind === "managed-local" && right.kind === "managed-local")
return left.headless === right.headless;
if (left.kind === "managed-local" && right.kind === "managed-local") {
return (
left.headless === right.headless &&
managedLocalLaunchCompatible(left.launch, right.launch)
);
}
if (left.kind === "cdp" && right.kind === "cdp") {
return left.endpoint === right.endpoint && left.targetId === right.targetId;
}
return true;
}

function managedLocalLaunchCompatible(
left?: ManagedLocalLaunchOptions,
right?: ManagedLocalLaunchOptions,
): boolean {
if (!left && !right) return true;
if (!left || !right) return false;

return (
left.executablePath === right.executablePath &&
left.connectTimeoutMs === right.connectTimeoutMs &&
JSON.stringify(left.args ?? []) === JSON.stringify(right.args ?? [])
);
}
2 changes: 2 additions & 0 deletions packages/cli/src/lib/driver/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "./commands/selectors.js";
import { executeDriverCommand } from "./commands/registry.js";
import type { DriverCommandName } from "./commands/types.js";
import { buildManagedLocalLaunchOptions } from "./launch-options.js";
import { discoverLocalCdp } from "./local-cdp-discovery.js";
import { NetworkCapture } from "./network-capture.js";
import { getRemote } from "./remote-binding.js";
Expand Down Expand Up @@ -280,6 +281,7 @@ export class DriverSessionManager {
env: "LOCAL",
localBrowserLaunchOptions: {
headless: target.headless,
...buildManagedLocalLaunchOptions(target.launch),
},
verbose: 0,
};
Expand Down
12 changes: 11 additions & 1 deletion packages/cli/src/lib/driver/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
export interface ManagedLocalLaunchOptions {
args?: string[];
connectTimeoutMs?: number;
executablePath?: string;
}

export type ConnectionTarget =
| { kind: "managed-local"; headless: boolean }
| {
kind: "managed-local";
headless: boolean;
launch?: ManagedLocalLaunchOptions;
}
| { kind: "remote" }
| { kind: "auto-connect" }
| { kind: "cdp"; endpoint: string; targetId?: string };
Expand Down
36 changes: 36 additions & 0 deletions packages/cli/tests/doctor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,42 @@ describe("doctor report builder", () => {
expect(report.next).toBe("browse status");
});

it("reports a missing Chrome executable for managed-local mode", async () => {
const daemonDir = await tempDaemonDir();
const report = await buildDoctorReport(
{
flags: {
"chrome-path": "/tmp/does-not-exist/chrome",
local: true,
},
session: "default",
},
{
env: { BROWSERBASE_API_KEY: "", CHROME_PATH: "" },
getDriverStatus: async () => null,
readPackageVersion: async () => "0.0.0-test",
resolveConnectionTarget: async (flags) => ({
headless: true,
kind: "managed-local",
launch: {
executablePath: flags["chrome-path"],
},
}),
},
);

expect(report).toMatchObject({
verdict: "fail",
checks: expect.arrayContaining([
expect.objectContaining({
message: "no Chrome executable found",
name: "browser",
status: "fail",
}),
]),
});
});

it("checks auto-connect discovery through injectable dependencies", async () => {
const daemonDir = await tempDaemonDir();
const previousDaemonDir = process.env.BROWSE_DAEMON_DIR;
Expand Down
23 changes: 23 additions & 0 deletions packages/cli/tests/launch-options.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, expect, it } from "vitest";

import { buildManagedLocalLaunchOptions } from "../src/lib/driver/launch-options.js";

describe("buildManagedLocalLaunchOptions", () => {
it("returns an empty object when launch options are omitted", () => {
expect(buildManagedLocalLaunchOptions()).toEqual({});
});

it("maps managed-local launch options into Stagehand options", () => {
expect(
buildManagedLocalLaunchOptions({
args: ["--renderer-process-limit=6"],
connectTimeoutMs: 30_000,
executablePath: "/opt/chrome/chrome",
}),
).toEqual({
args: ["--renderer-process-limit=6"],
connectTimeoutMs: 30_000,
executablePath: "/opt/chrome/chrome",
});
});
});
Loading
Loading