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
7 changes: 6 additions & 1 deletion packages/protocol/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -952,14 +952,18 @@ export const BrowserbaseContextSchema = z
.meta({ id: "BrowserbaseContext" });

/** Browserbase browser settings for session creation. */
export const BrowserbaseExtensionSchema = z
.enum(["onepassword", "browser-events", "stagehand"])
.meta({ id: "BrowserbaseExtension" });

export const BrowserbaseBrowserSettingsSchema = z
.object({
advancedStealth: z.boolean().optional(),
blockAds: z.boolean().optional(),
captchaImageSelector: z.string().optional(),
captchaInputSelector: z.string().optional(),
context: BrowserbaseContextSchema.optional(),
extensionId: z.string().optional(),
extensions: z.array(BrowserbaseExtensionSchema).optional(),
fingerprint: BrowserbaseFingerprintSchema.optional(),
logSession: z.boolean().optional(),
os: z.enum(["windows", "mac", "linux", "mobile", "tablet"]).optional(),
Expand All @@ -968,6 +972,7 @@ export const BrowserbaseBrowserSettingsSchema = z
verified: z.boolean().optional(),
viewport: BrowserbaseViewportSchema.optional(),
})
.strict()
.meta({ id: "BrowserbaseBrowserSettings" });

/** Browserbase managed proxy geolocation configuration. */
Expand Down
14 changes: 11 additions & 3 deletions packages/protocol/stagehand.v4.json
Original file line number Diff line number Diff line change
Expand Up @@ -1150,8 +1150,11 @@
"context": {
"$ref": "#/$defs/BrowserbaseContext"
},
"extension_id": {
"type": "string"
"extensions": {
"type": "array",
"items": {
"$ref": "#/$defs/BrowserbaseExtension"
}
},
"fingerprint": {
"$ref": "#/$defs/BrowserbaseFingerprint"
Expand All @@ -1175,7 +1178,8 @@
"viewport": {
"$ref": "#/$defs/BrowserbaseViewport"
}
}
},
"additionalProperties": false
},
"BrowserbaseContext": {
"type": "object",
Expand All @@ -1189,6 +1193,10 @@
},
"required": ["id"]
},
"BrowserbaseExtension": {
"type": "string",
"enum": ["onepassword", "browser-events", "stagehand"]
},
"BrowserbaseFingerprint": {
"type": "object",
"properties": {
Expand Down
2 changes: 2 additions & 0 deletions packages/protocol/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
BrowserbaseBrowserSettingsSchema,
BrowserbaseBrowserSourceSchema,
BrowserbaseContextSchema,
BrowserbaseExtensionSchema,
BrowserbaseFingerprintSchema,
BrowserbaseFingerprintScreenSchema,
BrowserbaseProxyConfigSchema,
Expand Down Expand Up @@ -381,6 +382,7 @@ export type ThinkingEffort = z.infer<typeof ThinkingEffortSchema>;

export type BrowserbaseBrowserSettings = z.infer<typeof BrowserbaseBrowserSettingsSchema>;
export type BrowserbaseContext = z.infer<typeof BrowserbaseContextSchema>;
export type BrowserbaseExtension = z.infer<typeof BrowserbaseExtensionSchema>;
export type BrowserbaseFingerprint = z.infer<typeof BrowserbaseFingerprintSchema>;
export type BrowserbaseFingerprintScreen = z.infer<typeof BrowserbaseFingerprintScreenSchema>;
export type BrowserbaseProxyConfig = z.infer<typeof BrowserbaseProxyConfigSchema>;
Expand Down
9 changes: 8 additions & 1 deletion packages/sdk-python/src/stagehand/_generated/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,15 @@ class BrowserGetVersionResult(WireModel):

class BrowserbaseBrowserSettings(WireModel):
model_config = ConfigDict(
extra="forbid",
validate_by_name=True,
)
advanced_stealth: Optional[StrictBool] = None
block_ads: Optional[StrictBool] = None
captcha_image_selector: Optional[StrictStr] = None
captcha_input_selector: Optional[StrictStr] = None
context: Optional[BrowserbaseContext] = None
extension_id: Optional[StrictStr] = None
extensions: Optional[list[BrowserbaseExtension]] = None
fingerprint: Optional[BrowserbaseFingerprint] = None
log_session: Optional[StrictBool] = None
os: Optional[Os] = None
Expand Down Expand Up @@ -203,6 +204,12 @@ class BrowserbaseContext(WireModel):
persist: Optional[StrictBool] = None


class BrowserbaseExtension(StrEnum):
onepassword = "onepassword"
browser_events = "browser-events"
stagehand = "stagehand"


class BrowserbaseFingerprint(WireModel):
model_config = ConfigDict(
validate_by_name=True,
Expand Down
25 changes: 24 additions & 1 deletion packages/sdk-python/tests/test_browser_source.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
from __future__ import annotations

import pytest
from pydantic import ValidationError

from stagehand.browser_source import ResolvedBrowserSource, resolve_browser_source
from stagehand.client_models import StagehandClientInitParams
from stagehand.client_models import BrowserbaseBrowserSource, StagehandClientInitParams


def test_browserbase_uploaded_extension_id_is_top_level_only() -> None:
params = StagehandClientInitParams.model_validate({
"api_key": "test",
"browser": {
"type": "browserbase",
"extension_id": "uploaded-extension",
"browser_settings": {"extensions": ["browser-events"]},
},
})

assert isinstance(params.browser, BrowserbaseBrowserSource)
assert params.browser.extension_id == "uploaded-extension"
with pytest.raises(ValidationError):
StagehandClientInitParams.model_validate({
"api_key": "test",
"browser": {
"type": "browserbase",
"browser_settings": {"extension_id": "uploaded-extension"},
},
})


@pytest.mark.asyncio
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-ts/src/browserSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export async function resolveBrowserSource(
cdpUrl: session.cdpUrl,
browserbaseSessionId: session.sessionId,
preloadedExtension: true,
residentBrowserConnection: false,
residentBrowserConnection: true,
keepAlive: browser.keepAlive ?? false,
close: session.close,
};
Expand Down
73 changes: 0 additions & 73 deletions packages/sdk-ts/src/browserbaseExtension.ts

This file was deleted.

62 changes: 25 additions & 37 deletions packages/sdk-ts/src/browserbaseSession.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import Browserbase from "@browserbasehq/sdk";
import type { BrowserbaseSessionCreateParams } from "../../protocol/types.js";
import {
createBrowserbaseExtensionClient,
provisionBrowserbaseExtension,
type BrowserbaseExtensionClient,
type BrowserbaseExtensionSdk,
type ProvisionedBrowserbaseExtension,
} from "./browserbaseExtension.js";
import type { BrowserbaseExtension, BrowserbaseSessionCreateParams } from "../../protocol/types.js";

const STAGEHAND_BROWSER_EXTENSION = "stagehand";

export type BrowserbaseSessionClient = {
createSession(
Expand All @@ -16,7 +11,7 @@ export type BrowserbaseSessionClient = {

export type BrowserbaseSessionClientFactory = (apiKey: string) => BrowserbaseSessionClient;

export type BrowserbaseApiClient = BrowserbaseExtensionClient & {
export type BrowserbaseApiClient = {
createSession(
params: BrowserbaseSessionCreateParams,
): Promise<{ id: string; connectUrl: string }>;
Expand All @@ -25,12 +20,9 @@ export type BrowserbaseApiClient = BrowserbaseExtensionClient & {

type BrowserbaseSessionClientDependencies = {
browserbase?: BrowserbaseApiClient;
provisionExtension?: (
client: BrowserbaseExtensionClient,
) => Promise<ProvisionedBrowserbaseExtension>;
};

type BrowserbaseSdk = BrowserbaseExtensionSdk & {
type BrowserbaseSdk = {
sessions: {
create(params: Browserbase.SessionCreateParams): Promise<{ id: string; connectUrl: string }>;
update(sessionId: string, params: { status: "REQUEST_RELEASE" }): Promise<unknown>;
Expand All @@ -44,27 +36,21 @@ export function createBrowserbaseSessionClient(
dependencies: BrowserbaseSessionClientDependencies = {},
): BrowserbaseSessionClient {
const browserbase = dependencies.browserbase ?? createBrowserbaseApiClient(apiKey);
const provisionExtension = dependencies.provisionExtension ?? provisionBrowserbaseExtension;

return {
async createSession(params) {
const extension = await provisionExtension(browserbase);
let session: { id: string; connectUrl: string };

try {
session = await browserbase.createSession({
...params,
extensionId: extension.extensionId,
});
session = await browserbase.createSession(withStagehandExtension(params));
} catch (error) {
await extension.cleanup().catch(() => undefined);
throw new Error("Failed to create a Browserbase session", { cause: error });
}

const sessionId = session.id.trim();
const cdpUrl = session.connectUrl.trim();
if (sessionId.length === 0 || cdpUrl.length === 0) {
await cleanupInvalidSession(browserbase, sessionId, extension);
await cleanupInvalidSession(browserbase, sessionId);
throw new Error(
sessionId.length === 0
? "Browserbase session creation returned an empty session ID"
Expand All @@ -73,7 +59,6 @@ export function createBrowserbaseSessionClient(
}

let sessionReleased = false;
let extensionCleaned = false;
return {
sessionId,
cdpUrl,
Expand All @@ -88,18 +73,7 @@ export function createBrowserbaseSessionClient(
}
}

let extensionCleanupError: unknown;
if (!extensionCleaned) {
try {
await extension.cleanup();
extensionCleaned = true;
} catch (error) {
extensionCleanupError = error;
}
}

if (releaseError) throw releaseError;
if (extensionCleanupError) throw extensionCleanupError;
},
};
},
Expand All @@ -111,10 +85,8 @@ export function createBrowserbaseApiClient(
createSdk: BrowserbaseSdkFactory = (key) => new Browserbase({ apiKey: key }),
): BrowserbaseApiClient {
const sdk = createSdk(apiKey);
const extensionClient = createBrowserbaseExtensionClient(apiKey, () => sdk);

return {
...extensionClient,
async createSession(params) {
const session = await sdk.sessions.create(params as Browserbase.SessionCreateParams);
return { id: session.id, connectUrl: session.connectUrl };
Expand All @@ -128,10 +100,26 @@ export function createBrowserbaseApiClient(
async function cleanupInvalidSession(
browserbase: BrowserbaseApiClient,
sessionId: string,
extension: ProvisionedBrowserbaseExtension,
): Promise<void> {
if (sessionId.length > 0) {
await browserbase.releaseSession(sessionId).catch(() => undefined);
}
await extension.cleanup().catch(() => undefined);
}

function withStagehandExtension(
params: BrowserbaseSessionCreateParams,
): BrowserbaseSessionCreateParams {
const extensions = [
...new Set<BrowserbaseExtension>([
...(params.browserSettings?.extensions ?? []),
STAGEHAND_BROWSER_EXTENSION,
]),
];
return {
...params,
browserSettings: {
...params.browserSettings,
extensions,
},
};
}
7 changes: 2 additions & 5 deletions packages/sdk-ts/src/clientSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,11 @@ import {
} from "../../protocol/schemas.js";
import { Page } from "./page.js";

const BrowserbaseClientBrowserSettingsSchema = BrowserbaseBrowserSettingsSchema.omit({
extensionId: true,
}).strict();
const BrowserbaseClientBrowserSettingsSchema = BrowserbaseBrowserSettingsSchema.strict();

/** Browserbase source fields exposed by the TS SDK. Stagehand provisions its own extension. */
/** Browserbase source fields exposed by the TS SDK. */
export const BrowserbaseBrowserSourceSchema = BrowserbaseSessionCreateParamsSchema.omit({
browserSettings: true,
extensionId: true,
})
.extend({
type: z.literal("browserbase"),
Expand Down
4 changes: 0 additions & 4 deletions packages/sdk-ts/src/extensionAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ import { fileURLToPath } from "node:url";

const packageRoot = new URL("../", import.meta.url);

export const STAGEHAND_EXTENSION_ARCHIVE_PATH = fileURLToPath(
new URL("dist/assets/stagehand-extension.zip", packageRoot),
);

export const STAGEHAND_EXTENSION_DIRECTORY_PATH = fileURLToPath(
new URL("dist/extension/", packageRoot),
);
Loading
Loading