Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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